2025-12-22
12:42:08, atom feed.
Here is the gist of the commit log for last week's dev log, this post coming in a bit late.
2025-12-15
feat: have aseprite exporter export to build/, not data/
2025-12-15
chore: bump SDL3 to 3.2.28, SDL_image to 3.2.4
2025-12-15
chore: move aseprite exporter into subfolder; add tools/jai-sdl3 submod
2025-12-15
feat: introduce main.jai; use SDL3 bindings to open window, cap fps
2025-12-19
feat: finish rewriting in Jai; add generated SDL3_image bindings
2025-12-19
feat: parse spawn layer from map asset; randomly spawn player/enemy
2025-12-19
feat: implement Map.assign_entity_locations, consolidate asset paths
The big thing from this last week was revisiting Jai's bindings generator and
committing to Jai, not Odin. You can see on the 15th that I was again tempted by
Jai, brought in (my own fork of) the jai-sdl3 bindings generator wrapper for
SDL3, and proceeded to rewrite the game code so far in Jai.
After resting two weekends ago, I realized that I didn't give the bindings
generator a fair shake. I mentioned it had some issues, or maybe that I was just
holding it wrong. I was just holding it wrong. I wrote a simple C library,
compiled it to dynamic and static libraries, and learned how to build bindings
for these. The knowledge gained here scaled up very quickly to allow me to
understand the bindings generator applied to SDL. I tweaked the implementation
(of just the Linux-targeted portion) of the wrapper to not require a system
installation of SDL. This way I can build bindings for my copies of SDL3 that I
build from source. I also added support for SDL3_image. This was much simpler
than the SDL3 support since SDL_image.h is a comparitively small file. The
generator accepts a visitor that can inspect each declaration as it is processed
and decide what to do--generate bindings with no customization, customize the
generation, or skip the declaration. Since SDL_image.h requires SDL.h, the
bindings generator will try to (re)produce bindings for SDL3. To avoid this,
we just have the visitor ignore declarations that don't start with IMG_.
Additionally, we add a header (or footer) to the output to
#import "sdl3";
where sdl3 is the name of the module that provides the SDL3 bindings.
The most time-consuming part of the port was rethinking the memory allocators.
In the end, I kept my own arena implementation and still pass those around, but
I also had to figure out what I was going to do with Jai's implicit context
passed to each procedure, namely the context's allocators. There is a good
thread in the Jai beta discord about this. It reminds us that the main purpose
of the allocators in the context is to help control which memory is used by
modules that you didn't implement yourself. So it makes sense for me to keep
passing my own arenas around. I can also call any Jai procedure that uses
temporary storage at will since I clear Jai's temporary storage at the
beginning of each frame in my game. In fact, writing this just made me realize
I need to clear my temp arena at the beginning of each frame too, forgot to
do that...
I haven't studied the implementation of Jai's temporary allocator, but from a
cursor glance I think it allocates its backing storage on-demand and grows the
storage as it needs to. This means that unlike my arenas, the memory is not
allocated entirely up front, and doing so was the entire purpose of introducing
arenas. That can be solved, though, and I believe Jai itself provides some
allocator implementations (Pool?) that allow for up-front, fixed allocation.
Another good idea from the Discord thread was creating a replacement for the
primary allocator assigned in the context that simply calls assert(false) for
any operation requested of it. This way we can detect when we are allocating
from somewhere that we don't intend to, assuming code we call respects the
allocators set in the context. I have yet to implement this idea but will do so.
About why Jai instead of Odin or some other language, I am just attracted to the syntax and power yet simplicity, at least for the features I care most about. The language feels familiar, not like something I've been writing in for a long time (I really haven't been) but like something that works mostly how I expect it to. Like earlier Python, and like good-enough C, it fits in my head. As you can see from the commit history, I did the entire Jai port in 2, maybe 3 commits. I could have done it in many, adding this functionality and testing it and committing, that functionality and testing it and committing, etc., but I just plowed through it. Amazingly, after getting the program to compile, with the exception of one case where I passed by value instead of by pointer, everything just worked.
With the port to Jai done, I'm getting back to adding features and experimenting again. To close the week out, I added random entity spawning to the game. I chose to place spawn points on my test map by creating another tilemap layer in my Aseprite test map that marks tiles available for spawning. The spawn point placement in my live game was off, and then I realized that each layer in Aseprite is exported with a total size determined by a bounding box placed around the layer's top-left-most tile and bottom-right-most tile. I briefly investigated ways to force the layer to have the same dimensions as all other layers but didn't turn anything up, at least not from the capabilities available through the Lua API. So I resorted to having 3 tiles IDs in the spawn layer. The first is the no-tile ID, which is just 0. This is the default in Aseprite, so I'm more accommodating it than designing it. The second is the ID that indicates a tile is available for spawning. The third are placeholder tiles I use to make sure the spawn layer matches other layers' dimensions. The placeholder tile is black (drawn at 50% opacity) in the image below. The yellow tiles are spawn tiles, the red tiles are collision tiles (on a separate collision layer), and the light green is moveable space. Beneath the red tiles are darker green tiles which represent walls or bushes as visible in previous demo videos.

I use Jai's temporary allocator to build an array of spawn tile locations when I parse the map data, and then I iterate through the entities to randomly assign them a spawn location. I will probably re-hard-code spawn locations soon to create reproducible environments for testing other features, but then I will reenable random spawn in some fashion. The idea plays into the game concept I have in mind.