Vacation time! I’ll be off during all of July. But before that, let’s talk a bit about what I’ve been tinkering with in June. Let’s talk a bit about game engines.
Make your game!
I tend to say that if you want to make a game from scratch (not using a any existing game engine), then you need to focus on making the game.
Do not try to first make a general purpose engine and then, when you are done with that engine, make your game on top.
Only solve the problem you actually have. Don’t dream about “what if I need X in the future”. Only write the code that gets you closer to shipping the game.
If you are on a bigger team, then you may need to think a bit more about “what if we need X in the future”. The smaller the team, the more you are usually able to focus on The Now. In small teams its easier to do crazy restructuring of the code. The more people, the scarier it is to break things.
But I want to make an engine!
That’s fine! Make an engine. You just need to be honest with yourself: Are you making a game, or making an engine?
When I say engine in this newsletter, I mean “general purpose game engine”, something like Unity or Godot. If you make 3 games and some kind of engine fall out as a bi-product over time, then that’s not really what I refer to in this case.
One thing you should keep in mind, is that that making an engine is a huge task. Let’s say that you want to make a general purpose game engine that other people can use. What is biggest amount of work? Fancy rendering code? An entity system? No: Tools. If you want to make an engine then you must be prepared to spend a lot of your time making tools. You must care about the user experience of those tools.
This may sound obvious, but here is a funny thing I’ve noticed with people who make game engines as a hobby: Many start with some 3D rendering code, perhaps they make some Physically Based Rendering pipeline, etc etc. And then when they’ve spent months on that, then they suddenly realize they need to make some tools. Many give it a go, don’t find it fun and then the project dies.
So here’s an idea: If you want to make a game engine, then you should probably start with these things:
UI rendering
Some way to store / load data
Then make some tiny toy tool using the UI rendering and go on from there.
Eventually, you can add some basic 2D / 3D rendering and experiment with some ideas of how to edit levels. Over time, you can add more rendering and perhaps an entity concept etc.
This way you’ll have a more holistic approach to game engine development. You’ve already started with the most important part: The tools.
So why are you writing about this?
I’m an experienced tools and engine programmer. So I thought it would be interesting to try the approach above. More specifically, what I would like is:
A general purpose game engine written in Odin
Odin is also used for gameplay programming
I focus on getting the tools programming up to speed first. If I can’t get anywhere with the tools, then I might just as well not continue.
Plugin-based: Make the engine modular. Have hot reloadable plugins.
Low-level: Does not use any graphics API wrapper. Instead I want to use the “most native” graphics API for each platform. On Windows that’d be Direct3D. On Mac Metal. On Linux…. Vulkan I guess.
I haven’t gotten far yet. But I’ve spent some time in June tinkering with these things.
I’ve written some rendering code in Direct3D 12 that can draw some rectangles in a single draw call. This will be the foundation of the Immediate Mode UI.
Then I’ve gotten started on the plugin system. It’s not 100% working yet, but I have my Direct3D 12 code as a plugin that is loaded dynamically.
Thinking about code organization
When making something modular and plugin-based, where the plugins are possible to dynamically load and hot reload, then each plugin probably becomes a separate package.
Using many small packages in Odin is usually a problem. You’ll compartmentalize incorrectly and you can’t have circular dependencies.
However, I envision my plugins to be quite big. At least the plugins of the core engine and the tools. The whole D3D12 renderer is one plugin etc. The IMUI rendering code is probably another plugin. A level editor may be another plugin.
So what I do is the following: In the D3D12 plugin I introduce an `@api` attribute. I mark procedures with it like so:
@api
buffer_create :: proc(blabla) -> Buffer_Handle {
// do things
}
When I compile the plugin I use the parser in `core:odin` to find those procs marked with `@api`. I add their signature to an API struct. That’s the API the user of the plugin has to use.
The API file for the D3D12 plugin currently looks like so:
package renderer_d3d12
import hm "kzg:base/handle_map"
import "kzg:base"
Shader_Handle :: distinct hm.Handle
Buffer_Handle :: distinct hm.Handle
Swapchain_Handle :: distinct hm.Handle
Pipeline_Handle :: distinct hm.Handle
State :: struct {}
Command_List :: struct {}
API :: struct {
create: proc(allocator := context.allocator, loc := #caller_location) -> ^State,
destroy: proc(s: ^State),
create_swapchain: proc(s: ^State, hwnd: u64, width: int, height: int) -> Swapchain_Handle,
destroy_swapchain: proc(s: ^State, sh: Swapchain_Handle),
create_pipeline: proc(s: ^State, shader_handle: Shader_Handle) -> Pipeline_Handle,
destroy_pipeline: proc(rs: ^State, ph: Pipeline_Handle),
flush: proc(s: ^State, sh: Swapchain_Handle),
begin_frame: proc(s: ^State, sh: Swapchain_Handle),
draw: proc(s: ^State, cmd: ^Command_List, index_buffer: Buffer_Handle, n: int),
create_command_list: proc(s: ^State, ph: Pipeline_Handle, sh: Swapchain_Handle) -> ^Command_List,
destroy_command_list: proc(rs: ^State, cmd: ^Command_List),
set_buffer: proc(rs: ^State, ph: Pipeline_Handle, name: string, h: Buffer_Handle),
begin_render_pass: proc(s: ^State, cmd: ^Command_List),
execute_command_list: proc(s: ^State, cmd: ^Command_List),
present: proc(s: ^State, sh: Swapchain_Handle),
shader_create: proc(s: ^State, shader_source: string) -> Shader_Handle,
shader_destroy: proc(s: ^State, h: Shader_Handle),
buffer_create: proc(s: ^State, num_elements: int, element_size: int) -> Buffer_Handle,
buffer_destroy: proc(s: ^State, h: Buffer_Handle),
buffer_map: proc(s: ^State, h: Buffer_Handle) -> rawptr,
buffer_unmap: proc(s: ^State, h: Buffer_Handle),
swapchain_size: proc(s: ^State, sh: Swapchain_Handle) -> base.Vec2i,
}
The procs inside that API struct are matched up with the actual procs in the DLL when the plugin is loaded.
So the plugin is, from the viewpoint of the running game engine just two things: This API definition file and the DLL that contains the code. In order to avoid circular dependencies the API file should only import things from `core`, `base` and my own `kzg:base` package.
However, inside the code in the DLL, behind the “API wall”, it can import any other plugin. That’s fine, it’s not going to create a circular dependency from the viewpoint of the Odin compiler. So this way we get simple APIs and arbitrarily complicated code behind those APIs.
The code
You can look at the code here: https://github.com/karl-zylinski/kzg — Keep in mind that currently it just renders some rectangles and does some plugin stuff. But it’s going to be fun to the the plugin stuff a bit more going and then go back and try to get the editor things off the ground.
Well, I’m off to vacation for a month!
Thanks for reading! Have a nice July!
/Karl
Have a fun and relaxing vacation, Karl!