mach/ecs/src/main.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

88 lines
3.3 KiB
Zig

//! mach/ecs is an Entity component system implementation.
//!
//! ## Design principles:
//!
//! * Clean-room implementation (author has not read any other ECS implementation code.)
//! * Solve the problems ECS solves, in a way that is natural to Zig and leverages Zig comptime.
//! * Avoid patent infringement upon Unity ECS patent claims.
//! * Fast. Optimal for CPU caches, multi-threaded, leverage comptime as much as is reasonable.
//! * Simple. Small API footprint, should be natural and fun - not like you're writing boilerplate.
//! * Enable other libraries to provide tracing, editors, visualizers, profilers, etc.
//!
//! ## Copyright & patent mitigation
//!
//! The initial implementation was a clean-room implementation by Stephen Gutekanst without having
//! read other ECS implementations' code, but with speaking to people familiar with other ECS
//! implementations. Contributions past the initial implementation may be made by individuals in
//! non-clean-room settings.
//!
//! Critically, this entity component system stores components for a classified archetype using
//! independent arrays allocated per component as well as hashmaps for sparse component data as an
//! optimization. This is a novel and fundamentally different process than what is described in
//! Unity Software Inc's patent US 10,599,560. This is not legal advice.
//!
const std = @import("std");
const testing = std.testing;
const EntityID = @import("entities.zig").EntityID;
const Entities = @import("entities.zig").Entities;
const Adapter = @import("systems.zig").Adapter;
const System = @import("systems.zig").System;
const World = @import("systems.zig").World;
// TODO:
// * Iteration
// * Querying
// * Multi threading
// * Multiple entities having one value
// * Sparse storage?
test "inclusion" {
_ = Entities;
}
test "example" {
const allocator = testing.allocator;
//-------------------------------------------------------------------------
// 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();
}