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; const is_debug = builtin.mode == .Debug; /// An entity ID uniquely identifies an entity globally within an Entities set. pub const EntityID = u64; const TypeId = enum(usize) { _ }; // typeId implementation by Felix "xq" Queißner fn typeId(comptime T: type) TypeId { _ = T; return @intToEnum(TypeId, @ptrToInt(&struct { var x: u8 = 0; }.x)); } const Column = struct { name: []const u8, typeId: TypeId, size: u32, alignment: u16, offset: usize, }; fn by_alignment_name(context: void, lhs: Column, rhs: Column) bool { _ = context; if (lhs.alignment < rhs.alignment) return true; return std.mem.lessThan(u8, lhs.name, rhs.name); } /// 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, /// The length of the table (used number of rows.) len: u32, /// The capacity of the table (allocated number of rows.) capacity: u32, /// Describes the columns stored in the `block` of memory, sorted by the smallest alignment /// value. columns: []Column, /// The block of memory where all entities of this archetype are actually stored. This memory is /// laid out as contiguous column values (i.e. the same way MultiArrayList works, SoA style) /// so `[col1_val1, col1_val2, col2_val1, col2_val2, ...]`. The number of rows is always /// identical (the `ArchetypeStorage.capacity`), and an "id" column is always present (the /// entity IDs stored in the table.) The value names, size, and alignments are described by the /// `ArchetypeStorage.columns` slice. /// /// When necessary, padding is added between the column value *arrays* in order to achieve /// alignment. block: []u8, /// 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; for (storage.columns) |column| { storage.hash ^= std.hash_map.hashString(column.name); } } pub fn deinit(storage: *ArchetypeStorage, gpa: Allocator) void { gpa.free(storage.columns); } fn debugValidateRow(storage: *ArchetypeStorage, gpa: Allocator, row: anytype) void { inline for (std.meta.fields(@TypeOf(row))) |field, index| { const column = storage.columns[index]; if (typeId(field.field_type) != column.typeId) { const msg = std.mem.concat(gpa, u8, &.{ "unexpected type: ", @typeName(field.field_type), " expected: ", column.name, }) catch |err| @panic(@errorName(err)); @panic(msg); } } } /// appends a new row to this table, with all undefined values. pub fn appendUndefined(storage: *ArchetypeStorage, gpa: Allocator) !u32 { try storage.ensureUnusedCapacity(gpa, 1); assert(storage.len < storage.capacity); const row_index = storage.len; storage.len += 1; return row_index; } pub fn append(storage: *ArchetypeStorage, gpa: Allocator, row: anytype) !u32 { if (is_debug) storage.debugValidateRow(gpa, row); try storage.ensureUnusedCapacity(gpa, 1); assert(storage.len < storage.capacity); storage.len += 1; storage.setRow(gpa, storage.len - 1, row); return storage.len; } pub fn undoAppend(storage: *ArchetypeStorage) void { storage.len -= 1; } /// Ensures there is enough unused capacity to store `num_rows`. pub fn ensureUnusedCapacity(storage: *ArchetypeStorage, gpa: Allocator, num_rows: usize) !void { return storage.ensureTotalCapacity(gpa, storage.len + num_rows); } /// Ensures the total capacity is enough to store `new_capacity` rows total. pub fn ensureTotalCapacity(storage: *ArchetypeStorage, gpa: Allocator, new_capacity: usize) !void { var better_capacity = storage.capacity; if (better_capacity >= new_capacity) return; while (true) { better_capacity += better_capacity / 2 + 8; if (better_capacity >= new_capacity) break; } return storage.setCapacity(gpa, better_capacity); } /// Sets the capacity to exactly `new_capacity` rows total /// /// Asserts `new_capacity >= storage.len`, if you want to shrink capacity then change the len /// yourself first. pub fn setCapacity(storage: *ArchetypeStorage, gpa: Allocator, new_capacity: usize) !void { assert(storage.capacity >= storage.len); // TODO: ensure columns are sorted by alignment var new_capacity_bytes: usize = 0; for (storage.columns) |*column| { const max_padding = column.alignment - 1; new_capacity_bytes += max_padding; new_capacity_bytes += new_capacity * column.size; } const new_block = try gpa.alloc(u8, new_capacity_bytes); var offset: usize = 0; for (storage.columns) |*column| { const addr = @ptrToInt(&new_block[offset]); const aligned_addr = std.mem.alignForward(addr, column.alignment); const padding = aligned_addr - addr; offset += padding; if (storage.capacity > 0) { const slice = storage.block[column.offset .. column.offset + storage.capacity * column.size]; mem.copy(u8, new_block[offset..], slice); } column.offset = offset; offset += new_capacity * column.size; } storage.block = new_block; storage.capacity = @intCast(u32, new_capacity); } /// Sets the entire row's values in the table. pub fn setRow(storage: *ArchetypeStorage, gpa: Allocator, row_index: u32, row: anytype) void { if (is_debug) storage.debugValidateRow(gpa, row); const fields = std.meta.fields(@TypeOf(row)); inline for (fields) |field, index| { const ColumnType = field.field_type; const column = storage.columns[index]; const columnValues = @ptrCast([*]ColumnType, @alignCast(@alignOf(ColumnType), &storage.block[column.offset])); columnValues[row_index] = @field(row, field.name); } } /// Sets the value of the named components (columns) for the given row in the table. pub fn set(storage: *ArchetypeStorage, gpa: Allocator, row_index: u32, name: []const u8, component: anytype) void { const ColumnType = @TypeOf(component); for (storage.columns) |column| { if (!std.mem.eql(u8, column.name, name)) continue; if (is_debug) { if (typeId(ColumnType) != column.typeId) { const msg = std.mem.concat(gpa, u8, &.{ "unexpected type: ", @typeName(ColumnType), " expected: ", column.name, }) catch |err| @panic(@errorName(err)); @panic(msg); } } const columnValues = @ptrCast([*]ColumnType, @alignCast(@alignOf(ColumnType), &storage.block[column.offset])); columnValues[row_index] = component; return; } @panic("no such component"); } pub fn get(storage: *ArchetypeStorage, gpa: Allocator, row_index: u32, name: []const u8, comptime ColumnType: type) ?ColumnType { for (storage.columns) |column| { if (!std.mem.eql(u8, column.name, name)) continue; if (is_debug) { if (typeId(ColumnType) != column.typeId) { const msg = std.mem.concat(gpa, u8, &.{ "unexpected type: ", @typeName(ColumnType), " expected: ", column.name, }) catch |err| @panic(@errorName(err)); @panic(msg); } } const columnValues = @ptrCast([*]ColumnType, @alignCast(@alignOf(ColumnType), &storage.block[column.offset])); return columnValues[row_index]; } return null; } pub fn getRaw(storage: *ArchetypeStorage, row_index: u32, name: []const u8) []u8 { for (storage.columns) |column| { if (!std.mem.eql(u8, column.name, name)) continue; const start = column.offset + (column.size * row_index); return storage.block[start .. start + (column.size)]; } @panic("no such component"); } pub fn setRaw(storage: *ArchetypeStorage, row_index: u32, column: Column, component: []u8) !void { if (is_debug) { const ok = blk: { for (storage.columns) |col| { if (std.mem.eql(u8, col.name, column.name)) { break :blk true; } } break :blk false; }; if (!ok) @panic("setRaw with non-matching column"); } mem.copy(u8, storage.block[column.offset + (row_index * column.size) ..], component); } /// Swap-removes the specified row with the last row in the table. pub fn remove(storage: *ArchetypeStorage, row_index: u32) void { if (storage.len > 1) { for (storage.columns) |column| { const dstStart = column.offset + (column.size * row_index); const dst = storage.block[dstStart .. dstStart + (column.size)]; const srcStart = column.offset + (column.size * (storage.len - 1)); const src = storage.block[srcStart .. srcStart + (column.size)]; std.mem.copy(u8, dst, src); } } storage.len -= 1; } /// 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.hasComponent(component_name)) return false; } return true; } /// Tells if this archetype has a component with the specified name. pub fn hasComponent(storage: *ArchetypeStorage, component: []const u8) bool { for (storage.columns) |column| { if (std.mem.eql(u8, column.name, component)) return true; } return false; } }; 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: u32 = 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.len) { 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.get(iter.entities.allocator, iter.row_index, "id", EntityID).?; 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 }; const columns = try allocator.alloc(Column, 1); columns[0] = .{ .name = "id", .typeId = typeId(EntityID), .size = @sizeOf(EntityID), .alignment = @alignOf(EntityID), .offset = undefined, }; try entities.archetypes.put(allocator, void_archetype_hash, ArchetypeStorage{ .allocator = allocator, .len = 0, .capacity = 0, .columns = columns, .block = undefined, .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| { entities.allocator.free(entry.value_ptr.block); entry.value_ptr.deinit(entities.allocator); } 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.append(entities.allocator, .{ .id = 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.undoAppend(); 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. if (archetype.len > 1) { const last_row_entity_id = archetype.get(entities.allocator, archetype.len - 1, "id", EntityID).?; 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. 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, comptime 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.hasComponent(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) { // getOrPut allocated, so the archetype we retrieved earlier may no longer be a valid // pointer. Refresh it now: archetype = entities.archetypeByID(entity); const columns = entities.allocator.alloc(Column, archetype.columns.len + 1) catch |err| { assert(entities.archetypes.swapRemove(new_hash)); return err; }; mem.copy(Column, columns, archetype.columns); columns[columns.len - 1] = .{ .name = name, .typeId = typeId(@TypeOf(component)), .size = @sizeOf(@TypeOf(component)), .alignment = @alignOf(@TypeOf(component)), .offset = undefined, }; std.sort.sort(Column, columns, {}, by_alignment_name); archetype_entry.value_ptr.* = ArchetypeStorage{ .allocator = entities.allocator, .len = 0, .capacity = 0, .columns = columns, .block = undefined, .hash = undefined, }; const new_archetype = archetype_entry.value_ptr; 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).?; current_archetype_storage.set(entities.allocator, 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.appendUndefined(entities.allocator); const old_ptr = entities.entities.get(entity).?; // Update the storage/columns for all of the existing components on the entity. current_archetype_storage.set(entities.allocator, new_row, "id", entity); for (archetype.columns) |column| { if (std.mem.eql(u8, column.name, "id")) continue; for (current_archetype_storage.columns) |corresponding| { if (std.mem.eql(u8, column.name, corresponding.name)) { const old_value_raw = archetype.getRaw(old_ptr.row_index, column.name); current_archetype_storage.setRaw(new_row, corresponding, old_value_raw) catch |err| { current_archetype_storage.undoAppend(); return err; }; break; } } } // Update the storage/column for the new component. current_archetype_storage.set(entities.allocator, new_row, name, component); var swapped_entity_id = archetype.get(entities.allocator, old_ptr.row_index, "id", EntityID).?; archetype.remove(old_ptr.row_index); // TODO: try is wrong here and below? 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); const ptr = entities.entities.get(entity).?; return archetype.get(entities.allocator, ptr.row_index, name, Component); } /// 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.hasComponent(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; for (archetype.columns) |column| { if (!std.mem.eql(u8, column.name, name)) new_hash ^= std.hash_map.hashString(column.name); } assert(new_hash != old_hash); // Find the archetype storage this entity will move to. Note that although an entity with // (A, B, C) components implies archetypes ((A), (A, B), (A, B, C)) exist there is no // guarantee that archetype (A, C) exists - and so removing a component sometimes does // require creating a new archetype table! var archetype_entry = try entities.archetypes.getOrPut(entities.allocator, new_hash); if (!archetype_entry.found_existing) { // getOrPut allocated, so the archetype we retrieved earlier may no longer be a valid // pointer. Refresh it now: archetype = entities.archetypeByID(entity); const columns = entities.allocator.alloc(Column, archetype.columns.len - 1) catch |err| { assert(entities.archetypes.swapRemove(new_hash)); return err; }; var i: usize = 0; for (archetype.columns) |column| { if (std.mem.eql(u8, column.name, name)) continue; columns[i] = column; i += 1; } archetype_entry.value_ptr.* = ArchetypeStorage{ .allocator = entities.allocator, .len = 0, .capacity = 0, .columns = columns, .block = undefined, .hash = undefined, }; const new_archetype = archetype_entry.value_ptr; new_archetype.calculateHash(); } 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.appendUndefined(entities.allocator); const old_ptr = entities.entities.get(entity).?; // Update the storage/columns for all of the existing components on the entity that exist in // the new archetype table (i.e. excluding the component to remove.) current_archetype_storage.set(entities.allocator, new_row, "id", entity); for (current_archetype_storage.columns) |column| { if (std.mem.eql(u8, column.name, "id")) continue; for (archetype.columns) |corresponding| { if (std.mem.eql(u8, column.name, corresponding.name)) { const old_value_raw = archetype.getRaw(old_ptr.row_index, column.name); current_archetype_storage.setRaw(new_row, column, old_value_raw) catch |err| { current_archetype_storage.undoAppend(); return err; }; break; } } } var swapped_entity_id = archetype.get(entities.allocator, old_ptr.row_index, "id", EntityID).?; archetype.remove(old_ptr.row_index); // TODO: try is wrong here and below? 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, }); } // 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" // // 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, void_archetype_hash), archetypes[0]); try testing.expectEqual(@as(u64, 6893717443977936573), archetypes[1]); try testing.expectEqual(@as(u64, 6672640730301731073), archetypes[2]); try testing.expectEqual(@as(u64, 14420739110802803032), archetypes[3]); try testing.expectEqual(@as(u64, 18216325908396511299), archetypes[4]); try testing.expectEqual(@as(u64, 4457032469566706731), archetypes[5]); // Number of (living) entities stored in an archetype table. try testing.expectEqual(@as(usize, 0), world.archetypes.get(archetypes[0]).?.len); try testing.expectEqual(@as(usize, 0), world.archetypes.get(archetypes[1]).?.len); try testing.expectEqual(@as(usize, 0), world.archetypes.get(archetypes[2]).?.len); try testing.expectEqual(@as(usize, 1), world.archetypes.get(archetypes[3]).?.len); try testing.expectEqual(@as(usize, 0), world.archetypes.get(archetypes[4]).?.len); try testing.expectEqual(@as(usize, 1), world.archetypes.get(archetypes[5]).?.len); // Components for a given archetype. var columns = world.archetypes.get(archetypes[2]).?.columns; try testing.expectEqual(@as(usize, 3), columns.len); try testing.expectEqualStrings("location", columns[0].name); try testing.expectEqualStrings("id", columns[1].name); try testing.expectEqualStrings("name", columns[2].name); // Archetype resolved via entity ID var player2_archetype = world.archetypeByID(player2); try testing.expectEqual(@as(u64, 722178222806262412), player2_archetype.hash); // 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); }