2026-01-09

14:48:17, atom feed.

This week I imlpemented an entity-component-system, a basic player attack, entity deletion, and text and textbox rendering.

Here is the curated commit log for this week's work.

2026-01-02
feat: implement an ECS; currently only the player is rendered

2026-01-03
feat: update enemy positions and render enemies using the ECS

2026-01-04
feat: add entity views, deletion, and initialize player/enemy procs

2026-01-05
feat: enable window resizing; set minimum window width and height

2026-01-05
feat: add SDL_ttf and extend our cmake build configuration

2026-01-05
feat: generate bindings for SDL_TTF

2026-01-05
feat: print debug text via SDL_TTF

2026-01-05
feat: update SDL3 and bindings to 3.4.0

2026-01-05
feat: downscale large text texture when rendering to improve sharpness

2026-01-06
feat: add health and action (attack); delete enemies with no health

2026-01-06
feat: build Components and supporting procedures via metaprogramming

2026-01-07
chore: call SDL_CreateWindowAndRenderer to avoid initial window flicker

2026-01-07
feat: export textbox Aseprite assets as binary as well

2026-01-07
feat: basic textbox rendering proof of concept

2026-01-08
feat: change font to Noto Sans; actually prime SDL3_ttf glyph cache

2026-01-08
feat: move text into new ui.jai; add tile-attributable text boxes

2026-01-09
feat: introduce big atlas; remove tilesets for entities, textboxes

2026-01-09
feat: move font loading in ui file

The more entities I thought about adding to the game, the more I wondered about how to organize them. Things like Player and Enemy largely have the same properties (well, are identical in all ways except the type itself in the type system at this time). Loading multiple instances of the same enemy had me loading the same tilesheet over and over and creating unique instances of textures that held the exact same data. Pathfinding had quirks because enemy positions always updated in the same order each turn, so if the first enemy to move was blocked by surrounding enemies, then after the surrounding enemies moved, the first enemy would stay in its original location for that turn. Maybe this was solveable by sorting enemies in some update queue, but how do I build the queue if we have more than just enemies moving around?

The concept of an entity-component-system came to mind, so this week I researched it and decided to implement a simple one. My current implementation is very simple but has taken me a long way.

Entities are as simple as

EntityType :: enum u8 {
  PLAYER :: 0;
  ENEMY  :: 0;
}

Entity :: struct {
  id: EntityType;
}

The game state instance stores a fixed number of entities as follows:

NUM_ENTITIES :: 100;

GameState :: struct {
  // ...

  _entities_data := [NUM_ENTITIES] Entity;
  entities := [] Entity;
}

_entities_data is the fixed-size backing storage for all entities, and entities is a size-adjustable view over the backing storage, the count of which allows us to track how many active entities we have, or in other words which subset of _entities_data is valid at any given time.

Components are arrays of wrappers around some property, where the wrapper also provides a boolean indicating whether the property is set.

Has :: struct(ElementType: Type) {
  prop: ElementType;
  has : bool;
}

Components :: struct {
  position: [MAX_ENTITIES] Has(Math.Vector2f);
  health  : [MAX_ENTITIES] Has(s16);
  // etc.
}

The index of an entity in GameState.entities is also used to get some corresponding component for that entity. The organization of Components is known as the struct-of-arrays (SoA) layout technique.

Before demonstrating a system, I'll elaborate on the entity view I created. As you can imagine, one issue with arranging data like this is that it takes work, for example in a debugger, to get at all the different properties of an entity. To make this easier, I implemented an entity view which provides pointers to all components for some entity.

EntityView :: struct {
  id: *EntityType;
  position: *Has(Math.Vector2f);
  health  : *Has(s16);
  // etc.
}

make_entity_view :: (
    index: s64, entities: [] Entity, components: Components) -> EntityView {
  return .{
    *entities[index].id,
    *components.position[index],
    *components.health[index]
  };
}

Lastly, systems are just algorithms that operate on components. For example:

update_positions :: (entities: [] Entity, components: Components) {
  for entities {
    ev := make_entity_view(it_index, entities, components);
    if #complete ev.id == {
      case .PLAYER;
        // update the player position using player input for this frame
      case .ENEMY;
        // update the enemy position using pathfinding
    }
  }
}

Deleting an entity works by:

  1. taking the index of the entity to delete
  2. copying the last valid entity in the entities view to the entity at the index to delete
  3. doing the same for every array in Components
  4. reducing entities.count by 1

We do the above because the backing storage is fixed-size, not a dynamically-sized data structure.

Of course, I added a component to the ECS and forgot to update the procedure to delete components, so when I did delete a component in-game, the entities started acting strangely. I thought about how to do this and decided to explore metaprogramming in Jai. Maybe there is a better way to metaprogram this, but what I implemented looks like the following (example is slimmed down):

NameAndType :: struct {
  name: string;
  type: Type;
}

create_component_names_and_types :: () -> [] NameAndType {
  #import,file "math.jai";

  array: [..] NameAndType;
  e := array_add(*array);
  e.name = "position";
  e.type = [MAX_ENTITIES] Has(Vector2f);
  return array;
}

component_names_and_types :: #run create_component_names_and_types();

delete_component_at :: (
    components: Components, entity_index: s64, last_index: s64) {
  pseudo_delete :: inline (arr: [] $T, index: s64, last_index: s64) {
    if index != last_index arr[index] = arr[last_index];
  }

  create_delete_list :: (nat: [] NameAndType) -> string {
    builder: String_Builder;
    for nat print_to_builder(
      *builder,
      "pseudo_delete(components.%, entity_index, last_index);\n",
      it.name);
    return builder_to_string(*builder);
  }

  #insert #run create_delete_list(component_names_and_types);
}

