2025-182
16:34:01, atom feed.
Today I implemented simple tilemap rendering. In my previous post, I discussed exporting a tilemap from an Aseprite file as a binary file. I parsed that binary file in my game and then used SDL3_image to load the tileset image. I was considering using stb image to do the loading, but since I'm committing to SDL, I figured I'd learn SDL3_image. If it feels too heavy, I'll switch to stb.
After building and using SDL3_image, when launching my game, the main window
would open, close quickly after, and then open again. The fix for this was
calling SDL_CreateWindow
with the SDL_WINDOW_HIDDEN
flag and then showing
the window only after I create the renderer (which I do separately from creating
the window).
The gist of accelerated graphics rendering in SDL is to load your media into a surface and then from a surface create a texture. In my case, I am dealing with tilemaps which themselves are built from tilesets. For the uninitiated, I'll give a quick outline.
A tileset consists of sprites. A sprite is essentially an image. Using text as
make-believe images, an example tileset might contain these three "sprites":
_.=
. Using the previous three characters, we might build the map:
========
=_..___=
=___...=
========
Who knows what that represents, but it is a map built from the "sprites" in our tileset. If we were talking about actual computer graphics, our map would be some larger, rendered image. The rendered image's size in RAM would increase as the image got larger, but we know that the image (in this examlpe) is only composed of three unique sprites, those from our tileset. We could compress the tilemap by instead representing it as indices into the tileset.
22222222
20110002
20001112
22222222
The sprites in a tileset are also known as tiles. So here we have represented a tilemap by showing which tiles to use when rendering the map by referring to tiles (by index) in a tileset.
Back to SDL, this leaves me with multiple ways to actually store the tile data. Currently my binary tilemap export only encodes a single tilemap (due to the way I drew the map in Aseprite). I could load that tileset and then parse individual tiles out of its pixel memory, create a surface for each unique tile, and then create a texture for each unique surface. Nothing wrong with this. But what I did instead was create a single texture for the tileset, and at render time, I index into the texture to render only a portion of the tileset texture (the tile I want) to the window. The upside of this is that there are fewer resources to deal with, fewer allocations. The downside is that all tiles are stored in a single texture, so I cannot apply per-tile modulation (recoloring, transforming, etc.) as easily.
Here is an excerpt of code I wrote showing how I render from this single tileset texture.
/**
* Render a tilemap instance to a renderer. `tileset` is expected to contain
* the entire tileset referred to by the tilemap, and the tile properties are
* assumed to be compatible with dimensions specified in the tilemap.
*/
void tilemap_render(Tilemap* tm, SDL_Texture* tileset, SDL_Renderer* renderer) {
uint16_t tileset_tile_width = tileset->w / tm->tile_width_px;
for (int i = 0; i < tm->height_tiles; i++) {
for (int j = 0; j < tm->width_in_tiles; j++) {
uint16_t tile_index = tm->tiles[i * tm->width_in_tiles + j];
float tile_x = tile_index % tileset_tile_width * tm->tile_width_px;
float tile_y = tile_index / tileset_tile_width * tm->tile_height_px;
SDL_FRect src_rect = {
tile_x, tile_y,
(float)tm->tile_width_px, (float)tm->tile_height_px
};
SDL_FRect dst_rect = {
(float)j * tm->tile_width_px,
(float)i * tm->tile_height_px,
(float)tm->tile_width_px,
(float)tm->tile_height_px
};
SDL_RenderTexture(renderer, tileset, &src_rect, &dst_rect);
}
}
}
I haven't done much work on the general encapsulation; just standing things up right now.