14 KiB
+++ title = "Game Progress Log - Six Months" date = 2025-04-18 +++
{% important() %} This is more of a personal progress log than a blog post. I wrote it more for my future self than anyone else, though you're obviously welcome to read it if you want. {% end %}
Background
Despite what I sometimes like to pretend, I'm not really a game dev. I've created and deleted my fair share of Godot and Unity projects, I've messed around with libGDX, I've completed three game jams (with varying degrees of success) but I've never actually made anything I'd consider finished. I'm great at starting projects but terrible at completing them.
A little over a year ago, I tried following the learnopengl tutorials, and had a lot of fun doing so. I got as far as a pig in a spotlight before bad design decisions and my inexperience with C++ caused the project to collapse under its own weight.
Maybe in retrospect trying to learn graphics programming and C++ at the same time was not the best idea...
{{ image(src="/assets/6months/pig.png", alt="Minecraft pig model drawn in front of a space-themed skybox", caption="piiiiiiiggggg") }}
Fast-forward to 8 or so months ago, and I finally thought up a game idea I thought was worth persuing. Something I could stay motivated to work on in the longer-term, and hopefully stick with long enough to complete.
I really want to love Godot. It's my go-to choice for game jams, but once a project hits a certain size I can't figure out how to structure it. And I guess I just sort of got bored, or something. I made a little prototype, ran into some weird Godot limitation I no longer remember, and thought "hmm, what if I did this myself."
Stuff
So, for the last six months I've been working on a little game framework thingy in my free time.
SDL3+Kotlin is a bit of a cursed combination, but Kotlin is my current favorite language and I wasn't going to repeat the mistake of using a language I'm not comfortable with.
When I started, LWJGL didn't have bindings for SDL (though it does now) so it wasn't an option. jextract works well enough for my purposes, since I was going to wrap most things in Kotlin anyway.
The first triangle was fairly straightforward.
{{ image(src="/assets/6months/triangle.png", alt="A window displaying a rainbow triangle", caption="hello triangle") }}
And I guess it just spiraled out of control from there. Instead of making a game I've been making a game framework.
I find the SDL GPU API (wow that's a lot of caps) to be a lot easier to reason about than OpenGL was.
Trying to learn OpenGL was difficult because everything was State Soup and some totally unrelated code somewhere else
would interfere with whatever I was trying to do, because I wasn't being careful enough.
At least with SDL I have some idea what's actually going on.
{% note() %} The lack of tutorials for SDL GPU was a little bit frustrating. Being extremely new, there's not much to go off other than some examples and the (actually fairly decent) documentation.
Also, RenderDoc my beloved... {% end %}
So let's see, after 6 months what have I got...
Sprite Batching
Most everything is based around a compute-shader based sprite batcher. It was loosely inspired by this example. I don't have a great way to benchmark performance, but it handles tens of thousands of sprites with no problem on my laptop, so it's more than fast enough.
It does have the annoying limitation that all sprites must come from the same atlas texture, and it won't automatically batch sprites from a bunch of different ones. I should probably change that at some point.
Box2D
I didn't really want to write my own physics. Thanks to Box2D 3.0+ being a C library, it was pretty easy to generate bindings for it.
Unfortunately there's now a conversions.kt
file that looks like
internal fun AABB.b2AABB(arena: Arena): MemorySegment {
val a = b2AABB.allocate(arena)
b2AABB.lowerBound(a, this.min.b2Vec2(arena))
b2AABB.upperBound(a, this.max.b2Vec2(arena))
return a
}
just to convert to and from Box2D's types, but eh, it's fine. I wrapped the callbacks with my own event system and it seems to be working well.
Lighting Experiments
I am very new to graphics programming, all things considered, and trying to implement radiance cascades was probably ill-advised.
I did get some cool screenshots before I realized my game wouldn't even benefit from this kind of lighting in the first place. I probably could have figured it out if I'd spent a little more time, but I was getting bored.
{{ image(src="/assets/6months/idk.png", alt="Very broken scene", caption="yeah I don't even know lmao") }}
{{ image(src="/assets/6months/brokenrc.png", alt="Very broken scene", caption="significantly closer") }}
{% note() %} It wasn't helped by the fact that my test scene was almost entirely text and thin debug lines left over from messing with Box2D. Even a correct RC implementation would probably struggle with that. {% end %}
I might revisit this later. We'll see.
3D Experiments
After so much 2D stuff I was getting a bit bored. My game is 2D, and there's no way I'll be writing a competent 3D engine, but I wanted to mess around a little. If I could draw the pig with OpenGL, maybe I could do something similar with what I'd built.
It went... about as well as could be expected.
{{ image(src="/assets/6months/brokenfox.png", alt="A fox with extremely broken textures", caption="biblically accurate fox or something idk") }}
I did eventually unbreak the fox and get a cubemap skybox mostly working.
It was pretty fun overall, even if not useful. I learned about gamma correction and some neat tricks for fullscreen render passes, so it wasn't a complete waste of time.
Audio
I initially tried plugging an opus decoder more or less directly into an SDL_AudioStream
, but it turns out that was a bad idea.
So for now, I'm using miniaudio.
{{ image(src="/assets/6months/audio.png", alt="debug ui for audio, with volume, pitch, and pan options", caption="little sound ui thing I made with Dear ImGui") }}
It supports basically everything I want to do anyway, like audio streaming, looping, pitch and pan modification, and more.
Getting it to compile was bit of a headache because it's not designed to be built as a shared library, but I don't exactly have any other option to use it from Kotlin. CMake makes me go insane...
{% note() %} I've been using stuff from the FTL soundtrack and ESCISM as test tracks, which are therefore forever burned into my head. There are worse problems to have. {% end %}
User Interface
I know basically nothing about UI, and trying to research how other people handle it proved kind of useless.
"Just draw a bunch of textured rectangles! It's easy!" Yeah, okay, but where and what and why and aaaaaaaaa I don't even know. I was almost tempted to try to use Dear ImGui for game UI, but the lack of theming made that impractical.
I figured drawing 9-slices would be a good place to start, using the sprite batch code I'd written earlier.
{{ image(src="/assets/6months/brokenui.png", alt="Extremely broken 9-slice drawing", caption="Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") }}
I decided to try to decouple the layout and the functionality a little, keeping the logic in Kotlin and the layout in HOCON.
Why HOCON? I don't know. The syntax is nice and Hoplite provides nice error messages when I mess something up.
It looks like this:
{
// some stuff (theme declarations, etc.) omitted for brevity
rootElement = {
type: PaddingContainer
padding: {
left = 30f
right = 30f
top = 20f
bottom = 20f
}
child = {
type: HorizontalListContainer
children = [
{
type: SizeConstraintContainer
max_width = 100f,
max_height = 100f,
child = {
type: NinePatchContainer
id: "test-button-1"
child = {
type: Label
font_size = 48f
text = "1"
}
}
},
{
type: SizeConstraintContainer
max_width = 100f,
max_height = 100f,
child = {
type: NinePatchContainer
id: "test-button-2"
child = {
type: Label
font_size = 48f
text = "2"
}
}
}
]
}
}
}
There are lots of other container types like AlignmentContainer
, ClippingContainer
and I'll probably need to add a few more,
like ScrollContainer
.
The Kotlin side looks like this, where the ButtonBehavior
handles mouse presses and the hovered/pressed/released state.
val gui = Gui(Game.colorTarget, Core.gameDir.resolve("assets/test_ui.conf")).apply {
addBehavior(getElementById("test-button-1"), ButtonBehavior().also {
it.onMousePress += {
println("Button 1!")
}
})
addBehavior(getElementById("test-button-2"), ButtonBehavior().also {
it.onMousePress += {
println("Button 2!")
}
})
show()
}
I don't think this will scale particularly well, but my game is fairly UI-light so it should be Fineeee™ for now at least.
It's significantly better than just spamming Batch.draw()
a bunch.
I don't know how I'm going to handle more complex UI elements, like scroll bars and tab groups. HOCON makes it easy to include
other files with include required(file("/path/to/thing.conf"))
so maybe I could try to make some templates for things like that.
One of the hardest parts was dealing with Batch
's aforementioned everything-must-be-from-the-same-texture limitation.
For now, I'm just generating a separate texture atlas for each UI at runtime, which doesn't feel great.
Text is handled through SDL_ttf's GPU text engine, which does most of the hard work with FreeType and HarfBuzz for me. All I have to do is keep track of different font styles. I tried using FreeType directly, and just about went insane on the spot. Text rendering is not a rabbit hole I want to go down, and this is probably good enough for everything I'll want to do.
Shader Hot-Reloading
There's not much to say about it, other than that it works!
It took too long to get the file watching working properly because Java's WatchService
is a little weird.
In the future I want to be able to hot-reload textures, though that's a little harder because the texture atlases would have to be rebuilt.
Misc
There are some other things that I worked on that aren't really worth their own sections, like:
- An overcomplicated asset management system
- Remappable keybind handling
- Most of a glm-esque math library from scratch
The Future
This is now my biggest project ever in terms of LoC, which is ...concerning? ...fun? I don't know. Let's go with fun.
I'm sure any professional engine devs are... what's the equivalent of "rolling in their graves" for people who are still alive? Whatever. That. They're probably doing that. I don't really know what I'm doing, and I guarantee there are a bunch of horrible design decisions that will come back to bite me later. To be honest though, I don't really care. I'm having fun and learning a lot, which is what I set out to do.
Once I get a little further along, I want to make the core framework open-source. Not because I think anyone should use it, (please no, spare yourself) but because I wouldn't have gotten nearly this far without all the example code on the internet, and if my project can be of any help as a reference for someone else, I'd like that.
I've experimented with adding the Fleks ecs (not to be confused with flecs) and I've been very happy with the results so far. It seems like a good way to structure the game itself, though I'm not quite sure how to cleanly tie Box2D's physics in.
Here's some things I might work on soon:™
- Shader caching? (probably not necessary, but might be fun to do)
- Getting it running on Windows (90% done already, shouldn't be too hard)
- Basic i18n maybe
- Despaghettify the UI code
- Figure out a name for this mess (I'm terrible at naming things)
- Start working on the actual game!
That last one is the main thing. It's about time I stop messing around and start actually making my game. If I want to make the core framework better, I have to try using it, and see what walls I run into. I'm sure there will be plenty.
Anyway, that's it for now, see you in another six months!