#scope_export

Components :: struct {
  create_component_members :: (nat: [] NameAndType) -> string {
    builder: String_Builder;
    for nat print_to_builder(*builder, "%: %;\n", it.name, it.type);
    return builder_to_string(*builder);
  }

  #insert #run create_component_members(component_names_and_types);
}

They keys here are the compiler directives #run which tell the compiler to execute the following code at compile time and #insert which tell the compiler to paste the following string or AST into the generated code. Essentially I have extracted the arrays of components out from the Components struct and now return an array of pairs of field names and types. In Components I generate the struct definition on-demand, and in the deletion procedure I generate the array deletion routines for all members. It's much more complicated than non-compile-time programming, but it saves me from a bug I've already hit, and I don't need to update any of the metaprogramming in general, so I decided the implementation will stay in the game.

By the way, importing my own math.jai in create_component_names_and_types was necessary to resolve my custom types. It seems the compile-time scope is different than the general module scope, which I guess makes sense if the idea is to allow generating code that affects the definitions available in the containing module scope.

Next up was text rendering text. The motivation for this came from wanting to draw text in UI elements, although I ended up implementing attributable textboxes as well as standalone textboxes in the end. Since I'm already using SDL3, I settled on bringing in SDL3_ttf. To do this, I generated Jai bindings for it, which just as simple as doing so for SDL3_image as discussed in a previous post.

Some text won't need to be re-rendered every single frame, such as labels in UI elements. When I say rendered, I mean that we won't draw text on the screen from a source string but will rather reuse an existing texture. Not everything behaves this way, though. An easy example, albeit not one in my game, is an FPS counter. Maybe a more practical example is a high-resolution timer counting down. To render this, we'd need to build some string representation of the timer value, then the typical SDL workflow is to render that to a surface (CPU-side) and then render that surface to a texture (GPU-side, if hardware acceleration is available). This is doable every frame for games that don't have too much per-frame compute. My game probably qualifies. But there is a more efficient way to do this which allows us to skip rendering to a texture and render directly to an SDL renderer. This is through SDL3's TTF_TextEngine construct.

We can create a text engine in one of 3 ways:

  1. TTF_CreateSurfaceTextEngine
  2. TTF_CreateRendererTextEngine
  3. TTF_CreateGPUTextEngine

The surface engine performs what I mentioned I wanted to avoid. The GPU engine always operates exclusively on the GPU, and I don't want to require that hardware acceleration be available to run my rather simple game. So I choose the renderer engine, which renders text to some SDL renderer, which in my case is the renderer associated with my game window.

The neat thing about the text engines is that they perform glyph caching. Essentially, if you want to build a new text object, if the engine has previously rendered all of the glyphs in your new text to render, it will reuse cached glyph textures and skip the string-to-texture rendering portion entirely.

I'm unsure how useful the following is, but I primed the glyph cache as follows at font load time.

load_font :: (
    /* ... */, prime_glyph_cache: bool, prime_at: Math.Vector2f) -> bool {

  // ...

  PRIMER_STRING :: #string EOS
abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!?,.:()[]
EOS
  primer_string := to_c_string(PRIMER_STRING,, allocator=temp);
  text_object = sdl3_ttf.TTF_CreateText(
    text_engine, text_font, primer_string, 0);
  assert(text_object != null);

  if prime_glyph_cache {
    ok := sdl3_ttf.TTF_DrawRendererText(text_object, prime_at.x, prime_at.y);
    if !ok {
      return false;
    }
  }

  return true;
}

Essentially, I just build a string of common characters and then render that when I load the font, which is in the game-initialize phase, which is essentially the first frame of the game. I render the string off screen even though I would otherwise draw over it with the game map anyway. This optimiation only works with English, but perhaps localization routines could provide different primer strings. Languages like Japanese would be interesting. We don't want a string of thousands of characters. Instead, we'd probably scan all Japanese text for our game and build a set of unique characters and then have that set be our primer string.

Rendering to a logical presentation instead of the actual window dimensions has blessings and curses. One blessing is that we get automatic letterboxing, although the scaling is not necessarily perfect. One curse is that we upscale rendered text, and the upscaling of text is just pixel-by-pixel, not vectorized. So, the text becomes blurry. Here is an interesting excerpt from the SDL docs themselves (ref):

perhaps most of the rendering is done to specific dimensions but to make fonts look sharp, the app turns off logical presentation while drawing text, for example

So dealing with that is in my future. The blurry font is growing on me, but it clashes with the sharply-upscaled pixel art of everything else in the game.

To address loading the same tilesets and corresponding textures multiple times, one for each instance of the same type of entity, I created what I call "big atlas." There's nothing special about it--it's just a typical texture atlas. But similar to games of old, this one atlas will hold most of my art, save for that for game maps. Similar to the update_positions procedure suggested above, I have an Entity.render procedure that loops through all entities and renders them. We switch on the entity ID. If the ID is a player, we know to use certain indices into the atlas. Similarly for enemies. So we don't even need to track tilesets or textures or even indicies in the entity or component storage anymore. Textbox graphics are also in the big atlas, although textboxes are not part of the ECS, are handled separatly by my UI procedures.

Here is a video of everything in action--window resizing, text boxes, and attacking (the action itself is not visible) enemies and having them get deleted when their health reaches 0.