In previous part we have seen how to draw a debug circle with rotation in 3D space in Unity project. This tutorial will introduce concepts of circle arc, segment and sector which can be particularly interesting for debugging multiple gameplay elements, for example, NPC field of view, radar cone, circle overlaps, but MOST importantly, they will allow us to draw a Debug Pizza.
This tutorial is a part of tutorial series Unity – Draw Custom Debug Shapes
Circle Arc
Circle arc is part of the circumference of a circle. Although arc is usually defined with two points, in the context of game development it is more useful to define it using two angles. Given a circle and two angles along its circumference, it is possible to define two arcs:
- Minor arc – shorter arc between two points
- Major arc – longer arc between two points
Minor arc (blue) and major arc (red) are shown on the following image:
By default, arcs between two angles are drawn in counter-clockwise direction. This might not be optimal in most cases, so we will need to introduce a way to to select direction. Additionally, this will introduce a small issue when drawing an arc which starts at an angle greater than 180° and ends at the angle lesser than 180° (as show on the previous image).
To prevent this issue, while still maintaining some degree of convenience for end user (potentially game designer, or another developer), our method will internally calculate the arc between 0° and the span between the defined start and end angle, and then offset it by the start angle.
As with the previous tutorials, we will start with the most basic implementation of the arc drawing method, which only takes start angle, end angle, radius and drawing color as parameters. For direction, it is dictated by the difference between starting and ending angle. If (mapped to 0°-360° range) start angle is lesser than end angle, arc will be drawn counterclockwise, otherwise, it will be drawn clockwise.
public static void DrawArc(float startAngle, float endAngle, float radius, Color color, int arcSegments = 32)
{
float arcSpan = Mathf.DeltaAngle(startAngle, endAngle);
// Since Mathf.DeltaAngle returns a signed angle of the shortest path between two angles, it
// is necessary to offset it by 360.0 degrees to get a positive value
if (arcSpan <= 0)
{
arcSpan += 360.0f;
}
// angle step is calculated by dividing the arc span by number of approximation segments
float angleStep = (arcSpan / arcSegments) * Mathf.Deg2Rad;
float stepOffset = startAngle * Mathf.Deg2Rad;
// stepStart, stepEnd, lineStart and lineEnd variables are declared outside of the following for loop
float stepStart = 0.0f;
float stepEnd = 0.0f;
Vector3 lineStart = Vector3.zero;
Vector3 lineEnd = Vector3.zero;
for (int i = 0; i < arcSegments; i++)
{
// Calculate approximation segment start and end, and offset them by start angle
stepStart = angleStep * i + stepOffset;
stepEnd = angleStep * (i + 1) + stepOffset;
lineStart.x = Mathf.Cos(stepStart);
lineStart.y = Mathf.Sin(stepStart);
lineStart *= radius;
lineEnd.x = Mathf.Cos(stepEnd);
lineEnd.y = Mathf.Sin(stepEnd);
lineEnd *= radius;
DrawLine(lineStart, lineEnd, color);
}
}
To make a few examples, we can use this method in the Update of Example class:
void Update()
{
Debug.DrawArc(-45, 45, 1.0f, Color.green);
Debug.DrawArc(45, -45, 0.9f, Color.magenta);
Debug.DrawArc(45, 135, 0.8f, Color.red);
Debug.DrawArc(135, 45, 0.7f, Color.yellow);
Debug.DrawArc(180, 0, 0.6f, Color.cyan);
Debug.DrawArc(0, 180, 0.5f, Color.white);
}
This example results in the following image:
Circle Segment
Circle segment is a region bounded by an arc of the circle and a line segment connecting the arc’s starting and ending point. This line segment is also named chord.
Depending on which arc is used to define a segment, segments are also split in two types: minor and major. These segments are shown on the following image:
We can extend the DrawArc method to include a flag if the chord should be drawn, and therefore the segment. Besides extending the method signature with the drawChord argument is is necessary to store arc start point (arcStart) and arc end point (arcEnd), and simply connect them using the Debug.DrawLine method.
public static void DrawArc(float startAngle, float endAngle, float radius, Color color, bool drawChord = false, int arcSegments = 32)
{
float arcSpan = Mathf.DeltaAngle(startAngle, endAngle);
// Since Mathf.DeltaAngle returns a signed angle of the shortest path between two angles, it
// is necessary to offset it by 360.0 degrees to get a positive value
if (arcSpan <= 0)
{
arcSpan += 360.0f;
}
// angle step is calculated by dividing the arc span by number of approximation segments
float angleStep = (arcSpan / arcSegments) * Mathf.Deg2Rad;
float stepOffset = startAngle * Mathf.Deg2Rad;
// stepStart, stepEnd, lineStart and lineEnd variables are declared outside of the following for loop
float stepStart = 0.0f;
float stepEnd = 0.0f;
Vector3 lineStart = Vector3.zero;
Vector3 lineEnd = Vector3.zero;
// arcStart and arcEnd need to be stored to be able to draw segment chord
Vector3 arcStart = Vector3.zero;
Vector3 arcEnd = Vector3.zero;
for (int i = 0; i < arcSegments; i++)
{
// Calculate approximation segment start and end, and offset them by start angle
stepStart = angleStep * i + stepOffset;
stepEnd = angleStep * (i + 1) + stepOffset;
lineStart.x = Mathf.Cos(stepStart);
lineStart.y = Mathf.Sin(stepStart);
lineStart *= radius;
lineEnd.x = Mathf.Cos(stepEnd);
lineEnd.y = Mathf.Sin(stepEnd);
lineEnd *= radius;
// If this is the first iteration, set the chordStart
if(i == 0)
{
arcStart = lineStart;
}
// If this is the last iteration, set the chordEnd
if(i == arcSegments - 1)
{
arcEnd = lineEnd;
}
DrawLine(lineStart, lineEnd, color);
}
if (drawChord)
{
DrawLine(arcStart, arcEnd, color);
}
}
Using the Example class we can demonstrate this new functionality:
void Update()
{
Debug.DrawArc(-35, 45, 1.0f, Color.cyan, true);
Debug.DrawArc(45, -35, 1.0f, Color.red, false);
}
Following image displays the result:
Circle sector
Similar to the circle segment, circle sector represents a region which is bounded with a circle arc, and (instead of a single) two line segments named rays. Circle sector requires two ray:
- Connecting arc start with the circle origin,
- Connecting arc end with the circle origin.
As with the circle arcs and segments, sectors too can be minor and major, depending on which arc defines them.
Similarly to the circle segment implementation, we need to extend the signature of DrawArc method with a drawSector argument. Only change that is needed in the method implementation, if the drawSector is true, to connect arcStart and arcEnd with the circle origin (at this moment it is still (0,0,0)).
public static void DrawArc(float startAngle, float endAngle, float radius, Color color, bool drawChord = false, bool drawSector = false, int arcSegments = 32)
{
float arcSpan = Mathf.DeltaAngle(startAngle, endAngle);
// Since Mathf.DeltaAngle returns a signed angle of the shortest path between two angles, it
// is necessary to offset it by 360.0 degrees to get a positive value
if (arcSpan <= 0)
{
arcSpan += 360.0f;
}
// angle step is calculated by dividing the arc span by number of approximation segments
float angleStep = (arcSpan / arcSegments) * Mathf.Deg2Rad;
float stepOffset = startAngle * Mathf.Deg2Rad;
// stepStart, stepEnd, lineStart and lineEnd variables are declared outside of the following for loop
float stepStart = 0.0f;
float stepEnd = 0.0f;
Vector3 lineStart = Vector3.zero;
Vector3 lineEnd = Vector3.zero;
// arcStart and arcEnd need to be stored to be able to draw segment chord
Vector3 arcStart = Vector3.zero;
Vector3 arcEnd = Vector3.zero;
// arcOrigin represents an origin of a circle which defines the arc
Vector3 arcOrigin = Vector3.zero;
for (int i = 0; i < arcSegments; i++)
{
// Calculate approximation segment start and end, and offset them by start angle
stepStart = angleStep * i + stepOffset;
stepEnd = angleStep * (i + 1) + stepOffset;
lineStart.x = Mathf.Cos(stepStart);
lineStart.y = Mathf.Sin(stepStart);
lineStart *= radius;
lineEnd.x = Mathf.Cos(stepEnd);
lineEnd.y = Mathf.Sin(stepEnd);
lineEnd *= radius;
// If this is the first iteration, set the chordStart
if(i == 0)
{
arcStart = lineStart;
}
// If this is the last iteration, set the chordEnd
if(i == arcSegments - 1)
{
arcEnd = lineEnd;
}
DrawLine(lineStart, lineEnd, color);
}
if (drawChord)
{
DrawLine(arcStart, arcEnd, color);
}
if (drawSector)
{
DrawLine(arcStart, arcOrigin, color);
DrawLine(arcEnd, arcOrigin, color);
}
}
Using the Example class we can demonstrate this new functionality:
void Update()
{
Debug.DrawArc(-35, 45, 1.0f, Color.cyan, false, true);
Debug.DrawArc(45, -35, 1.0f, Color.red);
}
Following image displays the result:
Position and Rotation
To make the newly created DrawArc method, we can extend it with position and orientation arguments. Details of this implementation are discussed in detail the previous tutorial, and the same implementation is still valid for this example:
public static void DrawArc(float startAngle, float endAngle,
Vector3 position, Quaternion orientation, float radius,
Color color, bool drawChord = false, bool drawSector = false,
int arcSegments = 32)
{
float arcSpan = Mathf.DeltaAngle(startAngle, endAngle);
// Since Mathf.DeltaAngle returns a signed angle of the shortest path between two angles, it
// is necessary to offset it by 360.0 degrees to get a positive value
if (arcSpan <= 0)
{
arcSpan += 360.0f;
}
// angle step is calculated by dividing the arc span by number of approximation segments
float angleStep = (arcSpan / arcSegments) * Mathf.Deg2Rad;
float stepOffset = startAngle * Mathf.Deg2Rad;
// stepStart, stepEnd, lineStart and lineEnd variables are declared outside of the following for loop
float stepStart = 0.0f;
float stepEnd = 0.0f;
Vector3 lineStart = Vector3.zero;
Vector3 lineEnd = Vector3.zero;
// arcStart and arcEnd need to be stored to be able to draw segment chord
Vector3 arcStart = Vector3.zero;
Vector3 arcEnd = Vector3.zero;
// arcOrigin represents an origin of a circle which defines the arc
Vector3 arcOrigin = position;
for (int i = 0; i < arcSegments; i++)
{
// Calculate approximation segment start and end, and offset them by start angle
stepStart = angleStep * i + stepOffset;
stepEnd = angleStep * (i + 1) + stepOffset;
lineStart.x = Mathf.Cos(stepStart);
lineStart.y = Mathf.Sin(stepStart);
lineStart.z = 0.0f;
lineEnd.x = Mathf.Cos(stepEnd);
lineEnd.y = Mathf.Sin(stepEnd);
lineEnd.z = 0.0f;
// Results are multiplied so they match the desired radius
lineStart *= radius;
lineEnd *= radius;
// Results are multiplied by the orientation quaternion to rotate them
// since this operation is not commutative, result needs to be
// reassigned, instead of using multiplication assignment operator (*=)
lineStart = orientation * lineStart;
lineEnd = orientation * lineEnd;
// Results are offset by the desired position/origin
lineStart += position;
lineEnd += position;
// If this is the first iteration, set the chordStart
if (i == 0)
{
arcStart = lineStart;
}
// If this is the last iteration, set the chordEnd
if(i == arcSegments - 1)
{
arcEnd = lineEnd;
}
DrawLine(lineStart, lineEnd, color);
}
if (drawChord)
{
DrawLine(arcStart, arcEnd, color);
}
if (drawSector)
{
DrawLine(arcStart, arcOrigin, color);
DrawLine(arcEnd, arcOrigin, color);
}
}
Debug Draw Pizza
With all technicalities out of the way, we can make the pièce de résistance of this tutorial: Drawing a debug pizza!
We need the following ingredients:
- Pizza slice (minor sector)
- Rest of the pie (major sector)
- Toppings (minor and major segments, depending on the location)
void DrawDebugPizza()
{
// Draw a slice
Vector3 sliceOffset = (Vector3.up + Vector3.right) * 0.16f;
Debug.DrawArc(20, 75, transform.position + sliceOffset, transform.rotation, 1.0f, Color.yellow, false, true);
Debug.DrawArc(20, 75, transform.position + sliceOffset, transform.rotation, 0.85f, Color.yellow, false, true);
// Draw the rest of the pie
Debug.DrawArc(75, 20, transform.position, transform.rotation, 1.0f, Color.yellow, false, true);
Debug.DrawArc(75, 20, transform.position, transform.rotation, 0.85f, Color.yellow, false, true);
// Draw full toppings
Debug.DrawCircle(transform.position + new Vector3(-0.4f, 0.2f,0), transform.rotation, 0.22f, 16, Color.red);
Debug.DrawCircle(transform.position + new Vector3(0.5f, -0.35f,0), transform.rotation, 0.17f, 16, Color.red);
Debug.DrawCircle(transform.position + new Vector3(-0.1f, -0.5f,0), transform.rotation, 0.22f, 16, Color.red);
// Draw sliced toppings
Debug.DrawArc(242, 88, transform.position + sliceOffset + new Vector3(0.2f, 0.55f, 0), transform.rotation, 0.20f, Color.red, true);
Debug.DrawArc(88, 242, transform.position + new Vector3(0.2f, 0.55f, 0), transform.rotation, 0.20f, Color.red, true);
Debug.DrawArc(29, 192, transform.position + sliceOffset + new Vector3(0.5f, 0.15f, 0), transform.rotation, 0.20f, Color.red, true);
Debug.DrawArc(192, 29, transform.position + new Vector3(0.5f, 0.15f, 0), transform.rotation, 0.20f, Color.red, true);
}
And finally, we can by calling this method from Update of the Example class, we get our own Debug Pizza:
The following part covers some basic C# concepts to make the extension functions a bit more useful and convenient.