Immediate-mode shape drawing in world and screen space that you reach for when you need to see what your AI sight cones, pathfinding nodes, hitboxes, and spatial queries are actually doing at runtime.
Summary
DebugDraw utils provide a single static entry point with the main intent of making invisible game state visible. A call like DebugDraw.Circle(transform.position, 1f) enqueues a draw command from anywhere in your code; at the end of every frame the system batches every queued command into one mesh per coordinate space and submits it through Graphics.DrawMesh. There is no bootstrap step, no scene object to drag in, and no module dependency. The first call in a Play session triggers auto-initialization via [RuntimeInitializeOnLoadMethod] and everything after that follows automatically.
Two coordinate spaces are supported:
- World-space shapes use
ZTest LEqualso they are occluded by scene geometry that lies between them and the camera, matching the behavior of Unity’s built-inDebug.DrawLine. - Screen-space shapes use
ZTest Alwaysand pixel coordinates with the origin at the bottom-left of the Game View, which is what you want for HUD-style overlays, on-screen crosshairs, and per-camera widgets.
Every shape accepts a duration argument: zero means one frame, a positive value persists across frames until the duration expires. DebugDraw.Clear discards every persistent command early when the visualization needs to reset – on a level switch, mode change, or replay scrub.
The shape catalog covers the most common debug visualization needs without a third-party gizmo asset: 2D primitives (line, polyline, triangle, rectangle, square, circle, ellipse, regular polygon, hexagon, octagon, pentagon), 3D wireframes (box, sphere), directional markers (arrow, cross, color-coded axes), and a grid. Lines render as camera-facing quads so their pixel width stays consistent regardless of distance. Outline thickness, color, and circle tessellation all default to sensible values you can override globally (DebugDraw.DefaultColor, DebugDraw.DefaultThickness, DebugDraw.DefaultSegments) or per-call.
The reason this matters is that gameplay code is much easier to reason about when its intermediate state is visible. Spatial queries, raycasts, AI perception cones, bounding boxes, path waypoints, trigger volumes, navigation grids, force vectors, hit results – every one of these has an obvious shape in the world, and DebugDraw lets you draw it inline with a single call. The auto-initialization, the absence of any Editor-only restriction, and the per-frame batching make it cheap enough to leave the calls in place during development without ever paying a bootstrap or render-cost penalty.
Use the debug draw utilities for:
- Visualizing spatial queries (raycasts, overlap checks, sphere casts, nearest-neighbor results) inline with the code that performs them. See Spatial Structures for the query APIs that produce these results.
- Drawing AI perception primitives: vision cones, hearing radii, threat awareness rings, last-known-position markers.
- Marking gameplay events with persistent shapes: hit locations, damage numbers, spawn points, waypoint trails that survive across frames.
- Building HUD-style debug overlays in screen space: on-screen crosshairs, frame budgets, target reticles, attention markers.
- Visualizing procedural generation output (room outlines, corridor paths, cell grids) before the game itself renders the result. See Grid for the grid data structures behind those cells.
- Sanity-checking transforms and orientations during development by drawing color-coded axes at the origin of any pivot.
Features
- Static
DebugDrawentry point with auto-initialization. No bootstrap, no scene object, no module dependency. The first call in a Play session triggers[RuntimeInitializeOnLoadMethod]and the rest just works from anywhere in your code. - World and screen coordinate spaces.
DebugDrawSpace.WorldusesZTest LEqualand Unity world coordinates so shapes occlude correctly;DebugDrawSpace.Screenuses pixel coordinates from the bottom-left of the Game View withZTest Alwaysfor HUD-style overlays. Every shape that supports both takes aspaceargument. - One-frame and persistent shapes. Every call accepts a
durationargument: zero (the default) draws for the current frame and discards; a positive value persists across frames until the expiry time is reached.DebugDraw.Cleardiscards every persistent command early. - Per-frame mesh batching. Every queued command is batched into one mesh per coordinate space at the end of the frame and submitted through
Graphics.DrawMesh. Hundreds of shapes cost one draw call per space, not one per shape. - Broad 2D and 3D shape catalog. Line, polyline, triangle, rectangle, square, circle, ellipse, regular polygon, hexagon, octagon, pentagon, wireframe box, wireframe sphere, arrow, cross, color-coded axes, and a configurable grid.
- Camera-facing line quads. Lines render as billboarded quads so their on-screen pixel width stays stable as the camera moves. World-space thickness is in world units; screen-space thickness is in pixels.
- Outlined and filled variants. Every closed shape exposes both forms via a
filledparameter. Outlined shapes use thethicknesssetting; filled shapes ignore it and use a triangle fan. - Global defaults plus per-call overrides.
DebugDraw.DefaultColor,DefaultThickness, andDefaultSegmentsset the look once at startup; every shape method also accepts the same parameters per call when a specific shape needs to deviate. - Multi-camera screen-space support. Screen-space shapes are submitted once per active Game camera, transformed through that camera’s viewport, so split-screen and multi-camera setups render correctly without manual replication.
- Scene View support for world shapes. Open Scene View panels receive every world-space command in addition to the Game cameras. Editor-time inspection of runtime debug visualization is automatic.
MaxCommandsper-frame cap. A hard ceiling on queued commands prevents runaway loops from saturating mesh build cost. Default 4096; raise it only when a real workload needs more shapes per frame.- Lightweight to leave in place. Disabled or empty frames cost nothing; the queue is drained per frame and the cost scales with the number of active shapes, not with the number of
DebugDrawcalls compiled into the project.
Coordinate spaces
A “coordinate space” picks how the X, Y, Z numbers in a call are interpreted before the mesh is submitted to the GPU. Two values cover the meaningful cases for debug visualization; the framework handles every other rendering concern (depth testing, per-camera transformation, mesh batching) internally and identically across the two spaces.
| Space | Origin | Depth test | Cameras | Use for |
|---|---|---|---|---|
World | Unity world space | LEqual | All active Game cameras plus open Scene View panels | Anything that has a position in the level: raycast results, AI cones, bounding volumes, world-space waypoints. Shapes are occluded by closer scene geometry, the same as a normal mesh. |
Screen | Bottom-left of Game View, in pixels | Always | All active Game cameras (one mesh per camera, transformed using each camera’s viewport) | HUD-style overlays drawn on top of everything else: crosshairs, on-screen labels, per-camera widgets. Coordinates are pixel-exact: (Screen.width / 2, Screen.height / 2) is the centre of the Game View. Scene View panels never receive screen-space shapes because pixel coordinates have no stable Scene View interpretation. |
The default space for every shape method is World. A handful of helpers are world-only by design (WireBox, WireSphere, Axes, Grid) and do not expose a space parameter; the rest take an optional DebugDrawSpace space = DebugDrawSpace.World parameter at the end of their argument list.
A few other concepts appear throughout the API and are worth defining up front. Thickness is the visual width of a line, measured in world units for world-space shapes and in pixels for screen-space shapes; the default is 0.02 world units / 2 pixels. Lines are rendered as camera-facing quads, so their on-screen width remains stable as the camera moves. Segments is the number of straight edges used to tessellate a curve (circle, ellipse); higher values look smoother but cost more vertices. Duration is how long in seconds a shape stays alive: zero (the default) means draw for the current frame and discard, a positive value keeps the command in the persistent list until the expiry time is reached. Filled vs outlined is a per-shape boolean: outlined shapes use the thickness parameter, filled shapes ignore it.
Recipes
Each recipe below is a self-contained answer to one game-development problem. You can jump straight to the one that matches what you’re building. The first recipe sets up the global defaults and introduces the API; the rest are independent but assume you’re comfortable with that foundation.
Configure global defaults and draw your first shape
Problem. You’re adding debug overlays to an existing project and you want every shape to use your project’s color scheme by default – bright yellow against a dark environment – rather than the system green. You also want to confirm the system actually works before building anything more complex.
Solution. Set the global defaults once at startup and call any shape method from Update. The very first call triggers auto-initialization; everything else is configuration. Every shape method accepts the same per-call overrides if a specific shape needs to deviate.
using Scylla.Core.Util.DebugDraw;
using UnityEngine;
/* The simplest invocation: one-frame green circle on the XZ plane at the origin.
* Color defaults to DebugDraw.DefaultColor (Color.green), thickness to
* DebugDraw.DefaultThickness (0.02 world units), segments to
* DebugDraw.DefaultSegments (32), duration to 0 (one frame). */
DebugDraw.Circle(Vector3.zero, radius: 1f);
/* Override the global defaults once at startup. Every subsequent call that
* passes no explicit color/thickness/segments will use the new values.
* Defaults reset to their compile-time values on every domain reload. */
DebugDraw.DefaultColor = Color.yellow;
DebugDraw.DefaultThickness = 0.05f;
DebugDraw.DefaultSegments = 48;
/* MaxCommands caps the per-frame queue. The default of 4096 covers virtually
* every debug scenario; raise it only if a real workload genuinely needs more
* shapes per frame, since each additional command increases mesh build cost. */
DebugDraw.MaxCommands = 8192;
Visualize an AI path or navmesh route
Problem. Your pathfinding AI is picking routes that look wrong in gameplay but the logic reads correctly. You need to see the evaluated waypoints and the final path in the world so you can tell whether the query itself is producing bad data or the movement code is ignoring a waypoint.
Solution. Use Line and Polyline to draw the path inline inside the code that computes it. Polyline with closed: false is the fastest way to trace an ordered list of waypoints in a single call. Line handles individual segments, hit normals, and anything that is not an ordered sequence.
/* Single line segment in world space. Defaults apply. Useful for drawing the
* current movement vector from the AI's position to its next waypoint. */
DebugDraw.Line(Vector3.zero, new Vector3(0f, 0f, 5f));
/* Same line in red, twice the default thickness, persistent for 3 seconds.
* The canonical "mark a hit normal" call: draw the normal at the impact point
* and let it fade so you can see the last N impacts even after the entity moves. */
DebugDraw.Line(
start: hitOrigin,
end: hitOrigin + hitNormal * 2f,
color: Color.red,
thickness: 0.04f,
duration: 3f
);
/* Polyline connecting a list of waypoints. Use this to visualize the full path
* the pathfinder returned in one call. Pass `closed: true` to outline a closed
* polygon; the system silently no-ops if the list is null or has fewer than
* two points. */
var waypoints = new List<Vector3>
{
new Vector3(0f, 0f, 0f),
new Vector3(2f, 0f, 1f),
new Vector3(4f, 0f, 0f),
new Vector3(3f, 0f, -2f)
};
DebugDraw.Polyline(waypoints, closed: true, color: Color.cyan);
/* Polyline as a screen-space HUD trail. Pixel coordinates, origin bottom-left.
* Useful for drawing a minimap path or an on-screen breadcrumb trail that
* follows the player across the Game View. */
var trail = new List<Vector3>
{
new Vector3(100f, 100f, 0f),
new Vector3(200f, 150f, 0f),
new Vector3(300f, 120f, 0f)
};
DebugDraw.Polyline(trail, closed: false, space: DebugDrawSpace.Screen);
Draw collision bounds and trigger areas
Problem. You’re debugging a trigger that fires too early or too late – maybe an AI detection zone or a damage area for an AoE (area of effect) ability. You need to see the actual bounds the code is testing against, not the collider Gizmos that only show up in the Scene View.
Solution. Triangle, Rectangle, Square, Circle, and Ellipse all accept a filled boolean. Draw the filled version of any shape for area markers that need to read at a glance (the AI’s filled vision cone, the AoE impact zone); use the outlined version for volume boundaries where you need to see through the shape (the bounding box of a selected unit, the radius of an ability). Circle accepts a normal vector that controls which plane it lies in; Ellipse accepts a full Quaternion rotation because it has a defined local XY orientation.
/* Outlined triangle at three explicit vertices. Useful for visualizing a
* triangular trigger or a directional vision arc defined by three points. */
DebugDraw.Triangle(
a: new Vector3(0f, 0f, 0f),
b: new Vector3(1f, 0f, 0f),
c: new Vector3(0.5f, 0f, 1f),
filled: false,
color: Color.magenta
);
/* Filled rectangle (size is full width/height, not half-extent), rotated.
* Semi-transparent orange marks the AoE damage footprint so the player can
* see exactly which tiles are affected. */
DebugDraw.Rectangle(
center: transform.position,
size: new Vector2(2f, 1f),
filled: true,
color: new Color(1f, 0.5f, 0f, 0.5f),
rotation: Quaternion.Euler(0f, 45f, 0f)
);
/* Square (equal sides), outlined, default rotation. The canonical tile-overlap
* marker: draw one square per cell the physics query finds occupied. */
DebugDraw.Square(transform.position, size: 0.5f, filled: false);
/* Circle on the XZ ground plane: normal defaults to Vector3.up.
* The range indicator for an ability or the aggro radius of an enemy. */
DebugDraw.Circle(transform.position, radius: 1.5f);
/* Same circle tilted to match a slope normal, so the range ring hugs the
* terrain surface rather than cutting through it. */
DebugDraw.Circle(transform.position, radius: 1.5f, normal: hitNormal, color: Color.green);
/* Filled circle on a vertical wall (XY plane, facing the Z axis). Useful for
* visualizing a wall-based hit zone or a decal placement area. */
DebugDraw.Circle(wallHitPoint, radius: 0.3f, filled: true, normal: Vector3.forward, color: Color.red);
/* Ellipse with independent X and Y radii and a full rotation quaternion.
* Use when the collision area is oval rather than circular (e.g. a depth-of-
* focus ring around a camera target, or a non-uniform attack arc). */
DebugDraw.Ellipse(
center: transform.position + Vector3.up * 0.1f,
radiusX: 1.5f,
radiusY: 0.7f,
filled: false,
rotation: Quaternion.Euler(-90f, 0f, 0f) /* lay flat on XZ */
);
Mark tile grids and hex map cells
Problem. You’re building a hex-based strategy map or a tile-based puzzle and you want to see each cell boundary while you’re debugging placement logic. Hexagons show up everywhere in game design – strategy maps, building grids, tile-based puzzles, even decorative UI – so the framework makes the common shapes first-class citizens.
Solution. RegularPolygon draws an N-sided polygon inscribed in a circle of the given radius (the “circumscribed” radius: each vertex sits exactly on that circle). Hexagon, Octagon, and Pentagon are convenience wrappers for the common counts. For your tile grid visualization, pair these with the Grid data structures to iterate the cells you want to highlight.
/* Outlined hexagon flat on the ground for tile visualization. Call this once
* per hex cell in the grid to see the full lattice during pathfinding debugging. */
DebugDraw.Hexagon(tileCenter, radius: 0.5f, color: Color.cyan);
/* Filled pentagon as a marker, rotated to align with the unit's motion direction.
* The arrowhead-like shape communicates directionality at a glance. */
DebugDraw.Pentagon(
center: markerPos,
radius: 0.4f,
filled: true,
color: Color.yellow,
rotation: Quaternion.LookRotation(velocity)
);
/* Eight-sided polygon for a stop-sign-style danger marker. Semi-transparent
* fill marks the area without blocking the view of what's inside it. */
DebugDraw.Octagon(dangerSpot, radius: 0.6f, filled: true, color: new Color(1f, 0f, 0f, 0.4f));
/* Custom side count: 12 for a clock face or circular timer ring. Values below
* 3 are clamped to 3 (the smallest closed polygon). */
DebugDraw.RegularPolygon(clockOrigin, radius: 1f, sides: 12, color: Color.white);
Inspect 3D bounding volumes and hit zones
Problem. Anyone who has debugged a collider for the first time knows the frustration: you’re not sure whether the physics overlap is testing the right volume because you can only see the visual mesh, not the AABB (axis-aligned bounding box) the engine is using. WireBox and WireSphere let you draw the actual bounds inline with the code that reads them.
Solution. WireBox is composed of 12 internal Line calls (four edges per top and bottom face, four vertical connectors); WireSphere is three orthogonal great-circle outlines. Both are world-space only and do not accept a space parameter. For the query results themselves, pair these with Spatial Structures spatial queries.
/* Wireframe bounding box at a transform with arbitrary rotation. Draw this
* in the same code path that reads bounds.center/size to confirm the
* box is where the physics engine thinks it is. */
DebugDraw.WireBox(
center: bounds.center,
size: bounds.size,
color: Color.green,
rotation: transform.rotation
);
/* Wireframe sphere at a hit point. Higher segment counts produce smoother
* great circles at the cost of three times that many line segments per
* sphere (three circles share the same segment count). Persistent for 1.5
* seconds so you can see the last few hits even after the entity moves. */
DebugDraw.WireSphere(
center: hitInfo.point,
radius: hitInfo.distance > 0f ? 0.3f : 0.1f,
color: Color.red,
segments: 24,
duration: 1.5f
);
Show velocity, forces, and orientation
Problem. There’s always a point in 3D debugging where the question is simply “which way is this thing facing” – and a marker that points at the answer beats any console log. You want to draw velocity vectors on moving entities to see whether steering is working, or draw the transform axes on a pivot to confirm your rotations aren’t gimbal-locked.
Solution. Arrow draws a shaft plus a filled triangular arrowhead, which communicates direction and magnitude at a glance. Cross draws three axis-aligned lines (two in screen space). Axes draws three color-coded lines (red X, green Y, blue Z) matching the Unity editor’s gizmo convention. For Math & Numbers utilities that produce the vectors you might want to visualize here, check that page.
/* Velocity vector at an entity. Headsize is a fraction of the total length
* (default 0.15 = 15 %), so arrowheads stay proportional across distances.
* Draw this every frame from Update to watch the steering behavior live. */
DebugDraw.Arrow(
start: entity.position,
end: entity.position + entity.velocity,
color: Color.cyan
);
/* Custom arrowhead size for tiny inspection arrows, such as surface normals
* or small force contributions. A larger headSize ratio (40%) makes the
* direction readable even on very short vectors. */
DebugDraw.Arrow(
start: pivot,
end: pivot + normal * 0.3f,
color: Color.yellow,
headSize: 0.4f /* 40 % of length, exaggerated for visibility */
);
/* Default-colored cross marker showing a pivot in 3D. Three lines along X, Y, Z.
* Use when you only care about position, not orientation. */
DebugDraw.Cross(pivot, size: 0.2f);
/* Screen-space crosshair at the centre of the Game View. Two lines (X and Y);
* the Z arm is omitted because screen space has no meaningful Z dimension. */
DebugDraw.Cross(
center: new Vector3(Screen.width * 0.5f, Screen.height * 0.5f, 0f),
size: 20f,
color: Color.white,
space: DebugDrawSpace.Screen
);
/* Color-coded world axes at a transform's pivot. X is always red, Y green, Z
* blue, regardless of DefaultColor. Use this rather than Cross when the
* orientation of the axes (not just their position) matters - it confirms
* the transform is not accidentally rotated. */
DebugDraw.Axes(transform.position, transform.rotation, size: 0.5f);
Overlay a spatial grid on procedural generation output
Problem. You’re running a procgen pass – maybe a cellular automaton for cave layouts or a BSP (Binary Space Partition) dungeon – and you need to see the cell grid superimposed on the result. The generated geometry fills the cells, but the cell boundaries themselves are invisible, and you need them to verify room placement, corridor connectivity, and border conditions.
Solution. Grid draws an evenly-spaced lattice of lines, world-space only. With the default identity rotation the grid lies flat on the XZ plane; the size.y component and the cellsY parameter correspond to world Z depth in that orientation. Apply a rotation to project the grid onto a vertical wall or any other plane. Pair this with Grid and the procgen pages for the underlying data.
/* 10x10 grid flat on the ground at the world origin, total 20x20 world units.
* Semi-transparent white reads clearly over most scene content. Call once
* after generation completes, with a duration that outlasts the inspect window. */
DebugDraw.Grid(
center: Vector3.zero,
size: new Vector2(20f, 20f),
cellsX: 10,
cellsY: 10,
color: new Color(1f, 1f, 1f, 0.3f)
);
/* 8x4 grid projected onto a vertical wall (XY plane) by rotating 90 degrees
* around the X axis. Useful for debugging wall-based placement logic such as
* a tile-set that places windows and doors on building facades. */
DebugDraw.Grid(
center: wallCenter,
size: new Vector2(8f, 4f),
cellsX: 8,
cellsY: 4,
color: Color.cyan,
rotation: Quaternion.Euler(90f, 0f, 0f)
);
Build a HUD-style debug overlay
Problem. Sometimes the debug overlay belongs on top of everything else: a crosshair locked to the centre of the screen, a frame-budget bar across the bottom, a “connected to server” indicator in the corner. You want these shapes to ignore scene geometry and depth, always appear in front, and use pixel-perfect coordinates rather than world positions.
Solution. Screen-space shapes always draw on top of everything (ZTest Always), use pixel coordinates with the origin at the bottom-left of the Game View, and appear on every active Game camera (each gets its own mesh built using that camera’s viewport). They do not appear in the Scene View; the pixel-to-world mapping there would change every time you pan the editor.
/* HUD-style health bar background. Position in screen pixels: 60 pixels from
* the left, 30 from the top. The filled rectangle reads as a panel. */
DebugDraw.Rectangle(
center: new Vector3(60f, Screen.height - 30f, 0f),
size: new Vector2(100f, 12f),
filled: true,
color: new Color(0f, 0f, 0f, 0.5f),
space: DebugDrawSpace.Screen
);
/* On-screen line from the player's screen position to a target's screen
* position, useful for visualizing aim-assist or lock-on logic. Convert
* world positions to screen coordinates first. */
Vector3 playerScreen = mainCamera.WorldToScreenPoint(player.position);
Vector3 targetScreen = mainCamera.WorldToScreenPoint(target.position);
DebugDraw.Line(
start: playerScreen,
end: targetScreen,
color: Color.red,
thickness: 2f, /* pixels */
space: DebugDrawSpace.Screen
);
/* Screen-space status dot at a fixed corner. A filled circle reads as an
* indicator light: green for connected, red for disconnected. */
DebugDraw.Circle(
center: new Vector3(20f, 20f, 0f),
radius: 6f,
filled: true,
color: Color.green,
space: DebugDrawSpace.Screen
);
Mark hit events and clear on level reload
Problem. Some debug shapes need to outlive a single frame. You want the bullet’s impact point marked for two seconds so you can see the last few hits in sequence without redrawing every frame, the AI’s last-known-player-position retained until it decides the trail is cold, and every persistent marker cleared cleanly when the level reloads.
Solution. Every shape method accepts a duration parameter that defaults to 0. Zero means “draw this frame only, then discard” – the immediate command list is cleared at the end of every render pass. A positive duration parks the command in a separate persistent list that survives across frames until Time.time exceeds the recorded expiry time. DebugDraw.Clear discards every persistent command immediately, regardless of remaining duration; immediate commands are unaffected because they never live longer than the current frame.
/* One-frame visualization: redraw every frame from Update. Cheap, no buildup.
* Use this for anything that changes position every frame (AI state, velocity). */
private void Update()
{
DebugDraw.Circle(transform.position, radius: 0.5f, color: Color.green);
}
/* Persistent hit marker that fades on its own after 2 seconds. The system
* tracks the expiry; you never have to bookkeep the duration yourself.
* Both shapes share the same duration so they expire together. */
private void OnHit(RaycastHit hit)
{
DebugDraw.WireSphere(hit.point, radius: 0.2f, color: Color.red, duration: 2f);
DebugDraw.Arrow(hit.point, hit.point + hit.normal * 0.5f, color: Color.cyan, duration: 2f);
}
/* Clear every persistent command on a mode switch or level reload. Immediate
* shapes (the per-frame ones drawn in Update) are unaffected because they
* never enter the persistent list. */
public void OnLevelReload()
{
DebugDraw.Clear();
}
API Sketch
The surface is one static class plus one enum. Every shape method follows the same parameter pattern (color, thickness, duration, space where applicable), so once one signature is familiar the others are predictable. There is no facade, no interface, no factory; static calls are the API.
namespace Scylla.Core.Util.DebugDraw
{
public enum DebugDrawSpace
{
World, /* World coordinates, ZTest LEqual, rendered to every Game camera and Scene View. */
Screen /* Pixel coordinates from bottom-left, ZTest Always, Game cameras only. */
}
public static class DebugDraw
{
/* Defaults: applied when the corresponding per-call argument is null or -1.
* All four are reset on every domain reload. */
public static Color DefaultColor { get; set; } /* default: Color.green */
public static float DefaultThickness { get; set; } /* default: 0.02 world units */
public static int DefaultSegments { get; set; } /* default: 32 */
public static int MaxCommands { get; set; } /* default: 4096 (min 256) */
/* Lines */
public static void Line(Vector3 start, Vector3 end,
Color? color = null, float thickness = -1f, float duration = 0f,
DebugDrawSpace space = DebugDrawSpace.World);
public static void Polyline(IReadOnlyList<Vector3> points, bool closed = false,
Color? color = null, float thickness = -1f, float duration = 0f,
DebugDrawSpace space = DebugDrawSpace.World);
/* Basic 2D shapes */
public static void Triangle(Vector3 a, Vector3 b, Vector3 c, bool filled = false,
Color? color = null, float thickness = -1f, float duration = 0f,
DebugDrawSpace space = DebugDrawSpace.World);
public static void Rectangle(Vector3 center, Vector2 size, bool filled = false,
Color? color = null, float thickness = -1f, Quaternion? rotation = null,
float duration = 0f, DebugDrawSpace space = DebugDrawSpace.World);
public static void Square(Vector3 center, float size, bool filled = false,
Color? color = null, float thickness = -1f, Quaternion? rotation = null,
float duration = 0f, DebugDrawSpace space = DebugDrawSpace.World);
public static void Circle(Vector3 center, float radius, bool filled = false,
Color? color = null, float thickness = -1f, int segments = -1,
Vector3? normal = null, float duration = 0f,
DebugDrawSpace space = DebugDrawSpace.World);
public static void Ellipse(Vector3 center, float radiusX, float radiusY,
bool filled = false, Color? color = null, float thickness = -1f,
int segments = -1, Quaternion? rotation = null, float duration = 0f,
DebugDrawSpace space = DebugDrawSpace.World);
/* Regular polygons */
public static void RegularPolygon(Vector3 center, float radius, int sides,
bool filled = false, Color? color = null, float thickness = -1f,
Quaternion? rotation = null, float duration = 0f,
DebugDrawSpace space = DebugDrawSpace.World);
public static void Pentagon(Vector3 center, float radius, bool filled = false, ...);
public static void Hexagon (Vector3 center, float radius, bool filled = false, ...);
public static void Octagon (Vector3 center, float radius, bool filled = false, ...);
/* Wireframe 3D primitives (world space only) */
public static void WireBox(Vector3 center, Vector3 size,
Color? color = null, float thickness = -1f, Quaternion? rotation = null,
float duration = 0f);
public static void WireSphere(Vector3 center, float radius,
Color? color = null, float thickness = -1f, int segments = -1,
float duration = 0f);
/* Directional markers */
public static void Arrow(Vector3 start, Vector3 end,
Color? color = null, float thickness = -1f, float headSize = -1f,
float duration = 0f, DebugDrawSpace space = DebugDrawSpace.World);
public static void Cross(Vector3 center, float size = -1f,
Color? color = null, float thickness = -1f, float duration = 0f,
DebugDrawSpace space = DebugDrawSpace.World);
public static void Axes(Vector3 position, Quaternion? rotation = null,
float size = -1f, float thickness = -1f, float duration = 0f);
/* Grids (world space only) */
public static void Grid(Vector3 center, Vector2 size, int cellsX, int cellsY,
Color? color = null, float thickness = -1f, Quaternion? rotation = null,
float duration = 0f);
/* Clear all persistent commands; immediate commands are unaffected. */
public static void Clear();
}
}
See the API reference for full signatures, remarks, and exception contracts on every member.
Shape catalog
Every shape method takes the common parameters described above (color, thickness, duration, and where applicable space); the table below lists the extra parameters each shape needs and the practical role each fills.
| Shape | Method | Extra parameters | Filled? | Spaces | Typical use |
|---|---|---|---|---|---|
| Line | Line | start, end | n/a | World, Screen | Raycast visualization, force vectors, single-segment debug lines. The base primitive every wireframe shape builds on. |
| Polyline | Polyline | points, closed | no | World, Screen | Path traces, waypoint trails, arbitrary closed outlines. closed: true adds a final segment back to the first point. |
| Triangle | Triangle | a, b, c | yes | World, Screen | Three-vertex polygons; the building block for fans and meshes. Filled triangles render as a solid mesh. |
| Rectangle | Rectangle | size (Vector2), rotation | yes | World, Screen | Bounding boxes for 2D objects, HUD panels in screen space, rotated trigger volumes. |
| Square | Square | size (float) | yes | World, Screen | Convenience wrapper around Rectangle for equal-side cases (tile markers, pixel-art sprites). |
| Circle | Circle | radius, segments, normal | yes | World, Screen | Range indicators, radius queries, footprints. The normal vector picks the plane in 3D (default Vector3.up). |
| Ellipse | Ellipse | radiusX, radiusY, segments, rotation | yes | World, Screen | Oval markers, depth-of-focus rings, anisotropic range indicators. Accepts a quaternion rather than a normal. |
| Regular polygon | RegularPolygon | radius, sides, rotation | yes | World, Screen | N-gon outlines and fills with arbitrary side count (clamped to >= 3). |
| Pentagon | Pentagon | radius, rotation | yes | World, Screen | Convenience wrapper for sides = 5. |
| Hexagon | Hexagon | radius, rotation | yes | World, Screen | Convenience wrapper for sides = 6. Common for hex grids. |
| Octagon | Octagon | radius, rotation | yes | World, Screen | Convenience wrapper for sides = 8. Common for stop-sign-style danger markers. |
| Wire box | WireBox | size (Vector3), rotation | no | World only | 3D bounding boxes, trigger volume outlines, rotated AABBs. Composed of 12 internal lines. |
| Wire sphere | WireSphere | radius, segments | no | World only | 3D bounding spheres, hit zones, awareness radii. Three orthogonal great-circle outlines. |
| Arrow | Arrow | headSize (fraction of length) | n/a | World, Screen | Direction visualization (velocity, forces, normals). Filled triangular head, double-sided so visible from any angle. |
| Cross | Cross | size | n/a | World, Screen | Single-color pivot or origin markers. Three axis-aligned lines in world, two in screen. |
| Axes | Axes | rotation, size | n/a | World only | Color-coded XYZ markers (red/green/blue) matching the Unity editor convention. Ignores DefaultColor. |
| Grid | Grid | size (Vector2), cellsX, cellsY, rotation | no | World only | Tile lattices, ground-plane references, procedural-generation cell visualization. |
Best Practices
- Leave
duration: 0for per-frame visualizations. A call fromUpdateorLateUpdatewithduration: 0is cheap (one allocation-free command per frame, batched into the same mesh as every other one-frame command) and self-cleaning. Reaching fordurationonly because the shape should “stay visible” usually leads to the accumulation bug warned about in the persistence recipe above. - Use
duration > 0for events. Hits, spawns, alerts, score events: anything that happens once and should remain readable for a few seconds. The system tracks the expiry on its own; you don’t need to bookkeep timestamps. - Reach for
Clearonly on a real state change. A level reload, a debug mode switch, a replay scrub.Clearinvalidates every persistent shape at once; calling it casually defeats the purpose ofduration. - Wrap calls in
#if UNITY_EDITORor a#if DEBUGfor release builds. The system itself runs in any build (no preprocessor gating in the source), so the visualizations are still drawn in a release player unless your code excludes them. A[Conditional("UNITY_EDITOR")]wrapper on a project-specific helper method strips the call site entirely. - Pick the right space per use case. World-space shapes occlude correctly and let you navigate around them in Play mode; screen-space shapes always draw on top and are camera-relative. Mixing the two in a single visualization (a world-space wire sphere with a screen-space label dot) is usually clearer than forcing both into one space.
- Override
DefaultColoronce at startup to recolor every default-colored shape. Useful for distinguishing debug overlays from gameplay UI, or for matching a project’s house style (orange instead of green, for instance). - Raise
MaxCommandsif a real workload needs it, but check for accumulation first. Hitting the cap is almost always a sign that persistent commands are accumulating unintentionally. Inspect the calls that pass a non-zerodurationbefore bumping the limit. - Prefer
AxesoverCrosswhen orientation matters. ACrossis a position marker (three lines, one color);Axesis an orientation marker (three lines, RGB-coded). Use the right one for what the visualization is showing. - Draw both halves of a query. A raycast visualization should include the ray and the hit point, an AI perception cone should include the cone and the detected target, a path query should include the input request and the output path. Half a query is often misleading.
- Tessellation defaults are tuned for normal camera distances.
DefaultSegments = 32produces visually smooth circles at typical distances; bumping to64is rarely worth the doubled vertex count except for close-up cinematic captures. Reducing to16is a meaningful win when many circles are drawn per frame. - Lines render as camera-facing quads. Their on-screen width is determined by
thickness, not by camera distance, so they remain readable when the camera pulls back. There is no orthographic shrinkage or perspective foreshortening of the line itself. - For thousands of shapes per frame, switch to a dedicated mesh.
DebugDrawis for debug visualization, not for game rendering. A pathfinding visualization with ten thousand nodes is better served by building a single procedural mesh than by enqueuing ten thousandCirclecommands.
Pitfalls
Related
- Math Utils
- Spatial Structures
- API reference:
Scylla.Core.Util.DebugDraw.DebugDraw - API reference:
Scylla.Core.Util.DebugDraw.DebugDrawSpace