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>
88 lines
3.3 KiB
Zig
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();
|
|
}
|