mach/ecs/src/entities.zig

783 lines
33 KiB
Zig

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;
if (@sizeOf(ColumnType) == 0) continue;
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);
if (@sizeOf(ColumnType) == 0) return;
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 (@sizeOf(ColumnType) == 0) return {};
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 = if (@sizeOf(@TypeOf(component)) == 0) 1 else @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);
archetype.remove(old_ptr.row_index);
const swapped_entity_id = archetype.get(entities.allocator, old_ptr.row_index, "id", EntityID).?;
// TODO: try is wrong here and below?
// if we removed the last entry from archetype, then swapped_entity_id == entity
// so the second entities.put will clobber this one
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;
}
}
}
archetype.remove(old_ptr.row_index);
const swapped_entity_id = archetype.get(entities.allocator, old_ptr.row_index, "id", EntityID).?;
// TODO: try is wrong here and below?
// if we removed the last entry from archetype, then swapped_entity_id == entity
// so the second entities.put will clobber this one
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);
}