Visualise Your Game's Logic with Command Buffers
The Problem
I used to mix my physics and rendering code until I discovered this pattern. Now my code is more concise and debugging is a breeze.
Have you been here? You’re building a game from scratch, or with some libraries like SDL2, raylib, etc. You’ve created your physics objects - Boxes, Lines, Circles…
Now you want to make them collide. You create a Raycast procedure.
Raycast in physics code refers to shooting a ray from a point in a direction with a magnitude (distance) and getting back what it hits (if anything)
You implement some custom physics logic using the Raycast, but you can’t quite tell if it’s working.
You want to see the Raycast, but you’re in the physics update, not the rendering part of the game loop.
Adding render calls in your physics code probably won’t work and may cause weird state depending on your setup.
You could do what I used to do: move the physics code inside the render loop… But, now you have physics tied directly to frame rate - you may not want that.
As well as that, you have a mixing of two totally different systems interspersed.
Here’s a nice solution that’s helped me a ton and can be used in other scenarios - so it’s a great tool to add to your toolbox.
The Solution: Command Buffers
A Command Buffer can be very simple: An array of “Commands” that can be “Played” later.
Let’s build a simple Command Buffer for debug drawing.
// Quick example of what we're building:
debug_draw_line(start, end, 2, YELLOW) // Draw a line during physics
// ... later in render loop, the line appears!
Whenever I design a system, even a small one like this, I start with what I want my usage code to look like:
// Inside our physics code somewhere
results := raycast(start, direction, magnitude)
// Draw the ray as a yellow line
debug_draw_line(start, start + direction * magnitude, 2, YELLOW)
// Draw any hits as a 4px radius orange circle
for result in results {
debug_draw_circle(result.pos, 4, ORANGE)
}
Given this, we can imagine the procedure types look something like so:
debug_draw_line :: proc(start, end: Vec2, thickness: f32, color: Color)
debug_draw_circle :: proc(pos: Vec2, radius: f32, color: Color)
// And for good measure
debug_draw_box :: proc(pos, size: Vec2, color: Color)
These procedures all need to add Commands to a Buffer, which can be more or less complicated.
For a simple setup like this, we can use a simple array:
debug_draw_commands: [dynamic]Debug_Draw_Cmd
Then, in each procedure, we append a Debug_Draw_Cmd
of the right type to the array:
debug_draw_line :: proc(start, end: Vec2, thickness: f32, color: Color) {
append(&debug_draw_commands, Debug_Draw_Line{
start = start, end = end, thickness = thickness, color = color
})
}
A Debug_Draw_Cmd
is a union
so that we can put all Commands into one Buffer and iterate over them.
Debug_Draw_Command :: union {
Debug_Draw_Box,
Debug_Draw_Line,
Debug_Draw_Circle,
}
Debug_Draw_Box :: struct {
pos: Vec2,
size: Vec2,
color: Color,
}
Debug_Draw_Line :: struct {
start: Vec2,
end: Vec2,
thickness: f32,
color: Color,
}
Debug_Draw_Circle :: struct {
pos: Vec2,
radius: f32,
color: Color,
}
This example is in Odin, but it’d work the same way in C or any other language with unions (though you may need to add a type
field + enum).
We create an array of Debug_Draw_Cmd
which can be either Box
, Line
, or Circle
.
You could also use a Queue
, though I find that there’s no need since we always process the entire array at draw time.
Next, we can process and draw everything during the render loop:
// Inside the render loop
// You may have something like:
draw_begin()
// ... all your drawing code
for cmd in debug_draw_commands {
switch v in cmd {
case Debug_Draw_Line:
draw_line(v.start, v.end, v.thickness, v.color)
case Debug_Draw_Circle:
draw_circle(v.pos, v.radius, v.color)
case Debug_Draw_Box:
draw_box(v.pos, v.size, v.color)
}
}
draw_end()
This is assuming your drawing code has draw_line
, draw_circle
, and draw_box
available.
The last thing we need to do is clear the Buffer after we draw everything.
clear(debug_draw_commands)
You could put this in the draw_end
procedure or anywhere after the loop we just made.
Taking it Further
This is a simple implementation, but they can get as complicated as you’d like.
You can use a Queue instead of an Array and process bits at a time based on some conditions.
You could create a thread-safe Queue and have Command Buffers that are added to and processed on multiple threads.
If you dive into DX12 or Vulkan, understanding Command Buffers even at this basic level should help.
Now that you know how to use Command Buffers, you need to understand how to manage the memory for it.
Check out this article about Simple Mental Models for Memory Management that will help you organise your game’s memory efficiently, including command buffers like the one we just built.
Want to see how other developers are using command buffers?
Join us in the Program Video Games Community where we’re building games from first principles.
No complexity for complexity’s sake. Just practical code and real solutions.
Our community shares: - Working examples - Clear answers - Devlogs and source code (including Magepunk, the game I’m building live on stream)
Thanks for reading!
Cheers, —Dylan