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.