diff --git a/ecs/src/entities.zig b/ecs/src/entities.zig new file mode 100644 index 0000000000..224f6fd6dd --- /dev/null +++ b/ecs/src/entities.zig @@ -0,0 +1,692 @@ +const std = @import("std"); +const mem = std.mem; +const Allocator = mem.Allocator; +const testing = std.testing; +const builtin = @import("builtin"); +const assert = std.debug.assert; + +/// An entity ID uniquely identifies an entity globally within an Entities set. +pub const EntityID = u64; + +/// Represents the storage for a single type of component within a single type of entity. +/// +/// Database equivalent: a column within a table. +pub fn ComponentStorage(comptime Component: type) type { + return struct { + /// A reference to the total number of entities with the same type as is being stored here. + total_rows: *usize, + + /// The actual component data. This starts as empty, and then based on the first call to + /// .set() or .setDense() is initialized as dense storage (an array) or sparse storage (a + /// hashmap.) + /// + /// Sparse storage may turn to dense storage if someone later calls .set(), see that method + /// for details. + data: std.ArrayListUnmanaged(Component) = .{}, + + const Self = @This(); + + pub fn deinit(storage: *Self, allocator: Allocator) void { + storage.data.deinit(allocator); + } + + // If the storage of this component is sparse, it is turned dense as calling this method + // indicates that the caller expects to set this component for most entities rather than + // sparsely. + pub fn set(storage: *Self, allocator: Allocator, row_index: u32, component: Component) !void { + if (storage.data.items.len <= row_index) try storage.data.appendNTimes(allocator, undefined, storage.data.items.len + 1 - row_index); + storage.data.items[row_index] = component; + } + + /// Removes the given row index. + pub fn remove(storage: *Self, row_index: u32) void { + if (storage.data.items.len > row_index) { + _ = storage.data.swapRemove(row_index); + } + } + + /// Gets the component value for the given entity ID. + pub inline fn get(storage: Self, row_index: u32) Component { + return storage.data.items[row_index]; + } + + pub inline fn copy(dst: *Self, allocator: Allocator, src_row: u32, dst_row: u32, src: *Self) !void { + try dst.set(allocator, dst_row, src.get(src_row)); + } + + pub inline fn copySparse(dst: *Self, allocator: Allocator, src_row: u32, dst_row: u32, src: *Self) !void { + // TODO: setSparse! + try dst.set(allocator, dst_row, src.get(src_row)); + } + }; +} + +/// A type-erased representation of ComponentStorage(T) (where T is unknown). +/// +/// This is useful as it allows us to store all of the typed ComponentStorage as values in a hashmap +/// despite having different types, and allows us to still deinitialize them without knowing the +/// underlying type. +pub const ErasedComponentStorage = struct { + ptr: *anyopaque, + deinit: fn (erased: *anyopaque, allocator: Allocator) void, + remove: fn (erased: *anyopaque, row: u32) void, + cloneType: fn (erased: ErasedComponentStorage, total_entities: *usize, allocator: Allocator, retval: *ErasedComponentStorage) error{OutOfMemory}!void, + copy: fn (dst_erased: *anyopaque, allocator: Allocator, src_row: u32, dst_row: u32, src_erased: *anyopaque) error{OutOfMemory}!void, + copySparse: fn (dst_erased: *anyopaque, allocator: Allocator, src_row: u32, dst_row: u32, src_erased: *anyopaque) error{OutOfMemory}!void, + + pub fn cast(ptr: *anyopaque, comptime Component: type) *ComponentStorage(Component) { + var aligned = @alignCast(@alignOf(*ComponentStorage(Component)), ptr); + return @ptrCast(*ComponentStorage(Component), aligned); + } +}; + +/// Represents a single archetype, that is, entities which have the same exact set of component +/// types. When a component is added or removed from an entity, it's archetype changes. +/// +/// Database equivalent: a table where rows are entities and columns are components (dense storage). +pub const ArchetypeStorage = struct { + allocator: Allocator, + + /// The hash of every component name in this archetype, i.e. the name of this archetype. + hash: u64, + + /// A mapping of rows in the table to entity IDs. + /// + /// Doubles as the counter of total number of rows that have been reserved within this + /// archetype table. + entity_ids: std.ArrayListUnmanaged(EntityID) = .{}, + + /// A string hashmap of component_name -> type-erased *ComponentStorage(Component) + components: std.StringArrayHashMapUnmanaged(ErasedComponentStorage), + + /// Calculates the storage.hash value. This is a hash of all the component names, and can + /// effectively be used to uniquely identify this table within the database. + pub fn calculateHash(storage: *ArchetypeStorage) void { + storage.hash = 0; + var iter = storage.components.iterator(); + while (iter.next()) |entry| { + const component_name = entry.key_ptr.*; + storage.hash ^= std.hash_map.hashString(component_name); + } + } + + pub fn deinit(storage: *ArchetypeStorage) void { + for (storage.components.values()) |erased| { + erased.deinit(erased.ptr, storage.allocator); + } + storage.entity_ids.deinit(storage.allocator); + storage.components.deinit(storage.allocator); + } + + /// New reserves a row for storing an entity within this archetype table. + pub fn new(storage: *ArchetypeStorage, entity: EntityID) !u32 { + // Return a new row index + const new_row_index = storage.entity_ids.items.len; + try storage.entity_ids.append(storage.allocator, entity); + return @intCast(u32, new_row_index); + } + + /// Undoes the last call to the new() operation, effectively unreserving the row that was last + /// reserved. + pub fn undoNew(storage: *ArchetypeStorage) void { + _ = storage.entity_ids.pop(); + } + + /// Sets the value of the named component (column) for the given row in the table. Realizes the + /// deferred allocation of column storage for N entities (storage.counter) if it is not already. + pub fn set(storage: *ArchetypeStorage, row_index: u32, name: []const u8, component: anytype) !void { + var component_storage_erased = storage.components.get(name).?; + var component_storage = ErasedComponentStorage.cast(component_storage_erased.ptr, @TypeOf(component)); + try component_storage.set(storage.allocator, row_index, component); + } + + /// Removes the specified row. See also the `Entity.delete()` helper. + /// + /// This merely marks the row as removed, the same row index will be recycled the next time a + /// new row is requested via `new()`. + pub fn remove(storage: *ArchetypeStorage, row_index: u32) !void { + _ = storage.entity_ids.swapRemove(row_index); + for (storage.components.values()) |component_storage| { + component_storage.remove(component_storage.ptr, row_index); + } + } + + /// The number of entities actively stored in this table (not counting entities which are + /// allocated in this table but have been removed) + pub fn count(storage: *ArchetypeStorage) usize { + return storage.entity_ids.items.len; + } + + /// Tells if this archetype has every one of the given components. + pub fn hasComponents(storage: *ArchetypeStorage, components: []const []const u8) bool { + for (components) |component_name| { + if (!storage.components.contains(component_name)) return false; + } + return true; + } +}; + +pub const void_archetype_hash = std.math.maxInt(u64); + +/// A database of entities. For example, all player, monster, etc. entities in a game world. +/// +/// ``` +/// const world = Entities.init(allocator); // all entities in our world. +/// defer world.deinit(); +/// +/// const player1 = world.new(); // our first "player" entity +/// const player2 = world.new(); // our second "player" entity +/// ``` +/// +/// Entities are divided into archetypes for optimal, CPU cache efficient storage. For example, all +/// entities with two components `Location` and `Name` are stored in the same table dedicated to +/// densely storing `(Location, Name)` rows in contiguous memory. This not only ensures CPU cache +/// efficiency (leveraging data oriented design) which improves iteration speed over entities for +/// example, but makes queries like "find all entities with a Location component" ridiculously fast +/// because one need only find the tables which have a column for storing Location components and it +/// is then guaranteed every entity in the table has that component (entities do not need to be +/// checked one by one to determine if they have a Location component.) +/// +/// Components can be added and removed to entities at runtime as you please: +/// +/// ``` +/// try player1.set("rotation", Rotation{ .degrees = 90 }); +/// try player1.remove("rotation"); +/// ``` +/// +/// When getting a component value, you must know it's type or undefined behavior will occur: +/// TODO: improve this! +/// +/// ``` +/// if (player1.get("rotation", Rotation)) |rotation| { +/// // player1 had a rotation component! +/// } +/// ``` +/// +/// When a component is added or removed from an entity, it's archetype is said to change. For +/// example player1 may have had the archetype `(Location, Name)` before, and after adding the +/// rotation component has the archetype `(Location, Name, Rotation)`. It will be automagically +/// "moved" from the table that stores entities with `(Location, Name)` components to the table that +/// stores `(Location, Name, Rotation)` components for you. +/// +/// You can have 65,535 archetypes in total, and 4,294,967,295 entities total. Entities which are +/// deleted are merely marked as "unused" and recycled +/// +/// Database equivalents: +/// * Entities is a database of tables, where each table represents a single archetype. +/// * ArchetypeStorage is a table, whose rows are entities and columns are components. +/// * EntityID is a mere 32-bit array index, pointing to a 16-bit archetype table index and 32-bit +/// row index, enabling entities to "move" from one archetype table to another seamlessly and +/// making lookup by entity ID a few cheap array indexing operations. +/// * ComponentStorage(T) is a column of data within a table for a single type of component `T`. +pub const Entities = struct { + allocator: Allocator, + + /// TODO! + counter: EntityID = 0, + + /// A mapping of entity IDs (array indices) to where an entity's component values are actually + /// stored. + entities: std.AutoHashMapUnmanaged(EntityID, Pointer) = .{}, + + /// A mapping of archetype hash to their storage. + /// + /// Database equivalent: table name -> tables representing entities. + archetypes: std.AutoArrayHashMapUnmanaged(u64, ArchetypeStorage) = .{}, + + /// Points to where an entity is stored, specifically in which archetype table and in which row + /// of that table. That is, the entity's component values are stored at: + /// + /// ``` + /// Entities.archetypes[ptr.archetype_index].rows[ptr.row_index] + /// ``` + /// + pub const Pointer = struct { + archetype_index: u16, + row_index: u32, + }; + + pub const Iterator = struct { + entities: *Entities, + components: []const []const u8, + archetype_index: usize = 0, + row_index: usize = 0, + + pub const Entry = struct { + entity: EntityID, + + pub fn unlock(e: Entry) void { + _ = e; + } + }; + + pub fn next(iter: *Iterator) ?Entry { + const entities = iter.entities; + + // If the archetype table we're looking at does not contain the components we're + // querying for, keep searching through tables until we find one that does. + var archetype = entities.archetypes.entries.get(iter.archetype_index).value; + while (!archetype.hasComponents(iter.components) or iter.row_index >= archetype.count()) { + iter.archetype_index += 1; + iter.row_index = 0; + if (iter.archetype_index >= entities.archetypes.count()) { + return null; + } + archetype = entities.archetypes.entries.get(iter.archetype_index).value; + } + + const row_entity_id = archetype.entity_ids.items[iter.row_index]; + iter.row_index += 1; + return Entry{ .entity = row_entity_id }; + } + }; + + pub fn query(entities: *Entities, components: []const []const u8) Iterator { + return Iterator{ + .entities = entities, + .components = components, + }; + } + + pub fn init(allocator: Allocator) !Entities { + var entities = Entities{ .allocator = allocator }; + + try entities.archetypes.put(allocator, void_archetype_hash, ArchetypeStorage{ + .allocator = allocator, + .components = .{}, + .hash = void_archetype_hash, + }); + + return entities; + } + + pub fn deinit(entities: *Entities) void { + entities.entities.deinit(entities.allocator); + + var iter = entities.archetypes.iterator(); + while (iter.next()) |entry| { + entry.value_ptr.deinit(); + } + entities.archetypes.deinit(entities.allocator); + } + + /// Returns a new entity. + pub fn new(entities: *Entities) !EntityID { + const new_id = entities.counter; + entities.counter += 1; + + var void_archetype = entities.archetypes.getPtr(void_archetype_hash).?; + const new_row = try void_archetype.new(new_id); + const void_pointer = Pointer{ + .archetype_index = 0, // void archetype is guaranteed to be first index + .row_index = new_row, + }; + + entities.entities.put(entities.allocator, new_id, void_pointer) catch |err| { + void_archetype.undoNew(); + return err; + }; + return new_id; + } + + /// Removes an entity. + pub fn remove(entities: *Entities, entity: EntityID) !void { + var archetype = entities.archetypeByID(entity); + const ptr = entities.entities.get(entity).?; + + // A swap removal will be performed, update the entity stored in the last row of the + // archetype table to point to the row the entity we are removing is currently located. + const last_row_entity_id = archetype.entity_ids.items[archetype.entity_ids.items.len - 1]; + try entities.entities.put(entities.allocator, last_row_entity_id, Pointer{ + .archetype_index = ptr.archetype_index, + .row_index = ptr.row_index, + }); + + // Perform a swap removal to remove our entity from the archetype table. + try archetype.remove(ptr.row_index); + + _ = entities.entities.remove(entity); + } + + /// Returns the archetype storage for the given entity. + pub inline fn archetypeByID(entities: *Entities, entity: EntityID) *ArchetypeStorage { + const ptr = entities.entities.get(entity).?; + return &entities.archetypes.values()[ptr.archetype_index]; + } + + /// Sets the named component to the specified value for the given entity, + /// moving the entity from it's current archetype table to the new archetype + /// table if required. + pub fn setComponent(entities: *Entities, entity: EntityID, name: []const u8, component: anytype) !void { + var archetype = entities.archetypeByID(entity); + + // Determine the old hash for the archetype. + const old_hash = archetype.hash; + + // Determine the new hash for the archetype + new component + var have_already = archetype.components.contains(name); + const new_hash = if (have_already) old_hash else old_hash ^ std.hash_map.hashString(name); + + // Find the archetype storage for this entity. Could be a new archetype storage table (if a + // new component was added), or the same archetype storage table (if just updating the + // value of a component.) + var archetype_entry = try entities.archetypes.getOrPut(entities.allocator, new_hash); + if (!archetype_entry.found_existing) { + archetype_entry.value_ptr.* = ArchetypeStorage{ + .allocator = entities.allocator, + .components = .{}, + .hash = 0, + }; + var new_archetype = archetype_entry.value_ptr; + + // Create storage/columns for all of the existing components on the entity. + var column_iter = archetype.components.iterator(); + while (column_iter.next()) |entry| { + var erased: ErasedComponentStorage = undefined; + entry.value_ptr.cloneType(entry.value_ptr.*, &new_archetype.entity_ids.items.len, entities.allocator, &erased) catch |err| { + assert(entities.archetypes.swapRemove(new_hash)); + return err; + }; + new_archetype.components.put(entities.allocator, entry.key_ptr.*, erased) catch |err| { + assert(entities.archetypes.swapRemove(new_hash)); + return err; + }; + } + + // Create storage/column for the new component. + const erased = entities.initErasedStorage(&new_archetype.entity_ids.items.len, @TypeOf(component)) catch |err| { + assert(entities.archetypes.swapRemove(new_hash)); + return err; + }; + new_archetype.components.put(entities.allocator, name, erased) catch |err| { + assert(entities.archetypes.swapRemove(new_hash)); + return err; + }; + + new_archetype.calculateHash(); + } + + // Either new storage (if the entity moved between storage tables due to having a new + // component) or the prior storage (if the entity already had the component and it's value + // is merely being updated.) + var current_archetype_storage = archetype_entry.value_ptr; + + if (new_hash == old_hash) { + // Update the value of the existing component of the entity. + const ptr = entities.entities.get(entity).?; + try current_archetype_storage.set(ptr.row_index, name, component); + return; + } + + // Copy to all component values for our entity from the old archetype storage + // (archetype) to the new one (current_archetype_storage). + const new_row = try current_archetype_storage.new(entity); + const old_ptr = entities.entities.get(entity).?; + + // Update the storage/columns for all of the existing components on the entity. + var column_iter = archetype.components.iterator(); + while (column_iter.next()) |entry| { + var old_component_storage = entry.value_ptr; + var new_component_storage = current_archetype_storage.components.get(entry.key_ptr.*).?; + new_component_storage.copy(new_component_storage.ptr, entities.allocator, new_row, old_ptr.row_index, old_component_storage.ptr) catch |err| { + current_archetype_storage.undoNew(); + return err; + }; + } + current_archetype_storage.entity_ids.items[new_row] = entity; + + // Update the storage/column for the new component. + current_archetype_storage.set(new_row, name, component) catch |err| { + current_archetype_storage.undoNew(); + return err; + }; + + var swapped_entity_id = archetype.entity_ids.items[archetype.entity_ids.items.len - 1]; + archetype.remove(old_ptr.row_index) catch |err| { + current_archetype_storage.undoNew(); + return err; + }; + try entities.entities.put(entities.allocator, swapped_entity_id, old_ptr); + + try entities.entities.put(entities.allocator, entity, Pointer{ + .archetype_index = @intCast(u16, archetype_entry.index), + .row_index = new_row, + }); + return; + } + + /// gets the named component of the given type (which must be correct, otherwise undefined + /// behavior will occur). Returns null if the component does not exist on the entity. + pub fn getComponent(entities: *Entities, entity: EntityID, name: []const u8, comptime Component: type) ?Component { + var archetype = entities.archetypeByID(entity); + + var component_storage_erased = archetype.components.get(name) orelse return null; + + const ptr = entities.entities.get(entity).?; + var component_storage = ErasedComponentStorage.cast(component_storage_erased.ptr, Component); + return component_storage.get(ptr.row_index); + } + + /// Removes the named component from the entity, or noop if it doesn't have such a component. + pub fn removeComponent(entities: *Entities, entity: EntityID, name: []const u8) !void { + var archetype = entities.archetypeByID(entity); + if (!archetype.components.contains(name)) return; + + // Determine the old hash for the archetype. + const old_hash = archetype.hash; + + // Determine the new hash for the archetype with the component removed + var new_hash: u64 = 0; + var iter = archetype.components.iterator(); + while (iter.next()) |entry| { + const component_name = entry.key_ptr.*; + if (!std.mem.eql(u8, component_name, name)) new_hash ^= std.hash_map.hashString(component_name); + } + assert(new_hash != old_hash); + + // Find the archetype storage for this entity. Could be a new archetype storage table (if a + // new component was added), or the same archetype storage table (if just updating the + // value of a component.) + var archetype_entry = try entities.archetypes.getOrPut(entities.allocator, new_hash); + if (!archetype_entry.found_existing) { + archetype_entry.value_ptr.* = ArchetypeStorage{ + .allocator = entities.allocator, + .components = .{}, + .hash = 0, + }; + var new_archetype = archetype_entry.value_ptr; + + // Create storage/columns for all of the existing components on the entity. + var column_iter = archetype.components.iterator(); + while (column_iter.next()) |entry| { + if (std.mem.eql(u8, entry.key_ptr.*, name)) continue; + var erased: ErasedComponentStorage = undefined; + entry.value_ptr.cloneType(entry.value_ptr.*, &new_archetype.entity_ids.items.len, entities.allocator, &erased) catch |err| { + assert(entities.archetypes.swapRemove(new_hash)); + return err; + }; + new_archetype.components.put(entities.allocator, entry.key_ptr.*, erased) catch |err| { + assert(entities.archetypes.swapRemove(new_hash)); + return err; + }; + } + new_archetype.calculateHash(); + } + + // Either new storage (if the entity moved between storage tables due to having a new + // component) or the prior storage (if the entity already had the component and it's value + // is merely being updated.) + var current_archetype_storage = archetype_entry.value_ptr; + + // Copy to all component values for our entity from the old archetype storage + // (archetype) to the new one (current_archetype_storage). + const new_row = try current_archetype_storage.new(entity); + const old_ptr = entities.entities.get(entity).?; + + // Update the storage/columns for all of the existing components on the entity. + var column_iter = current_archetype_storage.components.iterator(); + while (column_iter.next()) |entry| { + var src_component_storage = archetype.components.get(entry.key_ptr.*).?; + var dst_component_storage = entry.value_ptr; + dst_component_storage.copy(dst_component_storage.ptr, entities.allocator, new_row, old_ptr.row_index, src_component_storage.ptr) catch |err| { + current_archetype_storage.undoNew(); + return err; + }; + } + current_archetype_storage.entity_ids.items[new_row] = entity; + + var swapped_entity_id = archetype.entity_ids.items[archetype.entity_ids.items.len - 1]; + archetype.remove(old_ptr.row_index) catch |err| { + current_archetype_storage.undoNew(); + return err; + }; + try entities.entities.put(entities.allocator, swapped_entity_id, old_ptr); + + try entities.entities.put(entities.allocator, entity, Pointer{ + .archetype_index = @intCast(u16, archetype_entry.index), + .row_index = new_row, + }); + return; + } + + // TODO: iteration over all entities + // TODO: iteration over all entities with components (U, V, ...) + // TODO: iteration over all entities with type T + // TODO: iteration over all entities with type T and components (U, V, ...) + + // TODO: "indexes" - a few ideas we could express: + // + // * Graph relations index: e.g. parent-child entity relations for a DOM / UI / scene graph. + // * Spatial index: "give me all entities within 5 units distance from (x, y, z)" + // * Generic index: "give me all entities where arbitraryFunction(e) returns true" + // + + pub fn initErasedStorage(entities: *const Entities, total_rows: *usize, comptime Component: type) !ErasedComponentStorage { + var new_ptr = try entities.allocator.create(ComponentStorage(Component)); + new_ptr.* = ComponentStorage(Component){ .total_rows = total_rows }; + + return ErasedComponentStorage{ + .ptr = new_ptr, + .deinit = (struct { + pub fn deinit(erased: *anyopaque, allocator: Allocator) void { + var ptr = ErasedComponentStorage.cast(erased, Component); + ptr.deinit(allocator); + allocator.destroy(ptr); + } + }).deinit, + .remove = (struct { + pub fn remove(erased: *anyopaque, row: u32) void { + var ptr = ErasedComponentStorage.cast(erased, Component); + ptr.remove(row); + } + }).remove, + .cloneType = (struct { + pub fn cloneType(erased: ErasedComponentStorage, _total_rows: *usize, allocator: Allocator, retval: *ErasedComponentStorage) !void { + var new_clone = try allocator.create(ComponentStorage(Component)); + new_clone.* = ComponentStorage(Component){ .total_rows = _total_rows }; + var tmp = erased; + tmp.ptr = new_clone; + retval.* = tmp; + } + }).cloneType, + .copy = (struct { + pub fn copy(dst_erased: *anyopaque, allocator: Allocator, src_row: u32, dst_row: u32, src_erased: *anyopaque) !void { + var dst = ErasedComponentStorage.cast(dst_erased, Component); + var src = ErasedComponentStorage.cast(src_erased, Component); + return dst.copy(allocator, src_row, dst_row, src); + } + }).copy, + .copySparse = (struct { + pub fn copySparse(dst_erased: *anyopaque, allocator: Allocator, src_row: u32, dst_row: u32, src_erased: *anyopaque) !void { + var dst = ErasedComponentStorage.cast(dst_erased, Component); + var src = ErasedComponentStorage.cast(src_erased, Component); + return dst.copySparse(allocator, src_row, dst_row, src); + } + }).copySparse, + }; + } + + // TODO: ability to remove archetype entirely, deleting all entities in it + // TODO: ability to remove archetypes with no entities (garbage collection) +}; + +test "entity ID size" { + try testing.expectEqual(8, @sizeOf(EntityID)); +} + +test "example" { + const allocator = testing.allocator; + + //------------------------------------------------------------------------- + // Create a world. + var world = try Entities.init(allocator); + defer world.deinit(); + + //------------------------------------------------------------------------- + // 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. + + //------------------------------------------------------------------------- + // Introspect things. + // + // Archetype IDs, these are our "table names" - they're just hashes of all the component names + // within the archetype table. + var archetypes = world.archetypes.keys(); + try testing.expectEqual(@as(usize, 6), archetypes.len); + try testing.expectEqual(@as(u64, 18446744073709551615), archetypes[0]); + try testing.expectEqual(@as(u64, 6893717443977936573), archetypes[1]); + try testing.expectEqual(@as(u64, 7008573051677164842), archetypes[2]); + try testing.expectEqual(@as(u64, 14420739110802803032), archetypes[3]); + try testing.expectEqual(@as(u64, 13913849663823266920), archetypes[4]); + try testing.expectEqual(@as(u64, 0), archetypes[5]); + + // Number of (living) entities stored in an archetype table. + try testing.expectEqual(@as(usize, 0), world.archetypes.get(archetypes[2]).?.count()); + + // Component names for a given archetype. + var component_names = world.archetypes.get(archetypes[2]).?.components.keys(); + try testing.expectEqual(@as(usize, 2), component_names.len); + try testing.expectEqualStrings("name", component_names[0]); + try testing.expectEqualStrings("location", component_names[1]); + + // Component names for a given entity + var player2_archetype = world.archetypeByID(player2); + component_names = player2_archetype.components.keys(); + try testing.expectEqual(@as(usize, 1), component_names.len); + try testing.expectEqualStrings("rotation", component_names[0]); + + // TODO: iterating components an entity has not currently supported. + + //------------------------------------------------------------------------- + // Remove an entity whenever you wish. Just be sure not to try and use it later! + try world.remove(player1); +} diff --git a/ecs/src/main.zig b/ecs/src/main.zig index 4a05e0abbc..e8fecf041a 100644 --- a/ecs/src/main.zig +++ b/ecs/src/main.zig @@ -21,540 +21,68 @@ //! 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 mem = std.mem; -const Allocator = mem.Allocator; const testing = std.testing; -const builtin = @import("builtin"); -const assert = std.debug.assert; - -/// An entity ID uniquely identifies an entity globally within an Entities set. -/// -/// It stores the type of entity, as well the index of the entity within EntityTypeStorage, in only -/// 48 bits. -/// -/// Database equivalent: a row within a table -pub const EntityID = packed struct { - /// Entity type ("table ID") - type_id: u16, - - /// Entity ID ("row index") - id: u32, -}; - -/// Entity is a thin wrapper over an entity ID that makes interacting with a specific entity nicer. -/// -/// Database equivalent: a row within a table -pub const Entity = struct { - /// The ID of the entity. - id: EntityID, - - /// The entity type corresponding to id.type_id. You can look this up using Entities.byID() - /// - /// Database equivalent: table of entities - entity_type: *EntityTypeStorage, - - /// Adds or updates a component for this entity. - /// - /// Optimized for *most* entities (of this type) having this type of component. If only a few - /// entities will have it, use `.setSparse()` instead. - pub inline fn set(entity: Entity, name: []const u8, component: anytype) !void { - var entity_type = entity.entity_type; - var storage = try entity_type.get(name, @TypeOf(component)); - try storage.set(entity_type.allocator, entity.id.id, component); - } - - /// Adds or updates a component for this entity. - /// - /// Optimized for *few* entities (of this type) having this type of component. If most entities - /// will have it, use `.set()` instead. - pub inline fn setSparse(entity: Entity, component_name: []const u8, component: anytype) !void { - var entity_type = entity.entity_type; - var storage = try entity_type.get(component_name, @TypeOf(component)); - try storage.setSparse(entity_type.allocator, entity.id.id, component); - } - - /// Gets a component for this entity, returns null if that component is not set on this entity. - pub inline fn get(entity: Entity, component_name: []const u8, comptime Component: anytype) ?Component { - var entity_type = entity.entity_type; - var storage = entity_type.getIfExists(component_name, Component) orelse return null; - return storage.get(entity.id.id); - } - - /// Removes the given component from this entity, returning a boolean indicating if it did - /// exist on the entity. - pub inline fn remove(entity: Entity, component_name: []const u8) bool { - var entity_type = entity.entity_type; - var storage = entity_type.getErasedIfExists(component_name) orelse return false; - return storage.remove(storage.ptr, entity.id.id); - } - - // Deletes this entity. - pub inline fn delete(entity: Entity) !void { - var entity_type = entity.entity_type; - try entity_type.delete(entity.id); - } - - // TODO: iterator over all components for the entity -}; -/// Represents the storage for a single type of component within a single type of entity. -/// -/// Database equivalent: a column within a table. -pub fn ComponentStorage(comptime Component: type) type { - return struct { - /// A reference to the total number of entities with the same type as is being stored here. - total_entities: *u32, +const EntityID = @import("entities.zig").EntityID; +const Entities = @import("entities.zig").Entities; - /// The actual component data. This starts as empty, and then based on the first call to - /// .set() or .setDense() is initialized as dense storage (an array) or sparse storage (a - /// hashmap.) - /// - /// Sparse storage may turn to dense storage if someone later calls .set(), see that method - /// for details. - data: union(StorageType) { - empty: void, - dense: std.ArrayListUnmanaged(?Component), - sparse: std.AutoArrayHashMapUnmanaged(u32, Component), - } = .{ .empty = {} }, +const Adapter = @import("systems.zig").Adapter; +const System = @import("systems.zig").System; +const World = @import("systems.zig").World; - pub const StorageType = enum { - empty, - dense, - sparse, - }; - - const Self = @This(); - - pub fn deinit(storage: *Self, allocator: Allocator) void { - switch (storage.data) { - .empty => {}, - .dense => storage.data.dense.deinit(allocator), - .sparse => storage.data.sparse.deinit(allocator), - } - } - - // If the storage of this component is sparse, it is turned dense as calling this method - // indicates that the caller expects to set this component for most entities rather than - // sparsely. - pub fn set(storage: *Self, allocator: Allocator, row: u32, component: ?Component) !void { - switch (storage.data) { - .empty => if (component) |c| { - var new_dense = std.ArrayListUnmanaged(?Component){}; - try new_dense.ensureTotalCapacityPrecise(allocator, storage.total_entities.*); - try new_dense.appendNTimes(allocator, null, storage.total_entities.*); - new_dense.items[row] = c; - storage.data = .{ .dense = new_dense }; - } else return, - .dense => |dense| { - if (dense.items.len >= row) try storage.data.dense.appendNTimes(allocator, null, dense.items.len + 1 - row); - dense.items[row] = component; - }, - .sparse => |sparse| { - // Turn sparse storage into dense storage. - defer storage.data.sparse.deinit(allocator); - - var new_dense = std.ArrayListUnmanaged(?Component){}; - try new_dense.ensureTotalCapacityPrecise(allocator, storage.total_entities.*); - var i: u32 = 0; - while (i < storage.total_entities.*) : (i += 1) { - new_dense.appendAssumeCapacity(sparse.get(i)); - } - new_dense.items[row] = component; - storage.data = .{ .dense = new_dense }; - }, - } - } - - // If the storage of this component is dense, it remains dense. - pub fn setSparse(storage: *Self, allocator: Allocator, row: u32, component: ?Component) !void { - switch (storage.data) { - .empty => if (component) |c| { - var new_sparse = std.AutoArrayHashMapUnmanaged(u32, Component){}; - try new_sparse.put(allocator, row, c); - storage.data = .{ .sparse = new_sparse }; - } else return, - .dense => |dense| { - if (dense.items.len >= row) try storage.data.dense.appendNTimes(allocator, null, dense.items.len + 1 - row); - dense.items[row] = component; - }, - .sparse => if (component) |c| try storage.data.sparse.put(allocator, row, c) else { - _ = storage.data.sparse.swapRemove(row); - }, - } - } +// TODO: +// * Iteration +// * Querying +// * Multi threading +// * Multiple entities having one value +// * Sparse storage? - /// Removes the given entity ID. - pub fn remove(storage: *Self, row: u32) bool { - return switch (storage.data) { - .empty => false, - .dense => |dense| if (dense.items.len > row and dense.items[row] != null) { - dense.items[row] = null; - return true; - } else false, - .sparse => storage.data.sparse.swapRemove(row), - }; - } - - /// Gets the component value for the given entity ID. - pub inline fn get(storage: Self, row: u32) ?Component { - return switch (storage.data) { - .empty => null, - .dense => |dense| if (dense.items.len > row) dense.items[row] else null, - .sparse => |sparse| sparse.get(row), - }; - } - }; +test "inclusion" { + _ = Entities; } -/// A type-erased representation of ComponentStorage(T) (where T is unknown). -/// -/// This is useful as it allows us to store all of the typed ComponentStorage as values in a hashmap -/// despite having different types, and allows us to still deinitialize them without knowing the -/// underlying type. -pub const ErasedComponentStorage = struct { - ptr: *anyopaque, - deinit: fn (erased: *anyopaque, allocator: Allocator) void, - remove: fn (erased: *anyopaque, row: u32) bool, - - pub fn cast(ptr: *anyopaque, comptime Component: type) *ComponentStorage(Component) { - var aligned = @alignCast(@alignOf(*ComponentStorage(Component)), ptr); - return @ptrCast(*ComponentStorage(Component), aligned); - } -}; - -/// Represents a single type of entity, e.g. a player, monster, or some other arbitrary entity type. -/// -/// See the `Entities` documentation for more information about entity types and how they enable -/// performance. -/// -/// Database equivalent: a table where rows are entities and columns are components (dense storage) -/// or a secondary table with entity ID -> component value relations (sparse storage.) -pub const EntityTypeStorage = struct { - allocator: Allocator, - - /// This entity type storage identifier. This is used to uniquely identify this entity type - /// within the global set of Entities, and is identical to the EntityID.type_id value. - id: u16, - - /// The number of entities that have been allocated within this entity type. This is identical - /// to the EntityID.id value. - count: u32 = 0, - - /// A string hashmap of component_name -> type-erased *ComponentStorage(Component) - components: std.StringArrayHashMapUnmanaged(ErasedComponentStorage) = .{}, - - /// Free entity slots. When an entity is deleted, it is added to this map and recycled the next - /// time a new entity is requested. - free_slots: std.AutoArrayHashMapUnmanaged(u32, void) = .{}, - - pub fn init(allocator: Allocator, type_id: u16) EntityTypeStorage { - return .{ - .allocator = allocator, - .id = type_id, - }; - } - - pub fn deinit(storage: *EntityTypeStorage) void { - for (storage.components.values()) |erased| { - erased.deinit(erased.ptr, storage.allocator); - } - storage.components.deinit(storage.allocator); - storage.free_slots.deinit(storage.allocator); - } - - /// Creates a new entity of this type. - pub fn new(storage: *EntityTypeStorage) !Entity { - return Entity{ - .id = try storage.newID(), - .entity_type = storage, - }; - } - - // TODO: bulk allocation of entities - - /// Creates a new entity of this type. - pub fn newID(storage: *EntityTypeStorage) !EntityID { - // If there is a previously deleted entity, recycle it's ID. - // TODO: add some "debug" mode which catches use-after-delete of entities (could be super - // confusing if one system deletes it and another creates it and you don't notice!) - const free_slot = storage.free_slots.popOrNull(); - if (free_slot) |recycled| return EntityID{ .type_id = storage.id, .id = recycled.key }; - - // Create a new entity ID and space to store it in each component array. - const new_id = storage.count; - storage.count += 1; - return EntityID{ .type_id = storage.id, .id = new_id }; - } - - /// Deletes the specified entity. See also the `Entity.delete()` helper. - /// - /// This merely marks the entity as deleted, the same ID will be recycled the next time a new - /// entity is created. - pub fn delete(storage: *EntityTypeStorage, id: EntityID) !void { - assert(id.type_id == storage.id); - try storage.free_slots.put(storage.allocator, id.id, .{}); - } - - /// Returns the component storage for the given component. Creates storage for this type of - /// component if it does not exist. - /// - /// Note: This is a low-level API, you probably want to use `Entity.get()` instead. - pub fn get(storage: *EntityTypeStorage, component_name: []const u8, comptime Component: type) !*ComponentStorage(Component) { - var v = try storage.components.getOrPut(storage.allocator, component_name); - if (!v.found_existing) { - var new_ptr = try storage.allocator.create(ComponentStorage(Component)); - new_ptr.* = ComponentStorage(Component){ - .total_entities = &storage.count, - }; - - v.value_ptr.* = ErasedComponentStorage{ - .ptr = new_ptr, - .deinit = (struct { - pub fn deinit(erased: *anyopaque, allocator: Allocator) void { - var ptr = ErasedComponentStorage.cast(erased, Component); - ptr.deinit(allocator); - allocator.destroy(ptr); - } - }).deinit, - .remove = (struct { - pub fn remove(erased: *anyopaque, row: u32) bool { - var ptr = ErasedComponentStorage.cast(erased, Component); - return ptr.remove(row); - } - }).remove, - }; - } - return ErasedComponentStorage.cast(v.value_ptr.ptr, Component); - } - - /// Returns the component storage for the given component, returning null if it does not exist. - /// - /// Note: This is a low-level API, you probably want to use `Entity.get()` instead. - pub fn getIfExists(storage: *EntityTypeStorage, component_name: []const u8, comptime Component: type) ?*ComponentStorage(Component) { - var v = storage.components.get(component_name); - if (v == null) return null; - return ErasedComponentStorage.cast(v.?.ptr, Component); - } - - /// Returns the type-erased component storage for the given component, returning null if it does - /// not exist. - /// - /// Note: This is a low-level API, you probably want to use `Entity.get()` instead. - pub inline fn getErasedIfExists(storage: *EntityTypeStorage, component_name: []const u8) ?ErasedComponentStorage { - return storage.components.get(component_name); - } -}; - -/// A database of entities. For example, all player, monster, etc. entities in a game world. -/// -/// Entities are divided into "entity types", arbitrary named groups of entities that are likely to -/// have the same components. If you are used to archetypes from other ECS systems, know that these -/// are NOT the same as archetypes: you can add or remove components from an entity type at will -/// without getting a new type of entity. You can get an entity type using e.g.: -/// -/// ``` -/// const world = Entities.init(allocator); // all entities in our world -/// const players = world.get("player"); // the player entities -/// -/// const player1 = players.new(); // a new entity of type "player" -/// const player2 = players.new(); // a new entity of type "player" -/// ``` -/// -/// Storage is optimized around the idea that all entities of the same type *generally* have the -/// same type of components. Storing entities by type also enables quickly iterating over all -/// entities with some logical type without any sorting needed (e.g. iterating over all "player" -/// entities but not "monster" entities.) This also reduces the search area for more complex queries -/// and makes filtering entities by e.g. "all entities with a Renderer component" more efficient -/// as we just *know* that if player1 has that component, then player2 almost certainly does too. -/// -/// You can have 65,535 entity types in total. -/// -/// Although storage is *generally* optimized for all entities within a given type having the same -/// components, you may set/remove components on an entity at will via e.g. `player1.set(component)` -/// and `player1.remove(Component)`. `player1` and `player2` may not both have a Renderer component, -/// for example. -/// -/// If you use `player1.set(myRenderer);` then dense storage will be used: we will optimize for -/// *every* entity of type "player" having a Renderer component. In this case, every "player" entity -/// will pay the cost of storing a Renderer component even if they do not have one. -/// -/// If you use `player1.setSparse(myRenderer);` then sparse storage will be used: we will optimize -/// for *most* entities of type "player" not having a Renderer component. In this case, only the -/// "player" entities which have a Renderer component pay a storage cost. If most entities have a -/// Renderer component, this would be the wrong type of storage and less efficient. -/// -/// Database equivalents: -/// * Entities is a database of tables, where each table represents a type of entity. -/// * EntityTypeStorage is a table, whose rows are entities. -/// * EntityID is a 32-bit row ID and a 16-bit table ID, and so globally unique. -/// * ComponentStorage(T) is a column of data in a table for a specific component type -/// * Densely stored as an array of component values. -/// * Sparsely stored as a map of (row ID -> component value). -pub const Entities = struct { - allocator: Allocator, - - /// A mapping of entity type names to their storage. - /// - /// Database equivalent: table name -> tables representing entities. - types: std.StringArrayHashMapUnmanaged(EntityTypeStorage), - - pub fn init(allocator: Allocator) Entities { - return .{ - .allocator = allocator, - .types = std.StringArrayHashMapUnmanaged(EntityTypeStorage){}, - }; - } - - pub fn deinit(entities: *Entities) void { - var iter = entities.types.iterator(); - while (iter.next()) |entry| { - entry.value_ptr.deinit(); - } - entities.types.deinit(entities.allocator); - } - - // TODO: iteration over all entities - // TODO: iteration over all entities with components (U, V, ...) - // TODO: iteration over all entities with type T - // TODO: iteration over all entities with type T and components (U, V, ...) - - // TODO: "indexes" - a few ideas we could express either within a single entity type or across - // all entities: - // - // * Graph relations index: e.g. parent-child entity relations for a DOM / UI / scene graph. - // * Spatial index: "give me all entities within 5 units distance from (x, y, z)" - // * Generic index: "give me all entities where arbitraryFunction(e) returns true" - // - - /// Returns a nice helper for interfacing with the specified entity. - /// - /// This is a mere O(1) array access and so is very cheap. - pub inline fn byID(entities: *const Entities, id: EntityID) Entity { - return .{ - .id = id, - - // TODO: entity type lookup `entities.types.entries.get(id.type_id).value` - // would not give us a pointer to the entry, which is required. I am 99% sure we can do this - // in O(1) time, but MultiArrayList (`entries`) doesn't currently expose a getPtr method. - // - // For now this is actually not O(1), but still very fast. - .entity_type = entities.types.getPtr(entities.typeName(id)).?, - }; - } - - /// Returns the entity type name of the entity given its ID. - /// - /// This is a mere O(1) array access and so is very cheap. - pub inline fn typeName(entities: *const Entities, id: EntityID) []const u8 { - return entities.types.entries.get(id.type_id).key; - } - - // Returns the storage for the given entity type name, creating it if necessary. - // TODO: copy name? - pub fn get(entities: *Entities, entity_type_name: []const u8) !*EntityTypeStorage { - const num_types = entities.types.count(); - var v = try entities.types.getOrPut(entities.allocator, entity_type_name); - if (!v.found_existing) { - v.value_ptr.* = EntityTypeStorage.init(entities.allocator, @intCast(u16, num_types)); - } - return v.value_ptr; - } - - // TODO: ability to remove entity type entirely, deleting all entities in it - // TODO: ability to remove entity types with no entities (garbage collect) -}; - test "example" { const allocator = testing.allocator; //------------------------------------------------------------------------- // Create a world. - var world = Entities.init(allocator); + var world = try World.init(allocator); defer world.deinit(); - //------------------------------------------------------------------------- - // Define component types, any Zig type will do! - // A location component. - const Location = struct { - x: f32 = 0, - y: f32 = 0, - z: f32 = 0, - }; - - // A name component. - const Name = []const u8; - - //------------------------------------------------------------------------- - // Create a player entity type. Every entity with the same type ("player" here) - // will pay to store the same set of components, whether they use them or not. - var players = try world.get("player"); - - // Create first player entity. - var player1 = try players.new(); - try player1.set("name", @as(Name, "jane")); // add Name component - try player1.set("location", Location{}); // add Location component - - // Create second player entity. Note that it pays the cost of storing a Name and Location - // component regardless of whether or not we use it: all entities in the same type ("players") - // pays to store the same set of components. - var player2 = try players.new(); - try testing.expect(player2.get("location", Location) == null); - try testing.expect(player2.get("name", Name) == null); - - //------------------------------------------------------------------------- - // We can add new components at will. Now every player entity will pay to store a Rotation - // component. - const Rotation = struct { degrees: f32 }; - try player2.set("rotation", Rotation{ .degrees = 90 }); - try testing.expect(player1.get("rotation", Rotation) == null); // player1 has no rotation - - //------------------------------------------------------------------------- - // Most of your entities don't have a component, but a few do? Use setSparse instead! - // This is optimized for some entities having the component, but most not having it. - const Weapon = struct { name: []const u8 }; - try player1.setSparse("weapon", Weapon{ .name = "sword" }); - try testing.expectEqualStrings("sword", player1.get("weapon", Weapon).?.name); // lookup is the same regardless of storage type - try testing.expect(player2.get("weapon", Weapon) == null); // player2 has no weapon - - //------------------------------------------------------------------------- - // Remove a component from any entity at will. We'll still pay the cost of storing it for each - // component, it's just set to `null` now. - // TODO: add a way to "cleanup" truly unused components. - _ = player1.remove("location"); // remove Location component - _ = player1.remove("weapon"); // remove Weapon component - - //------------------------------------------------------------------------- - // At runtime we can query the type of any entity. - try testing.expectEqualStrings("player", world.typeName(player1.id)); - - //------------------------------------------------------------------------- - // Entity IDs are all you need to store, they're 48 bits. You can always look up an entity by ID - // in O(1) time (mere array access): - const player1_by_id = world.byID(player1.id); - - //------------------------------------------------------------------------- - // Introspect things. - // Entity types - var entity_types = world.types.keys(); - try testing.expectEqual(@as(usize, 1), entity_types.len); - try testing.expectEqualStrings("player", entity_types[0]); - - // Component types for a given entity type "player" - var component_names = (try world.get("player")).components.keys(); - try testing.expectEqual(@as(usize, 4), component_names.len); - try testing.expectEqualStrings("name", component_names[0]); - try testing.expectEqualStrings("location", component_names[1]); - try testing.expectEqualStrings("rotation", component_names[2]); - try testing.expectEqualStrings("weapon", component_names[3]); - - // TODO: iterating components an entity has not currently supported. - - //------------------------------------------------------------------------- - // Delete an entity whenever you wish. Just be sure not to try and use it later! - try player1_by_id.delete(); -} + 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); -test "entity ID size" { - try testing.expectEqual(6, @sizeOf(EntityID)); + world.tick(); } diff --git a/ecs/src/systems.zig b/ecs/src/systems.zig new file mode 100644 index 0000000000..b2fec27693 --- /dev/null +++ b/ecs/src/systems.zig @@ -0,0 +1,55 @@ +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); + } + } +};