Skip to content

Conversation

@emidoots
Copy link
Member

In the past:

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.

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:

mach/ecs/src/main.zig

Lines 49 to 87 in 9792401

//-------------------------------------------------------------------------
// Create a world.
var world = try World.init(allocator);
defer world.deinit();
const player1 = try world.entities.new();
const player2 = try world.entities.new();
const player3 = try world.entities.new();
try world.entities.setComponent(player1, "physics", @as(u16, 1234));
try world.entities.setComponent(player1, "geometry", @as(u16, 1234));
try world.entities.setComponent(player2, "physics", @as(u16, 1234));
try world.entities.setComponent(player3, "physics", @as(u16, 1234));
const physics = (struct {
pub fn physics(adapter: *Adapter) void {
var iter = adapter.query(&.{"physics"});
std.debug.print("\nphysics ran\n", .{});
while (iter.next()) |row| {
std.debug.print("found entity: {}\n", .{row.entity});
defer row.unlock();
}
}
}).physics;
try world.register("physics", physics);
const rendering = (struct {
pub fn rendering(adapter: *Adapter) void {
var iter = adapter.query(&.{"geometry"});
std.debug.print("\nrendering ran\n", .{});
while (iter.next()) |row| {
std.debug.print("found entity: {}\n", .{row.entity});
defer row.unlock();
}
}
}).rendering;
try world.register("rendering", rendering);
world.tick();

mach/ecs/src/entities.zig

Lines 625 to 656 in 9792401

//-------------------------------------------------------------------------
// Define component types, any Zig type will do!
// A location component.
const Location = struct {
x: f32 = 0,
y: f32 = 0,
z: f32 = 0,
};
//-------------------------------------------------------------------------
// Create first player entity.
var player1 = try world.new();
try world.setComponent(player1, "name", "jane"); // add Name component
try world.setComponent(player1, "location", Location{}); // add Location component
// Create second player entity.
var player2 = try world.new();
try testing.expect(world.getComponent(player2, "location", Location) == null);
try testing.expect(world.getComponent(player2, "name", []const u8) == null);
//-------------------------------------------------------------------------
// We can add new components at will.
const Rotation = struct { degrees: f32 };
try world.setComponent(player2, "rotation", Rotation{ .degrees = 90 });
try testing.expect(world.getComponent(player1, "rotation", Rotation) == null); // player1 has no rotation
//-------------------------------------------------------------------------
// Remove a component from any entity at will.
// TODO: add a way to "cleanup" truly unused archetypes
try world.removeComponent(player1, "name");
try world.removeComponent(player1, "location");
try world.removeComponent(player1, "location"); // doesn't exist? no problem.

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

Signed-off-by: Stephen Gutekanst [email protected]

  • By selecting this checkbox, I agree to license my contributions to this project under the license(s) described in the LICENSE file, and I have the right to do so or have received permission to do so by an employer or client I am producing work for whom has this right.

In the past:

* #156 was the initial ECS implementation detailed in https://devlog.hexops.com/2022/lets-build-ecs-part-1
* #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](#157 (comment)).

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:

* https://github.com/hexops/mach/blob/979240135bbe12d32760eed9f29f9795ead3c340/ecs/src/main.zig#L49
* https://github.com/hexops/mach/blob/979240135bbe12d32760eed9f29f9795ead3c340/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 <[email protected]>
@emidoots emidoots merged commit 0ef13eb into main Mar 19, 2022
@emidoots emidoots deleted the sg/ecs-r3 branch March 19, 2022 17:59
@emidoots emidoots added the object object system label Aug 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

object object system

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants