Blog
Atom feed is here.
This last week I focused on implementing basic animations.
This week I imlpemented an entity-component-system, a basic player attack, entity deletion, and text and textbox rendering.
This week I implemented A* and added support for multiplate enemies to the game.
Here is the commit log from last week's work.
2025-12-23
feat: introduce default allocator replacement for context
2025-12-26
feat: introduce "standard library" (sl) with ring buffer and test
2025-12-26
feat: add heap data structure and min-heap procedures to sl
2025-12-27
feat: separate min_heap out of heap impl; add max_heap; add tests
The main focus this week was pathfinding, but I was also up to some other peripheral things as well.
Firstly, we have the introduction of a default allocator replacement for the Jai
context. Jai passes around a special variable implicitly to each scope called
the context. I think I discussed this earlier, but in short, the context has
various useful properties on it such as the currently preferred logging function
and the preferred default and temporary allocators. In my case, I want to avoid
allocating using the default allocator. I also want to avoid allocating using
the temp allocator as much as possible, but I'm less concerned about this
because I clear its storage at the beginning of each frame. To catch usage of
the default allocator, I implemented my own allocator which just
assert(false, "[...]");s on any operation requested of it. I caught a few
issues with this, mostly String.join calls, that I fixed using
String.join(...,, allocator=temp);
Note the ,, which is an operator in Jai which allows you to modify context
properties at callsites for arbitrary functions. I would like to use my own
temporary allocators provided through the most basic of arenas, but for Jai
"standard library" things like this, it's easier to use Jai's temporary
allocator. I could always implement the allocator interface for my arena and
thus use my own backing memory pool, though. Probably a project for a later
time.
Toward pathfinding, I have a simple grid map with uniform cost to move between squares. I recalled the standard A* algorithm for pathfinding in such situations. It's been over 10 years since I've dealt with A*, though, so I needed a refresher. I came across Red Blob Games. This blog and the author's few others linked from it are a wealth of information on algorithms for games, particularly top-down, 2D ones such as mine. I highly recommend checking the site out if you're interested in such.
I thought I was going to use a queue, and a ring buffer would work well here,
so I implemented one. I followed a great tip from
Juho Snellman
on using only positive indicies (no forced wrap-around via %) and masking the
indicies only when you need to look into the backing storage. Clever and neat.
Now, it turns out I actually wanted a priority queue, so for that I decided to use a min heap. To my knowledge, Jai does not provide a min heap, so I implemented one of those as well. To brush up on that, I referenced the classic Introduction to Data Structures and Alrogithms book. I had learned from this in college as well. After re-reading a very small portion of this recently, I admit I cannot recommend it. It does many things well, but I find it overreaches with its advice in places and also (probably accidentally) misrepresents some concepts by overfitting descriptions on very narrow use cases in specific languages, for example by calling the heap a region of garbage-collected storage. Anyway, it did the job.
I ultimately implemented a heap (heap.jai) and then implemented the min-heap and
max-heap interfaces in respective files that both import heap.jai. I
reintroduced my "sl" concept from when I was playing with data structure
generation via the C preprocessor two or three weeks ago. I have a special file
in the sl module called test.jai which provides a main function and imports
all of the files in sl and tests their features. I have a build.jai file in sl
that builds a binary from test.jai. Building and running the tests is a manual
process, but I don't anticipate changing sl so frequently that I'll hurt for
any automation, except that one time when I will probably change it, introduce
a bug, and forget to run the tests haha.
I then got to implementing A*. The work is on a branch currently and will be the subject of this week's update.
I also started streaming livecoding (and some gaming) on Twitch, as noted in a previous post. The idea here is to maybe build even the smallest of audiences or at least presence in parallel with building the game instead of serializing the process. Presumably an audience takes just as long if not longer to build than the game itself, but what do I know--I'm totally new to this aspect!
I decided to give streaming a go. You can check out the content here. I will try streaming both development and games here and there. The motivation is building an audience more organically in the event I do end up trying to market a game.
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.
Here is the commit log from this last week of work.
2025-12-08
feat: add array and slice-generation macro and test for it
What happened?! The time has come to switch off of "C++". Even my beloved dialect of C++-that-is-actually-mostly-C leaves me with lots of work to do to create a language I'm happy to use, before I can get back to the problem at hand of developing a game.
As I refreshed my memory on search algorithms and studied pathfinding algorithms I was not familiar with, I realized I'd benefit from some more sophisticated data structures. As you can see from the commit above, I started to implement such data structures. I had three options. The first was to just use containers from C++'s standard library. I really didn't want to go this route for reasons I won't elaborate on here; suffice to say that I don't plan on depending more on C++ features in general. The second was implmenting my own code generator, like spec'ing out code with variables that a (hand-written?) templating tool would replace with values like type information, running this as a pre-build step, and then including the generated code in compilation. Again, more work, essentially somewhat re-implementing C++'s template features and also portions of what the third option was: preprocessor macros.
I opted for the third approach since, in theory, it is write-once-test-a-bunch-use-without-too-much-care-afterward. The natural place for this work was in the small standard library I was growing. I did prove out that I could generate a satisfying array type and a corresponding slice type. But honestly the thought of repeating that for a queue, a priority queue, a dynamic array, a hash map, etc. dissuaded me from continuing. Not only would I need to take a lot of care to avoid bugs and write a bunch of tests, I'd also need to get into the weeds about which implementation of which data structure was most optimal (or at least good enough), etc. And I just can't be bothered right now.
Coincidentally, I came across a podcast interviewing Jon Blow (this one? I don't quite remember unfortuantely), asking about why he was creating Jai and what hope he thought he had of creating a relevant C++ competitor or even replacement. I have been spending a lot of time thinking about whether to go all-in on Jai, weighing the tradeoffs of an actually-sane language but one developed and maintained by only a very small number of people (albeit with a big flagship project proving that the language is ready for production projects) and an industry-standard language backed by tools with massive user bases and a much larger number of contributors. Jon's answer was very convincing. In short, he said that we as a species have not used the same programming language forever. Even Big Industry switches languages over time. So if we accept that we are not going to use some particular language forever, then we don't have worry about asking ourselves whether we can compete with things like C++. Instead, we have to ask ourselves, when we do have a new language that we gravitate toward, which one will it be? Jai is Jon's language in this regard.
The last time I used Jai seriously was one year ago, last winter. Back then I
wrote essentially what I have now in "C++" and SDL (load tilemaps and sprites,
paint a level and a character, move the character around in a 2D world) but in
Jai and its built-in windowing abstraction called "Simp." So I spent a couple
days brushing up on the language and learning about what had changed since when
I last used it. After refreshing, I gave a Big Think over whether or not to use
Simp again for my project. I decided against it and chose to stay with SDL. So
I started exploring Jai's bindings generator tool that is often used to generate
Jai bindings to useful C and C++ utilities people want in their Jai projects.
At the time of this writing, there is an
open-source repo that uses the
bindings generator to create SDL3 bindings for Windows, macOS, and Linux. I
forked it and modified it a little to support my own workflow. Instead of the
current open-source behavior which wants a system-installed SDL3 library at
least for the Linux use case, I compile SDL3 from scratch for my project and so
want to point the bindings generator at that library at generation time. I got
it working in the end but suspect I was holding the tool wrong because there
was some non-obvious behavior such as transforming things like
../../build/libSDL3.so to __/__/build/libSDL3.so which broke library
loading at runtime untils I made some manual tweaks to the generator and also
stuffed a library in my source code instead of just in the build/ folder. So
despite it working, I wondered if there was an easier way overall.
Again coincidentally I came across some videos on YouTube about using Odin for game development. I've known of Odin for years but have never gotten into it. Odin does provide SDL3 bindings out of the box. Although not shown in the commit log at the beginning of this post, I've started down the Odin path, first to see if I enjoy the language enough to keep using it and second to see if it's any easier (by my taste) to achieve what I'm trying to do with Odin than it has been with Jai.
Currently I am opening a window via SDL3, painting the window the same gray background color I was using in my previous C-like implementation, and reacting to window-close events. The magic to work with my compiled SDL libraries is simply:
-
build SDL3 and emit libraries in
./build -
build my Odin project as follows
odin build src/ \ -extra-linker-flags:"-L./build" \ -out:"./build/game" \ -show-timings
Now, I am working on porting all my other code from "C++" to Odin. It's annoying, but it's best to pay this language-and-tooling cost up front in the project and have a consistent set of tools I enjoy using for the life of the project, which will likely be years.
So far, I think Jai is simpler than Odin, both in syntax and in its concepts. There are very many similarities in the language behavior and syntax. I cannot tell who inspired whom, presumably Jon-->Bill? Regardless, Odin hasn't been hard to pick up. The thing I spend the most time on is figuring out which types I should replace C-style buffers and arrays with in Odin. I'm only a couple days in, so maybe this is obvious and simple, but I'm just not there yet, have been playing more with SDL3 and doing a lot of reading about Odin to get a sense for its capabilities and staying power.
I did find one thing amusing, that there will be an upcoming change, part of which is explicitly passing an allocator into some functions (ref). I am already doing this everywhere in C, so no big deal. Jai passes a context to every Jai call that provides things such as an allocator so that callers can easily swap out the allocator used by third-party code, unless the library author explicitly chooses to ignore the requested allocator. I think Odin has something similar.
Next week I hope to have at least my previous game implementation but this time in Odin, if I don't decide to switch off of it.
Now that I'm doing my own thing, I've got more time on my hands to explore interesting posts, videos, books, etc. I've also resolved to put more of my thoughts here in this blog instead of on a variety of disparate networks.
A few days ago I came across a fellow by the name of Sylvan Franklin. I identify a lot with what his recent journey appears to be, but that story is for another time.
This post is prompted by The Addiction that hides in plain slight. In this post, I'll casually mix the target I am writing to.
One point Sylvan brings up is that many of us may have an addiction to information. I certainly do. I noticed this a few months ago, when I realized I would hesitate to start house chores or cooking until I found something interesting to listen to on my Bluetooth headphones.
One day, on a podcast I don't remember, I heard someone ask the rhetorical question, "how much information is enough?" The context implied the full question was, "how much information is enough to equip you to do what you think you need to do?"
I opened that question up more and realized the next implication, that I didn't actually have an agenda with any of that information. I didn't want to do anything with what I was "learning." In fact I told myself I was learning, but in reality I just wanted to be entertained. Learning is evidenced by a change in behavior, but I wasn't changing much. The gotcha was that every now and then I would consume something that resonated deeply with me--made me feel understood--or did change the way I thought and lived. What I became addicted to was the next revelation. But there is no promise of when or where this will come, much like there is no promise of when you may see something actually transformative while doomscrolling, be it a social media feed or a collection of proverbs.
In the end, I was just saturating my brain, hardly giving anything enough time and thought to personalize it.
You can tell when you've personalized something when you can communicate it to others without referencing anyone or anything else, that is, by appealing to personal experience and not authority, without saying, "the Senior Architect said...," or, "The Meditations say..." Now, let us not plaigarize, and if you want to cite the source, that's fine, but you haven't personalized something if you appeal to the source. One of the things I appreciate in Sylvan's recent videos I've seen is his self-awareness about his journey being personal.
"what I have to do is, do the hard work of coding everyday, or working on the math everyday, or reading philosophers everyday and thinking about them"
You already know the path forward! But perhaps you are not convinced yet. I'll elaborate below, but in the end, only you can convince yourself.
"What got you out of information consumption? I'm looking for the One Trick (tm)."
If you only look externally, you will be searching unsatisfied forever. As you say, it's like climbing a mountain. Looking externally has its benefits, but ultimately you will see others' techniques that worked for them. The catch is that the mountain you climb is your mountain. You can see their mountains from your mountain, or sometimes at least imagine their mountains. Your terrain might look like their terrain at times, but ultimately it is not their terrain.
The trick is to know who you are and where you are going. Knowing this in itself is a journey, and unless you concern yourself with cosmology, is perhaps the journey.
Who you are means understanding yourself--your nature, your past, your desires, etc. Consuming information externally is useful for bootstrapping your search. For example, Zen--as you've also discovered--and the "noting" meditation technique worked well for me, and understanding this and that about behavioral psychology was also useful. The trigger to look inward was looking outward for answers to questions about myself until I realized I am the only one who knows me. That, and after having asked {God|the Universe|whatever}, "what is my purpose," for so long, only to realize I am actually being asked that question and the asker is just waiting for my response. Coming to know who you are is often a long journey, perhaps never-ending, depending on your definition of finished. However, answers to who I am have brought me peace from mind, the mind being all I was trying to appease by some new toy theory to juggle, programming language to learn, or other distraction to concentrate on.
Where you are going, as a question, is, "what is your current goal?" If you have this agitation that your goals are distractions or are perhaps meaningless, also ask yourself, "why," regarding what your current goal is. Iterate on that until you've found something meaningful and satisfying to you and then go with it. Where you are going will naturally change over time, sometimes multiple times a year, sometimes once every 5 or 10 years, unless you are the rare type who pursues a lifelong mission that outlives you. But even then, you will likely have milestones along the way.
When we know who we are and where we are going, we can return to the deluge of information, but this time with a natural filter. We're here with a direction and a purpose. We take what is useful, we're thankful for all else but are not distracted by it or {time|compute}-enslaved to it. It's worth noting, it's useful to allow ourselves time to play and wander periodically (the search part of search-exploit, if you will). It's not about always filtering but rather about having the filter and the awareness to apply it as necessary.
The fact that you are already looking means that it is only a matter of time, so travel with a peaceful mind, fellow sailor!
Given what I understand about you so far, if you have not already read The Beginning of Infinity (David Deutsch), please do so if you're looking for any bootstrapping.
Aside:
"You program long enough and the patterns emerge, and you start to see things."
I hadn't heard of the Gang of 4 book until years into my software engineering career. When I read it, everything in the book I actually cared about was already familiar. I had developed it myself via the Feynman Algorithm. And I don't say this boastfully--I'm no genius. I just had problems and thought hard about how to solve them well until I solved them. One amusing thing I noticed, was that because I had derived the designs myself in-context, it stood out to me immediately when a peer appealed to the Gang of 4 in a, "well, I don't know, but these guys say..." way. Which is not The Way (tm).
Godspeed.
Here's the curated commit log for last week.
2025-12-03
fix: allow player to change facing direction even if they can't move
2025-12-03
refactor: wrap platform-side details (renderer, screen dims) in struct
2025-12-03
feat: use renderer logical presentation to support arbitrary upscaling
This also removes explicit transformation from world tiles to world
pixels as world pixels were only ever used as an intermediate frame when
transforming to screen pixels.
2025-12-04
docs: add concept groundwork for first game; separate next game content
2025-12-04
feat: add first enemy, random movement; add entity-entity collision
2025-12-05
fix: round lowest visible tile idx toward 0 to fix gap in map_render
I split my time three ways this week, one by necessity and another mostly by chance. As this post is about solo game dev, I will share details peripheral to development from time to time as they are part of the journey. In this case, the by-necessity thing was applying for health insurance as it is mandatory to have if living in Japan. I'll split that into another short post, though.
The second thing was game design. While I will go into detail about the mechanics and perhaps level design, I will probably never go into much detail about the story itself, as I'd like to leave that up to players to experience and interpret. I guess I can elaborate a bit on my trajectory until recently, though. I plan on this being my first game I'll publish and sell. I'd love for it to be "successful," but I don't bank on that at all, pun intended. What I want to do is get experience going end-to-end, from an empty file in a text editor to a game in the market. That will be quite a journey, but that will be the first iteration. From there I'll learn whatever I can and repeat. Toward that, I don't want to develop slop, will try to come up with something creative and worthwhile for me and others.
At first I had an idea to clone an existing, small game I enjoyed and add some extensions. This gave me well-scoped goals and removed a big task, which was novel game design. This was all toward getting some iteration out in public quickly. Lots of the big indie game hits took 5+ years to develop. I'd like to get something out in 1 or 2 years, again, without the aspiration that my title would mean anything to anyone but me. To date, I've been developing my own game for something to the tune of 7 weeks or so, and already, my own game design ideas are popping up in my mind. I did rack my brain for the first two weeks for some interesting, novel idea I could build a game around, but nothing really surfaced. I recalled a talk I watched from Jon Blow, I think it was his keynote at GDC 2014, where he was discussing where to find inspiration for interesting games. I've also learned from my beloved Kojima Hideo-san and also Sakurai Masahiro-san. The thing I always walked away with, though, is that I'm not going to sit and try to come up with an idea that makes for an interesting story or game. And indeed, this has proven true in my life. Perhaps ironically, it is by not trying that I have had my most promising ideas for games. This time around has been no different. As I develop and think about which feature to try and implement next, ideas seem to naturally bubble up into my mind. "Ah, what if instead of the game working in this way, it works in that way?" And "that" way turns out to have some parallel with a transformative experience I had in life, and suddenly a game feature becomes a metaphor for a realization I had or a way of thinking that changed my life. And then a game feature and a story element naturally weave themselves together. I'm being vague here still so as to not spoil anything about the story I'm thinking of writing, but I think I've described the gist.
Paraphrasing the Tao Te Ching, by not doing, it is done.
I also read and listened to talks about creating game design documents. Creating one before doing any other work probably works well for the niche of people that are borderline clairvoyants, but they don't work like that for me. In fact, they smell like the stereotypical project manager who tries to outline a product on paper and get it right the first time. Of course, the game design document is a living document in either case, so it does evolve over time, typically to be come less prescriptive and more descriptive. For me, the way it works is that I develop something, seem to naturally get interesting ideas over time related to my life experiences, and then I just jot the ideas down in a Markdown file checked in at the root of my game's repo. It's less a design document and more a journal of the concepts at play in the game and ideas I've had that I want to explore further. Once I have such ideas, these are seeds from which I can sit and intentionally think more deeply about and further develop. Going from 0 to 1 idea-wise, I have no process for. I often just need to let my mind wander (/wonder). Once I have 1, I can play with that intentionally and grow it in this and that way.
One guiding principle of my game design is that I want to respect the player's time. What I mean by this is try to share an interesting mechanic, idea, or thought with the player in 3-5 hours. The game would need to be priced accordingly, but otherwise, I don't like the thought of me creating something that, if a hit, consumes 80+ hours of tens of thousands if not millions of people's time just distracting them from something else in life.
Finally, the third thing was typical feature development. The most interesting topics this week are rendering logical presentation and adding the first test enemy.
Regarding logical presentation, after creating a 16x16 px sprite and then
rendering it in my game, I thought the sprite was a bit too small on my monitor.
I had two choices. The first was redraw the sprite, and eventually every asset,
at a larger resolution. The second was to upscale the sprite. The first would
cause the art to take more time, and the second seemed more practical, so I
tried to do the second. When I did that, I got some strange effects. There
seemed to be some sort of visual tearing between tiles. What was in fact
happening was that SDL was applying blending upon upscaling. This is controlled
per-texture using
SDL_SetTextureScaleMode.
SDL3 conveniently provides SDL_SCALEMODE_PIXELART as a scale mode, perfect
for my use cases.
Here is what rendering looked like before applying.

Here is what it looked like after.
![]()
You can see the "tearing" issue is fixed. The pixelart version is much sharper, although there is a nice CRT-like aesthetic to the previous scale mode, which I think was just nearest-neighbor. I'm going to keep the pixelart scale mode for a variety of reasons. I won't get into that more this time, though.
You may also have noticed that the image of the game seems zoomed in compared to images or videos in previous posts. At first, I was scaling the rendering by adding an explicit scale factor (2x) and then propagating that scale factor to all code that dealt with transforming to or working in screen pixel space. One question that naturally comes up is, how do I render to a screen space that has an aspect ratio different than my world map? If I create a scale factor based on the latest window dimensions, I will need a horizontal and vertical scale factor. Or, I could create a uniform scale factor, but what do I do when my scaling doesn't fill the entire screen in some dimension? I'd generally accommodate this manually. One would typically fill the gap with letterboxes. (Grokipedia isn't so great at the time of this writing in that it doesn't show images, and an image would explain this concept immediately. Anyway, here's to hoping the article is better in the future.)
I found that SDL has a function to handle this for me, called
SDL_SetRenderLogicalPresentation.
So I employed that function. The gist is that you have some actual screen
dimensions that the rendered image is displayed in, but you can configure a
logical render space that you will target your code/math to. SDL will handle
scaling your logical render space to the physical render space at presentation
time. So, you don't have to worry about handling letterboxing or uniform
scaling anymore. To boot, you also don't have to propagate manual scaling
factors throughout your code. Instead, I just propagate the logical render
dimensions. When I want to scale up, I configure the logical render dimensions
to be smaller than the physical render dimensions.
The second notable feature was adding my first test enemy. I hacked the enemy
directly into the game state object for now, but I realized I'll have to do
some thinking on how to efficiently manage multiple enemies (generalized to
entities) for some level or game state. Problems include: reusing an entity
asset across multiple entities instead of each entity owning its own
(potentially duplicate) asset, creating a statically-sized collection for
entities and marking some as existent/non-existent or having the collection
by dynamically-sized (so non-existence is handled via delete or remove
or something like that on the collection), and how to store information about
how many entities and their kinds to spawn and where for each level.
Then I also need to implement pathfinding for enemies.
Currently I have a simple ghost-like sprite spawned at a hard-coded location that maybe moves in some random direction when the player moves. As the game is tile-based, collision detection is simply
- check for collision on the map
- check for collision with other entity
Here is the curated commit log and post for this week's game development.
2025-11-24
chore: move standard-library-like code to src/sl ("standard library")
2025-11-24
feat: path_concat now maybe adds separators; add test and README
2025-11-24
feat: add string view path_concat overload; extend test
2025-11-25
feat: add arena allocator to standard library
2025-11-25
feat: map loading and path_concat now use an arena
2025-11-25
feat: add arena_realloc and test in preparation for using stb_image
2025-11-25
feat: introduce stb_image as library; use our own allocators; load PNGs
2025-11-26
feat: replace STBI with SDL_image; load map tilesets as texture atlases
2025-11-26
feat: render actual game map instead of placeholder game map
2025-11-27
feat: move game logic out of main; introduce persist, temp, map arenas
2025-11-28
feat: generalize aseprite_to_map tool to aseprite_asset_exporter
2025-11-28
refactor: extract map into general tileset asset
2025-11-28
feat: load player sprite, add player-facing direction
Last week's commit log query started from the beginning of time in my repo, so
I've added a new filter to the git log query, --since. Again, mostly just
pasting this here for my reference.
git log --pretty=format:"%ad%n%B" --date=short --since "2025-11-24" --reverse
As the first commit suggests, I've extracted some features from my game into a
separate library I'm calling sl, just meaning "standard library." It's
essentially a variety of game-independent utilities that I would typically find
in a languages' standard library but flavored to my taste.
path_concat, which I posted about in last week's post, will now add path
separators when required. This change is ultimately what led me to introduce
arena allocators which I did shortly after. Without arena allocators, the caller
has to set up enough buffer space for the string concatenation, and the
requirements to set up the buffer correctly grew as the function became more
featureful. Some months ago, in my last group of posts about the game I am
making, I discussed arenas, so I won't be going into them here.
At the beginning of the week, I was rendering a placeholder map and character. I already had Aseprite map loading working in my game last week, but I was not rendering the map. This week I tossed out the placeholder map and am now rendering "real" maps. This made testing a lot more fun. It also reminded me that I need to get a lot better at pixel art :) Anyway, the idea was just to get something rendered to test the end-to-end art-to-play path.
I previously implemented collision by just checking the tile ID of the tile the player wanted to move onto. Now, as I probably posted about months ago as well, I export at least 2 layers from a single map in Aseprite. The first is the layer containing the graphics for the map, essentially the painted tiles. The second is the collision layer. I use a single tile, typically red, in the collision layer to indicate which spaces have collision. Collision in this case means things like walls. For interactable things in the map, this will likely be a third layer (or maybe an extension of the collision layer) which indicates the spawn locations of various entities. I'll then create the entities at runtime and they will particpate in the dynamic game update loop, not just be static in the map. This is how you can have opening doors, switches, things like that.
Actually, about arenas, one new thing I did with mine is implement the realloc
behavior described in
malloc(3). This was
because I was preparing to use STB image to load tileset PNGs from Aseprite, and
I didn't want to implement a PNG parser myself. STB image kindly provides
STBI_REALLOC_SIZED
which is like realloc but passes the size of the existing allocation for use
in allocators that don't track individual allocation size. This is often the
case for arena allocators, and mine is no different. As you can see in the
commit log, I forgot and then later remembered that SDL provides SDL Image to
do the same thing. Since I'm already using SDL, I STB image and use SDL Image
instead.
I also moved all of the game logic out of main.cc into its own game.{h|cc}
file. This is to help separate the OS-specific platform portion of the
implementation from the OS-agnostic game logic. I will have to add OS awareness
to things like my standard library, but that will come later. As part of moving
the game logic out of main.cc, I also created a few arenas on the platform
side, bundled them up into a simple Memory struct, and hand them off to
game_update which is the entrypoint for my game logic for a single frame.
Lastly, I also replaced rendering of the placeholder character with rendering
of one I drew in Aseprite. I drew 4 sprites for the player: up, down, left,
and right. I could have just exported a sprite sheet (as PNG) and loaded that
into an SDL_Texture, but since I already have an Aseprite-to-binary workflow
with the aforementioned conversion tool, I create the player as a tilemap,
export that, and then re-use my binary-parsing logic to get at the tileset for
the player. Thankfully this doesn't waste any memory ultimately. I allocate
the tilemap in the temp arena and just copy out what I need into the persistent
arena. The temp arena gets cleared at the end of the current frame on the
platform side.
There is a bit of a catch, though. Here is a cobbled-together group of struct definitions from across various game files.
struct Tileset {
SDL_Texture* atlas;
size_t atlas_width_tiles;
size_t atlas_height_tiles;
uint16_t tile_width_px;
uint16_t tile_height_px;
};
struct Entity {
EntityType type;
Vector2f pos;
Tileset ts;
};
struct Player {
Entity e;
FacingDirection facing;
Cooldown move_cooldown;
};
struct GameState {
uint64_t perf_frequency;
const char* base_path;
Player player;
Camera camera;
Map map;
};
Starting from the bottom of the definitions, GameState is what lives in my
persistent arena. Persistent means the memory survives across frames.
A Player is an Entity by composition, and an Entity contains a Tileset
which in turn contains an SDL_Texture. I have not replaced SDL's allocators
with my own allocators, and I'm not sure if I will. This means that I let SDL
use whatever its backing allocators are (not necessarily malloc), but I don't
control the memory pool itself. So, things are a little bit fragmented across
memory--I control many allocations in my game logic, but SDL just does whatever
it wants.
The reason I have not replaced SDL's allocators is because SDL uses free quite
a lot. I think I might have posted about this before... Anyway, I don't want to
write an optimized allocator at this time in my life. When I implemented "free"
for STB image, it was just ((void)0), meaning that I didn't actually do
anything, because my arena can't free individual allocations.
We'll see if this hurts me down the road.
Anyway, last point for today, about the Tileset. The name of the texture is
atlas. An atlas is just a single texture that may contain multiple tiles.
You get at the tile you want by specifying a subset of the atlas as a
source rectangle
when rendering. Player::FacingDirection in my case doubles as an index into
the atlas, so when I render a player, I render one of the aforementioned
directional sprites by indexing into the atlas.
In my last implementation, I was loading a unique texture for every sprite.
That may be less efficient at render time if the allocations for the sprites
live far away from each other in memory. That, and it's just more things to
keep track of. Map also uses Tileset, so atlases benefit us there, too,
where there are many more sprites than there typically are for entities like
players.
Here's a demo. Pardon the programmer art.