mach/ecs/src/systems.zig
Stephen Gutekanst 0ef13eb1cc ecs: third major redesign/rethink of implementation
In the past:

* hexops/mach#156 was the initial ECS implementation detailed in https://devlog.hexops.com/2022/lets-build-ecs-part-1
* hexops/mach#157 was the second major redesign in which we:
    * Eliminated major limitations (e.g. inability to add/remove components at runtime)
    * Investigated sparse sets
    * Began thinking in terms of databases
    * Enabled runtime introspection

Our second revision of the ECS, however, still had _archetypes_ exposed as a public-facing
user concern. When a new component was added to an entity, say a weapon, the table storing
entities of that archetype changed to effectively have a new column `?Weapon` with a null
value for _all existing entities of that archetype_. We can say that our ECS had archetypes
as a user-facing concern AND this made performance worse: when iterating all entities with
a weapon, we needed to check if the component value was `null` or not because every column
was `?Weapon` instead of a guaranteed non-null value like `Weapon`. This was a key learning
that I got from [discussing ECS tradeoffs with the Bevy team](https://github.com/hexops/mach/pull/157#issuecomment-1022916117).

This third revision of our ECS has some big benefits:

* Entities are now just IDs proper, you can add/remove arbitrary components at runtime.
    * You don't have an "entity which always belongs to one archetype table which changes"
    * Rather, you have an "entity of one archetype" and adding a component means that entity _moves_ from one archetype table to another.
    * Archetypes are now an implementation detail, not something you worry about as a consumer of the API.
* Performance
    * We benefit from the fact that we no longer need check if a component on an entity is `null` or not.
* Introspection
    * Previously iterating the component names/values an entity had was not possible, now it is.
* Querying & multi-threading
    * Very very early stages into this, but we now have a general plan for how querying will work and multi-threading.
    * Effectively, it will look much like interfacing with a database: you have a connection (we call it an adapter)
      and you can ask for information through that. More work to be done here.
* Systems, we now have a (very) basic starting point for how systems will work.

Some examples of how the API looks today:

* 979240135b/ecs/src/main.zig (L49)
* 979240135b/ecs/src/entities.zig (L625-L656)

Much more work to do, I will do a blog post detailing this step-by-step first though.

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
2022-03-19 10:59:26 -07:00

55 lines
1.4 KiB
Zig

const std = @import("std");
const mem = std.mem;
const Allocator = mem.Allocator;
const testing = std.testing;
const Entities = @import("entities.zig").Entities;
const Iterator = Entities.Iterator;
pub const Adapter = struct {
world: *World,
pub fn query(adapter: *Adapter, components: []const []const u8) Iterator {
return adapter.world.entities.query(components);
}
};
pub const System = fn (adapter: *Adapter) void;
pub const World = struct {
allocator: Allocator,
systems: std.StringArrayHashMapUnmanaged(System) = .{},
entities: Entities,
pub fn init(allocator: Allocator) !World {
return World{
.allocator = allocator,
.entities = try Entities.init(allocator),
};
}
pub fn deinit(world: *World) void {
world.systems.deinit(world.allocator);
world.entities.deinit();
}
pub fn register(world: *World, name: []const u8, system: System) !void {
try world.systems.put(world.allocator, name, system);
}
pub fn unregister(world: *World, name: []const u8) void {
world.systems.orderedRemove(name);
}
pub fn tick(world: *World) void {
var i: usize = 0;
while (i < world.systems.count()) : (i += 1) {
const system = world.systems.entries.get(i).value;
var adapter = Adapter{
.world = world,
};
system(&adapter);
}
}
};