Through the previous four parts of this tutorial we have seen how to draw a debug circle in Unity, how to rotate it in 3D space and how to draw arcs, segments and sectors of a circle.
In this part we will cover some basic C# concepts to make the extension functions a bit more useful and convenient. Examples show in this tutorial are just a first step from which you should be able to modify the code according to your needs.
Drawing on a Plane
In many cases (at least in my experience) drawing a shape is handled within an existing plane of the 3D space. For convenience, these planes can be named based on the axes that define them, namely: XY, XZ and YZ.
This can be accomplished in two ways, either having three dedicated methods, or by passing the desired plane identifier as a parameter. For the sake of explaining the both approaches we can also do a hybrid of those two.
Let’s start with drawing a circle in XY plane (as it is also the most common in first person, and 2D games made in Unity, since Unity uses Y as vertical axis). First thing to do is to modify the signature of the DrawCircle method, so that the number of segments has a default value of 16. This is done purely for convenience, but also to match the signature of the DrawArc method:
public static void DrawCircle(Vector3 position, Quaternion orientation, float radius, Color color, int segments = 16)
With this modification out of the way, we can continue with the next step and create the DrawCircleXY method. The only role of this method is to call the existing DrawCircle method and pass the Quaternion.idenitity as the orientation parameter. Since this orientation will be constant, the signature of DrawCircleXY does not need to contain it:
public static void DrawCircleXY(Vector3 position, float radius, Color color, int segments = 16)
{
DrawCircle(position, Quaternion.identity, radius, color, segments);
}
Same approach can be used for the remaining two planes XZ and YZ, where the only difference is that we need to pass a different orientation. In the case of XZ, we need to use (0, -90°, 0°) and in the case of YZ we need to use (90°, 0°, 0°).
public static void DrawCircleXZ(Vector3 position, float radius, Color color, int segments = 16)
{
DrawCircle(position, Quaternion.Euler(0, -90, 0), radius, color, segments);
}
public static void DrawCircleYZ(Vector3 position, float radius, Color color, int segments = 16)
{
DrawCircle(position, Quaternion.Euler(90, 0, 0), radius, color, segments);
}
The second approach, as previously mentioned, is to create a method which takes the plane identifier as an parameter. The most common way of defining this parameter would be to use enum. An enum is a value type that defines a number of named constants. In this case we need to create an enum called DrawingPlane and its constants can be named simply to match the planes: XY, XZ, YZ. By default, these constants will have an underlaying type int.
public enum DrawingPlane
{
XY,
XZ,
YZ
}
Next thing that we need to do is to overload the DrawCircle method. Simply described, method overloading is a way to implement multiple methods with the same name, but with a different signature. This can be useful if we need methods with the same name, but with a different number, type or an order of parameters. Overloaded version of DrawCircle method needs to include the DrawingPlane enum, and based on its value call the appropriate method for drawing the circle in a specific plane. The decision which method to call is handled through a switch statement (default case of this statement calls the same method as the DrawingPlane.XY case):
public static void DrawCircle(Vector3 position, DrawingPlane drawingPlane, float radius, Color color, int segments = 16)
{
switch (drawingPlane)
{
case DrawingPlane.XY:
DrawCircleXY(position, radius, color, segments);
break;
case DrawingPlane.XZ:
DrawCircleXZ(position, radius, color, segments);
break;
case DrawingPlane.YZ:
DrawCircleYZ(position, radius, color, segments);
break;
default:
DrawCircleXY(position, radius, color, segments);
break;
}
}
To test this functionality, we can use the following code in the Example class:
void Update()
{
Debug.DrawCircleXY(Vector3.zero, 1, Color.blue, 32);
Debug.DrawCircleXZ(Vector3.zero, 1, Color.red, 32);
Debug.DrawCircleYZ(Vector3.zero, 1, Color.green, 32);
Debug.DrawCircle(Vector3.zero, DrawingPlane.XY, 0.5f, Color.blue, 32);
Debug.DrawCircle(Vector3.zero, DrawingPlane.XZ, 0.5f, Color.red, 32);
Debug.DrawCircle(Vector3.zero, DrawingPlane.YZ, 0.5f, Color.green, 32);
}
The result of this code can be seen in the following image (for better preview, camera location is set to (5, 4, -5), rotation to (30, -45, 0) and FOV to 30 degrees):
Same logic can be applied to the DrawArc method. The following code implements exactly the same concepts as the previous code, and it can be seen below:
public static void DrawArcXY(float startAngle, float endAngle,
Vector3 position, float radius, Color color, bool drawChord = false,
bool drawSector = false, int arcSegments = 16)
{
DrawArc(startAngle, endAngle, position, Quaternion.identity, radius,
color, drawChord, drawSector, arcSegments);
}
public static void DrawArcXZ(float startAngle, float endAngle,
Vector3 position, float radius, Color color, bool drawChord = false,
bool drawSector = false, int arcSegments = 16)
{
DrawArc(startAngle, endAngle, position, Quaternion.Euler(0, -90, 0), radius,
color, drawChord, drawSector, arcSegments);
}
public static void DrawArcYZ(float startAngle, float endAngle,
Vector3 position, float radius, Color color, bool drawChord = false,
bool drawSector = false, int arcSegments = 16)
{
DrawArc(startAngle, endAngle, position, Quaternion.Euler(90, 0, 0), radius,
color, drawChord, drawSector, arcSegments);
}
public static void DrawArc(float startAngle, float endAngle,
Vector3 position, DrawingPlane drawingPlane, float radius,
Color color, bool drawChord = false, bool drawSector = false,
int arcSegments = 16)
{
switch (drawingPlane)
{
case DrawingPlane.XY:
DrawArcXY(startAngle, endAngle, position, radius, color,
drawChord, drawSector, arcSegments);
break;
case DrawingPlane.XZ:
DrawArcXZ(startAngle, endAngle, position, radius, color,
drawChord, drawSector, arcSegments);
break;
case DrawingPlane.YZ:
DrawArcYZ(startAngle, endAngle, position, radius, color,
drawChord, drawSector, arcSegments);
break;
default:
DrawArcXY(startAngle, endAngle, position, radius, color,
drawChord, drawSector, arcSegments);
break;
}
}
We can test this new code using our Example class:
void Update()
{
Debug.DrawArcXY(0.0f, 90.0f, Vector3.zero, 1.0f, Color.blue, false, true, 16);
Debug.DrawArcXZ(90.0f, 180.0f, Vector3.zero, 1.0f, Color.red, false, true, 16);
Debug.DrawArcYZ(-90.0f, 0.0f, Vector3.zero, 1.0f, Color.green, false, true, 16);
Debug.DrawArc(0.0f, 90.0f, Vector3.zero, DrawingPlane.XY, 0.5f, Color.blue, false, true, 16);
Debug.DrawArc(90.0f, 180.0f, Vector3.zero, DrawingPlane.XZ, 0.5f, Color.red, false, true, 16);
Debug.DrawArc(-90.0f, 0.0f, Vector3.zero, DrawingPlane.YZ, 0.5f, Color.green, false, true, 16);
}
The result of this code can be seen in the following image:
Coming Up!
The following tutorials will address how to expand the Debug class with multiple methods for drawing triangles and rectangles. Stay tuned!