ecs-invaders
Source: https://codeberg.org/knighting/ecs-invaders
Asciinema: https://asciinema.org/a/BgUlsMEyt3oc2sjT
I’m not a game developer but after learning some bits about ECS (Entity Component System) I just had to try it. NOTE: I have only vague understanding of inner workings and correct usage.
On high level the premise is query your entities like database. You declare components (data) and then write systems (functions) where each system can specify which components it needs, thus system iterates only over matching entities.
For example, my simple game renders characters to terminal, so I’ve created Character component that will be used for all visual entities.
const Character = struct {
grapheme: []const u8,
};
Simplified version of draw system looks like this. Given coordinates (*const Transform) and *const Character it writes Character’s grapheme to the cell.
fn drawCharacters(
ctx: struct {
win: *const vaxis.Window,
},
transform: *const Transform,
char: *const Character,
) void {
const x: u16 = @trunc(transform.pos.x);
const y: u16 = @trunc(transform.pos.y);
ctx.win.writeCell(x, y, .{
.char = .{ .grapheme = char.grapheme },
});
}
But after a while, I decided to add explosion animations, how do you do that? With current approach, it’s possible to add a new animate system without changing draw system!
Here’s Animation component with some helper methods.
const Animation = struct {
string: []const []const u8,
current_index: usize = 0,
lifetime: usize,
fn increment(self: *Animation) void {
self.current_index += 1;
if (self.current_index >= self.string.len) {
self.current_index = 0;
}
}
fn grapheme(self: *const Animation) []const u8 {
return self.string[self.current_index];
}
};
and animate system.
fn animate(es: *const Entities, cb: *CmdBuf) void {
var it = es.iterator(struct {
char: *Character,
anim: *Animation,
entity: Entity,
});
while (it.next(es)) |e| {
if (e.anim.lifetime == 0) {
e.entity.destroy(cb);
return;
}
e.anim.lifetime -= 1;
e.anim.increment();
e.char.grapheme = e.anim.grapheme();
}
}
I’ve barely scratched the surface, there are a lot of builtin goodies. For example Node extension that allows you to establish parent-child relations. I hope this can serve as an entry-level example of ECS.
What I didn’t get easily
Collisions
I had to look up what https://github.com/MasonRemaley/2Pew/blob/main/src/update.zig is doing for that. Turns out, you can simply copy iterator and iterate them both.
fn collide(es: *const Entities, cb: *CmdBuf, bounds: Vec) void {
var it = es.iterator(struct {});
while (it.next(es)) |first| {
var second_it = it;
while (second_it.next(es)) |second| {
// ...
}
}
}
Grid movement
Initially bullets had velocity y=1 and obstacles y=-1, but then, if collision detection is run only once, it’s possible for bullets to step over obstacles.
Dependencies
Supported Zig versions
0.16