2025-11-28
15:30:46, atom feed.
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.