16. Debug Drawing
Every game developer has experienced the frustration of invisible bugs - collision boundaries that don't align with what you see, entities spawning in unexpected locations, or physics calculations that seem to work in mysterious ways.
Debug drawing transforms these invisible systems into visual information you can actually see and understand.
The Fractal Code Cycle Applied to Debug Drawing
Recall our fundamental pattern:
Input → Processing → Output
For our debug drawing system:
- Input: Debug draw commands from various systems (physics, collisions, AI paths)
- Processing: Queue commands during update, batch similar draw operations
- Output: Visual representations overlaid on the game world
What Is Debug Drawing Good For?
Collision Visualisation
- See the actual boundaries entities use for collision detection
- Verify that collision shapes match visual expectations
- Debug why certain collisions aren't working as expected
Physics Debugging
- Visualise force vectors and movement calculations
- Show the cross-sections used in 3D collision detection
- Display the local coordinate spaces during transformations
AI and Pathfinding
- Draw planned movement paths
- Show AI decision boundaries and trigger zones
- Visualise line-of-sight calculations
Performance Profiling
- Highlight expensive operations with colour coding
- Show which systems are active each frame
- Visualise spatial partitioning structures
Debug Drawing in the System Stack
Debug drawing is unique in our system stack - it exists as a cross-cutting concern that can be accessed by any layer:
- Base Layer: Time system might draw frame timing graphs
- World Layer: Physics and collision systems draw boundaries and forces
- Rendering Layer: Camera systems might show frustum boundaries
The debug system provides a common interface for all systems to visualise their internal state.
The Command Pattern Approach
Rather than immediately drawing debug information, we use a command queue:
Debug_Draw_Command :: union {
Debug_Draw_Command_Rect_3D,
Debug_Draw_Command_Circle_3D,
Debug_Draw_Command_Sphere,
Debug_Draw_Command_Box,
}
Why Use Commands Instead of Immediate Drawing?
The Core Problem: Drawing Outside Render Context
Systems like physics and collision detection run during the update phase, but 3D drawing can only happen during the render phase (between rl.BeginMode3D() and rl.EndMode3D()).
Without a command system, you'd have to:
- Pass debug flags through every procedure call
- Store debug data separately and reconstruct it later
- Complicate your logic with rendering concerns
The Solution: Decouple When from Where
- Systems push debug commands whenever they want (during update)
- All actual drawing happens at the correct time (during render)
- Clean separation between game logic and rendering requirements
Command Types in Our System
3D Rectangle Commands
Debug_Draw_Command_Rect_3D :: struct {
color: rl.Color,
rect: Rect,
center: Vec3,
z_offset: f32,
rotation_matrix: Mat4,
}
Perfect for visualising cross-sections in 3D collision detection, showing the 2D planes used in sphere-vs-OBB calculations.
3D Circle Commands
Debug_Draw_Command_Circle_3D :: struct {
color: rl.Color,
center: Vec3,
radius: f32,
axis: Vec3,
angle: f32,
}
Used to show spherical collision boundaries at specific orientations, matching the rotated coordinate spaces.
Sphere and Box Commands
Simple wrappers around Raylib's built-in wireframe drawing functions for basic 3D shapes.
The Push-Flush Pattern
Our debug system follows a simple two-phase approach:
Phase 1: Push Commands (During Update)
debug_draw_push :: proc(cmd: Debug_Draw_Command) {
append(&gs.debug.draw_commands, cmd)
}
Systems call this during their update phase to queue debug visualization.
Phase 2: Flush Commands (During Render)
debug_draw_flush :: proc() {
for cmd in gs.debug.draw_commands {
// Draw each command...
}
clear(&gs.debug.draw_commands)
}
All debug drawing happens at once during the render phase, then the queue is cleared.
Refactoring for Organisation
In this lesson, we move drawing and utility functions into separate files:
debug.odin- Debug command system and flushingdraw.odin- 3D drawing helper functionsutil.odin- General utility functionsphysics.odin- Physics calculations and debug visualisation
This separation makes the codebase more maintainable and allows each file to focus on its specific responsibility.
Integration with Physics System
The collision detection code now pushes debug commands instead of drawing immediately:
debug_draw_push(Debug_Draw_Command_Rect_3D{
color = {64, 255, 64, 80},
rect = cross_section_xy,
center = obb.center,
z_offset = xy_up_offset,
rotation_matrix = rotation_matrix,
})
This approach allows the physics system to focus on calculations while providing rich visual feedback for debugging.
The Big Idea
Debug drawing systems transform abstract mathematical concepts into concrete visual information that human brains can process intuitively.
By using a command-based approach, we maintain clean separation between system logic and visualisation while providing powerful debugging capabilities that scale across the entire codebase.
The investment in a robust debug drawing system pays dividends throughout development, making complex bugs visible and understandable rather than mysterious and frustrating.