src/ecs: move mach-ecs@83a3ed801008a976dd79e10068157b02c3b76a36 package to here
Helps hexops/mach#1165 Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
parent
d4cd79440e
commit
5f70579360
8 changed files with 1891 additions and 0 deletions
241
src/ecs/Archetype.zig
Normal file
241
src/ecs/Archetype.zig
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
//! Represents a single archetype. i.e., entities which have a specific set of components. When a
|
||||
//! component is added or removed from an entity, it's archetype changes because the archetype is
|
||||
//! the set of components an entity has.
|
||||
//!
|
||||
//! Database equivalent: a table where rows are entities and columns are components (dense storage).
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const testing = std.testing;
|
||||
const assert = std.debug.assert;
|
||||
const builtin = @import("builtin");
|
||||
const StringTable = @import("StringTable.zig");
|
||||
const comp = @import("comptime.zig");
|
||||
|
||||
const Archetype = @This();
|
||||
|
||||
/// Describes a single column of the archetype (table); i.e. a single type of component
|
||||
pub const Column = struct {
|
||||
/// The unique name of the component this column stores.
|
||||
name: StringTable.Index,
|
||||
|
||||
/// A unique identifier for the programming-language type this column stores. In the case of Zig
|
||||
/// this is a comptime type identifier. For other languages, it may be something else or simply
|
||||
/// zero if unused.
|
||||
///
|
||||
/// This value need only uniquely identify the column type for the duration of a single build of
|
||||
/// the program.
|
||||
type_id: u32,
|
||||
|
||||
/// The size of the component this column stores.
|
||||
size: u32,
|
||||
|
||||
/// The alignment of the component type this column stores.
|
||||
alignment: u16,
|
||||
|
||||
/// The actual memory where the values are stored. The length/capacity is Archetype.len and
|
||||
/// Archetype.capacity, as all columns in an Archetype have identical lengths/capacities.
|
||||
values: []u8,
|
||||
};
|
||||
|
||||
/// The length of the table (in-use number of rows)
|
||||
len: u32,
|
||||
|
||||
/// The capacity of the table (total allocated number of rows)
|
||||
capacity: u32,
|
||||
|
||||
/// Describes the columns in this table. Each column stores all rows for that column.
|
||||
columns: []Column,
|
||||
|
||||
/// A reference to the string table that can be used to identify Column.name's
|
||||
component_names: *StringTable,
|
||||
|
||||
/// A hash composed of all Column.name's, effectively acting as the unique name of this table.
|
||||
hash: u64,
|
||||
|
||||
/// An index to Entities.archetypes, used in the event of a *bucket* hash collision (not a collision
|
||||
/// of the .hash field) - see Entities.archetypeOrPut for details.
|
||||
next: ?u32 = null,
|
||||
|
||||
pub fn deinit(storage: *Archetype, gpa: Allocator) void {
|
||||
if (storage.capacity > 0) {
|
||||
for (storage.columns) |column| gpa.free(column.values);
|
||||
}
|
||||
gpa.free(storage.columns);
|
||||
}
|
||||
|
||||
/// appends a new row to this table, with all undefined values.
|
||||
pub fn appendUndefined(storage: *Archetype, gpa: Allocator) !u32 {
|
||||
try storage.ensureUnusedCapacity(gpa, 1);
|
||||
assert(storage.len < storage.capacity);
|
||||
const row_index = storage.len;
|
||||
storage.len += 1;
|
||||
return row_index;
|
||||
}
|
||||
|
||||
// TODO: comptime: missing a runtime variant of this function
|
||||
pub fn append(storage: *Archetype, gpa: Allocator, row: anytype) !u32 {
|
||||
comp.debugAssertRowType(storage, row);
|
||||
|
||||
try storage.ensureUnusedCapacity(gpa, 1);
|
||||
assert(storage.len < storage.capacity);
|
||||
storage.len += 1;
|
||||
|
||||
storage.setRow(storage.len - 1, row);
|
||||
return storage.len;
|
||||
}
|
||||
|
||||
pub fn undoAppend(storage: *Archetype) void {
|
||||
storage.len -= 1;
|
||||
}
|
||||
|
||||
/// Ensures there is enough unused capacity to store `num_rows`.
|
||||
pub fn ensureUnusedCapacity(storage: *Archetype, 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: *Archetype, 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: *Archetype, gpa: Allocator, new_capacity: usize) !void {
|
||||
assert(new_capacity >= storage.len);
|
||||
|
||||
// TODO: ensure columns are sorted by type_id
|
||||
for (storage.columns) |*column| {
|
||||
const old_values = column.values;
|
||||
const new_values = try gpa.alloc(u8, new_capacity * column.size);
|
||||
if (storage.capacity > 0) {
|
||||
@memcpy(new_values[0..old_values.len], old_values);
|
||||
gpa.free(old_values);
|
||||
}
|
||||
column.values = new_values;
|
||||
}
|
||||
storage.capacity = @as(u32, @intCast(new_capacity));
|
||||
}
|
||||
|
||||
// TODO: comptime: missing a runtime variant of this function
|
||||
/// Sets the entire row's values in the table.
|
||||
pub fn setRow(storage: *Archetype, row_index: u32, row: anytype) void {
|
||||
comp.debugAssertRowType(storage, row);
|
||||
|
||||
const fields = std.meta.fields(@TypeOf(row));
|
||||
inline for (fields, 0..) |field, index| {
|
||||
const ColumnType = field.type;
|
||||
if (@sizeOf(ColumnType) == 0) continue;
|
||||
|
||||
const column = storage.columns[index];
|
||||
const column_values = @as([*]ColumnType, @ptrCast(@alignCast(column.values.ptr)));
|
||||
column_values[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: *Archetype, row_index: u32, name: StringTable.Index, component: anytype) void {
|
||||
const ColumnType = @TypeOf(component);
|
||||
if (@sizeOf(ColumnType) == 0) return;
|
||||
if (comp.is_debug) comp.debugAssertColumnType(storage, storage.columnByName(name).?, @TypeOf(component));
|
||||
storage.setDynamic(
|
||||
row_index,
|
||||
name,
|
||||
std.mem.asBytes(&component),
|
||||
@alignOf(@TypeOf(component)),
|
||||
comp.typeId(@TypeOf(component)),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setDynamic(storage: *Archetype, row_index: u32, name: StringTable.Index, component: []const u8, alignment: u16, type_id: u32) void {
|
||||
if (comp.is_debug) {
|
||||
// TODO: improve error messages
|
||||
assert(storage.len != 0 and storage.len >= row_index);
|
||||
assert(storage.columnByName(name).?.size == component.len);
|
||||
assert(storage.columnByName(name).?.alignment == alignment);
|
||||
assert(storage.columnByName(name).?.type_id == type_id);
|
||||
}
|
||||
|
||||
const values = storage.getColumnValuesRaw(name) orelse @panic("no such component");
|
||||
const start = component.len * row_index;
|
||||
@memcpy(values[start .. start + component.len], component);
|
||||
}
|
||||
|
||||
pub fn get(storage: *Archetype, row_index: u32, name: StringTable.Index, comptime ColumnType: type) ?ColumnType {
|
||||
if (@sizeOf(ColumnType) == 0) return {};
|
||||
if (comp.is_debug) comp.debugAssertColumnType(storage, storage.columnByName(name) orelse return null, ColumnType);
|
||||
|
||||
const bytes = storage.getDynamic(row_index, name, @sizeOf(ColumnType), @alignOf(ColumnType), comp.typeId(ColumnType)) orelse return null;
|
||||
return @as(*ColumnType, @alignCast(@ptrCast(bytes.ptr))).*;
|
||||
}
|
||||
|
||||
pub fn getDynamic(storage: *Archetype, row_index: u32, name: StringTable.Index, size: u32, alignment: u16, type_id: u32) ?[]u8 {
|
||||
const values = storage.getColumnValuesRaw(name) orelse return null;
|
||||
if (comp.is_debug) {
|
||||
// TODO: improve error messages
|
||||
assert(storage.columnByName(name).?.size == size);
|
||||
assert(storage.columnByName(name).?.alignment == alignment);
|
||||
assert(storage.columnByName(name).?.type_id == type_id);
|
||||
}
|
||||
|
||||
const start = size * row_index;
|
||||
const end = start + size;
|
||||
return values[start..end];
|
||||
}
|
||||
|
||||
/// Swap-removes the specified row with the last row in the table.
|
||||
pub fn remove(storage: *Archetype, row_index: u32) void {
|
||||
if (storage.len > 1) {
|
||||
for (storage.columns) |column| {
|
||||
const dstStart = column.size * row_index;
|
||||
const dst = column.values[dstStart .. dstStart + column.size];
|
||||
const srcStart = column.size * (storage.len - 1);
|
||||
const src = column.values[srcStart .. srcStart + column.size];
|
||||
@memcpy(dst, src);
|
||||
}
|
||||
}
|
||||
storage.len -= 1;
|
||||
}
|
||||
|
||||
/// Tells if this archetype has every one of the given components.
|
||||
pub fn hasComponents(storage: *Archetype, names: []const u32) bool {
|
||||
for (names) |name| {
|
||||
if (!storage.hasComponent(name)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Tells if this archetype has a component with the specified name.
|
||||
pub fn hasComponent(storage: *Archetype, name: StringTable.Index) bool {
|
||||
return storage.columnByName(name) != null;
|
||||
}
|
||||
|
||||
pub fn getColumnValues(storage: *Archetype, name: StringTable.Index, comptime ColumnType: type) ?[]ColumnType {
|
||||
const values = storage.getColumnValuesRaw(name) orelse return null;
|
||||
if (comp.is_debug) comp.debugAssertColumnType(storage, storage.columnByName(name).?, ColumnType);
|
||||
var ptr = @as([*]ColumnType, @ptrCast(@alignCast(values.ptr)));
|
||||
const column_values = ptr[0..storage.capacity];
|
||||
return column_values;
|
||||
}
|
||||
|
||||
pub fn getColumnValuesRaw(storage: *Archetype, name: StringTable.Index) ?[]u8 {
|
||||
const column = storage.columnByName(name) orelse return null;
|
||||
return column.values;
|
||||
}
|
||||
|
||||
pub inline fn columnByName(storage: *Archetype, name: StringTable.Index) ?*Column {
|
||||
for (storage.columns) |*column| {
|
||||
if (column.name == name) return column;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
105
src/ecs/StringTable.zig
Normal file
105
src/ecs/StringTable.zig
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
//! Stores null-terminated strings and maps them to unique 32-bit indices.
|
||||
//!
|
||||
//! Lookups are omnidirectional: both (string -> index) and (index -> string) are supported
|
||||
//! operations.
|
||||
//!
|
||||
//! The implementation is based on:
|
||||
//! https://zig.news/andrewrk/how-to-use-hash-map-contexts-to-save-memory-when-doing-a-string-table-3l33
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const StringTable = @This();
|
||||
|
||||
string_bytes: std.ArrayListUnmanaged(u8) = .{},
|
||||
|
||||
/// Key is string_bytes index.
|
||||
string_table: std.HashMapUnmanaged(u32, void, IndexContext, std.hash_map.default_max_load_percentage) = .{},
|
||||
|
||||
pub const Index = u32;
|
||||
|
||||
/// Returns the index of a string key, if it exists.
|
||||
pub fn index(table: *StringTable, key: []const u8) ?Index {
|
||||
const slice_context: SliceAdapter = .{ .string_bytes = &table.string_bytes };
|
||||
const found_entry = table.string_table.getEntryAdapted(key, slice_context);
|
||||
if (found_entry) |e| return e.key_ptr.*;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the index of a string key, inserting if not exists.
|
||||
pub fn indexOrPut(table: *StringTable, allocator: std.mem.Allocator, key: []const u8) !Index {
|
||||
const slice_context: SliceAdapter = .{ .string_bytes = &table.string_bytes };
|
||||
const index_context: IndexContext = .{ .string_bytes = &table.string_bytes };
|
||||
const entry = try table.string_table.getOrPutContextAdapted(allocator, key, slice_context, index_context);
|
||||
if (!entry.found_existing) {
|
||||
entry.key_ptr.* = @intCast(table.string_bytes.items.len);
|
||||
try table.string_bytes.appendSlice(allocator, key);
|
||||
try table.string_bytes.append(allocator, '\x00');
|
||||
}
|
||||
return entry.key_ptr.*;
|
||||
}
|
||||
|
||||
/// Returns a null-terminated string given the index
|
||||
pub fn string(table: *StringTable, idx: Index) [:0]const u8 {
|
||||
return std.mem.span(@as([*:0]const u8, @ptrCast(table.string_bytes.items.ptr)) + idx);
|
||||
}
|
||||
|
||||
pub fn deinit(table: *StringTable, allocator: std.mem.Allocator) void {
|
||||
table.string_bytes.deinit(allocator);
|
||||
table.string_table.deinit(allocator);
|
||||
}
|
||||
|
||||
const IndexContext = struct {
|
||||
string_bytes: *std.ArrayListUnmanaged(u8),
|
||||
|
||||
pub fn eql(ctx: IndexContext, a: u32, b: u32) bool {
|
||||
_ = ctx;
|
||||
return a == b;
|
||||
}
|
||||
|
||||
pub fn hash(ctx: IndexContext, x: u32) u64 {
|
||||
const x_slice = std.mem.span(@as([*:0]const u8, @ptrCast(ctx.string_bytes.items.ptr)) + x);
|
||||
return std.hash_map.hashString(x_slice);
|
||||
}
|
||||
};
|
||||
|
||||
const SliceAdapter = struct {
|
||||
string_bytes: *std.ArrayListUnmanaged(u8),
|
||||
|
||||
pub fn eql(adapter: SliceAdapter, a_slice: []const u8, b: u32) bool {
|
||||
const b_slice = std.mem.span(@as([*:0]const u8, @ptrCast(adapter.string_bytes.items.ptr)) + b);
|
||||
return std.mem.eql(u8, a_slice, b_slice);
|
||||
}
|
||||
|
||||
pub fn hash(adapter: SliceAdapter, adapted_key: []const u8) u64 {
|
||||
_ = adapter;
|
||||
return std.hash_map.hashString(adapted_key);
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var table: StringTable = .{};
|
||||
defer table.deinit(gpa);
|
||||
|
||||
const index_context: IndexContext = .{ .string_bytes = &table.string_bytes };
|
||||
_ = index_context;
|
||||
|
||||
// "hello" -> index 0
|
||||
const hello_index = try table.indexOrPut(gpa, "hello");
|
||||
try std.testing.expectEqual(@as(Index, 0), hello_index);
|
||||
|
||||
try std.testing.expectEqual(@as(Index, 6), try table.indexOrPut(gpa, "world"));
|
||||
try std.testing.expectEqual(@as(Index, 12), try table.indexOrPut(gpa, "foo"));
|
||||
try std.testing.expectEqual(@as(Index, 16), try table.indexOrPut(gpa, "bar"));
|
||||
try std.testing.expectEqual(@as(Index, 20), try table.indexOrPut(gpa, "baz"));
|
||||
|
||||
// index 0 -> "hello"
|
||||
try std.testing.expectEqualStrings("hello", table.string(hello_index));
|
||||
|
||||
// Lookup "hello" -> index 0
|
||||
try std.testing.expectEqual(hello_index, table.index("hello").?);
|
||||
|
||||
// Lookup "foobar" -> null
|
||||
try std.testing.expectEqual(@as(?Index, null), table.index("foobar"));
|
||||
}
|
||||
64
src/ecs/comptime.zig
Normal file
64
src/ecs/comptime.zig
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const Archetype = @import("Archetype.zig");
|
||||
const StringTable = @import("StringTable.zig");
|
||||
|
||||
pub const is_debug = builtin.mode == .Debug;
|
||||
|
||||
/// Returns a unique comptime usize integer representing the type T. Value will change across
|
||||
/// different compilations.
|
||||
pub fn typeId(comptime T: type) u32 {
|
||||
_ = T;
|
||||
return @truncate(@intFromPtr(&struct {
|
||||
var x: u8 = 0;
|
||||
}.x));
|
||||
}
|
||||
|
||||
/// Asserts that T matches the type of the column.
|
||||
pub inline fn debugAssertColumnType(storage: *Archetype, column: *Archetype.Column, comptime T: type) void {
|
||||
if (is_debug) {
|
||||
if (typeId(T) != column.type_id) std.debug.panic("unexpected type: {s} expected: {s}", .{
|
||||
@typeName(T),
|
||||
storage.component_names.string(column.name),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts that a tuple `row` to be e.g. appended to an archetype has values that actually match
|
||||
/// all of the columns of the archetype table.
|
||||
pub inline fn debugAssertRowType(storage: *Archetype, row: anytype) void {
|
||||
if (is_debug) {
|
||||
inline for (std.meta.fields(@TypeOf(row)), 0..) |field, index| {
|
||||
debugAssertColumnType(storage, &storage.columns[index], field.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: comptime refactor
|
||||
pub fn ArchetypeSlicer(comptime all_components: anytype) type {
|
||||
return struct {
|
||||
archetype: *Archetype,
|
||||
|
||||
pub fn slice(
|
||||
slicer: @This(),
|
||||
comptime namespace_name: std.meta.FieldEnum(@TypeOf(all_components)),
|
||||
comptime component_name: std.meta.DeclEnum(@field(all_components, @tagName(namespace_name))),
|
||||
) []@field(
|
||||
@field(all_components, @tagName(namespace_name)),
|
||||
@tagName(component_name),
|
||||
) {
|
||||
const Type = @field(
|
||||
@field(all_components, @tagName(namespace_name)),
|
||||
@tagName(component_name),
|
||||
);
|
||||
if (namespace_name == .entity and component_name == .id) {
|
||||
const name_id = slicer.archetype.component_names.index("id").?;
|
||||
return slicer.archetype.getColumnValues(name_id, Type).?[0..slicer.archetype.len];
|
||||
}
|
||||
const name = @tagName(namespace_name) ++ "." ++ @tagName(component_name);
|
||||
const name_id = slicer.archetype.component_names.index(name).?;
|
||||
return slicer.archetype.getColumnValues(name_id, Type).?[0..slicer.archetype.len];
|
||||
}
|
||||
};
|
||||
}
|
||||
880
src/ecs/entities.zig
Normal file
880
src/ecs/entities.zig
Normal file
|
|
@ -0,0 +1,880 @@
|
|||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const testing = std.testing;
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const query_mod = @import("query.zig");
|
||||
const Archetype = @import("Archetype.zig");
|
||||
const StringTable = @import("StringTable.zig");
|
||||
const comp = @import("comptime.zig");
|
||||
|
||||
/// An entity ID uniquely identifies an entity globally within an Entities set.
|
||||
pub const EntityID = u64;
|
||||
|
||||
fn byTypeId(context: void, lhs: Archetype.Column, rhs: Archetype.Column) bool {
|
||||
_ = context;
|
||||
return lhs.type_id < rhs.type_id;
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// * Archetype 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 fn Entities(comptime all_components: anytype) type {
|
||||
// TODO: validate all_components is a namespaced component set in the form we expect
|
||||
return 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) = .{},
|
||||
|
||||
// All archetypes are stored in a bucket. The number of buckets is configurable, and which
|
||||
// bucket an archetype will be stored in is based on the hash of all its columns / component
|
||||
// names.
|
||||
seed: u64 = 0xdeadbeef,
|
||||
buckets: []?u32, // indices into archetypes
|
||||
archetypes: std.ArrayListUnmanaged(Archetype) = .{},
|
||||
|
||||
/// Maps component names <-> unique IDs
|
||||
component_names: *StringTable,
|
||||
id_name: StringTable.Index = 0,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// 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.items[ptr.archetype_index].rows[ptr.row_index]
|
||||
/// ```
|
||||
///
|
||||
pub const Pointer = struct {
|
||||
archetype_index: u32,
|
||||
row_index: u32,
|
||||
};
|
||||
|
||||
/// A complex query for entities matching a given criteria
|
||||
pub const Query = query_mod.Query(all_components);
|
||||
pub const QueryTag = query_mod.QueryTag;
|
||||
|
||||
pub fn init(allocator: Allocator) !Self {
|
||||
const component_names = try allocator.create(StringTable);
|
||||
errdefer allocator.destroy(component_names);
|
||||
component_names.* = .{};
|
||||
|
||||
const buckets = try allocator.alloc(?u32, 1024); // TODO: configurable size
|
||||
errdefer allocator.free(buckets);
|
||||
for (buckets) |*b| b.* = null;
|
||||
|
||||
var entities = Self{
|
||||
.allocator = allocator,
|
||||
.component_names = component_names,
|
||||
.buckets = buckets,
|
||||
};
|
||||
entities.id_name = try entities.component_names.indexOrPut(allocator, "id");
|
||||
|
||||
const columns = try allocator.alloc(Archetype.Column, 1);
|
||||
columns[0] = .{
|
||||
.name = entities.id_name,
|
||||
.type_id = comp.typeId(EntityID),
|
||||
.size = @sizeOf(EntityID),
|
||||
.alignment = @alignOf(EntityID),
|
||||
.values = undefined,
|
||||
};
|
||||
|
||||
const archetype_entry = try entities.archetypeOrPut(columns);
|
||||
archetype_entry.ptr.* = .{
|
||||
.len = 0,
|
||||
.capacity = 0,
|
||||
.columns = columns,
|
||||
.component_names = entities.component_names,
|
||||
.hash = archetype_entry.hash,
|
||||
};
|
||||
return entities;
|
||||
}
|
||||
|
||||
pub fn deinit(entities: *Self) void {
|
||||
entities.entities.deinit(entities.allocator);
|
||||
entities.component_names.deinit(entities.allocator);
|
||||
entities.allocator.destroy(entities.component_names);
|
||||
entities.allocator.free(entities.buckets);
|
||||
for (entities.archetypes.items) |*archetype| archetype.deinit(entities.allocator);
|
||||
entities.archetypes.deinit(entities.allocator);
|
||||
}
|
||||
|
||||
fn archetypeOrPut(
|
||||
entities: *Self,
|
||||
columns: []const Archetype.Column,
|
||||
) !struct {
|
||||
found_existing: bool,
|
||||
hash: u64,
|
||||
index: u32,
|
||||
ptr: *Archetype,
|
||||
} {
|
||||
var hasher = std.hash.XxHash64.init(entities.seed);
|
||||
for (columns) |column| {
|
||||
hasher.update(std.mem.asBytes(&column.name));
|
||||
}
|
||||
const hash = hasher.final();
|
||||
const bucket_index = hash % entities.buckets.len;
|
||||
if (entities.buckets[bucket_index]) |bucket| {
|
||||
// Bucket already exists
|
||||
const archetype = &entities.archetypes.items[bucket];
|
||||
if (archetype.next) |_| {
|
||||
// Multiple archetypes in bucket (there were collisions)
|
||||
while (archetype.next) |collision_index| {
|
||||
const collision = &entities.archetypes.items[collision_index];
|
||||
if (collision.hash == hash) {
|
||||
// Probably a match
|
||||
// TODO: technically a hash collision could occur here, so maybe check
|
||||
// column IDs are equal here too?
|
||||
return .{ .found_existing = true, .hash = hash, .index = collision_index, .ptr = collision };
|
||||
}
|
||||
}
|
||||
|
||||
// New collision
|
||||
try entities.archetypes.append(entities.allocator, undefined);
|
||||
const index = entities.archetypes.items.len - 1;
|
||||
const ptr = &entities.archetypes.items[index];
|
||||
archetype.next = @intCast(index);
|
||||
return .{ .found_existing = false, .hash = hash, .index = @intCast(index), .ptr = ptr };
|
||||
} else if (archetype.hash == hash) {
|
||||
// Exact match
|
||||
return .{ .found_existing = true, .hash = hash, .index = bucket, .ptr = archetype };
|
||||
}
|
||||
|
||||
// New collision
|
||||
try entities.archetypes.append(entities.allocator, undefined);
|
||||
const index = entities.archetypes.items.len - 1;
|
||||
const ptr = &entities.archetypes.items[index];
|
||||
archetype.next = @intCast(index);
|
||||
return .{ .found_existing = false, .hash = hash, .index = @intCast(index), .ptr = ptr };
|
||||
}
|
||||
|
||||
// Bucket doesn't exist
|
||||
try entities.archetypes.append(entities.allocator, undefined);
|
||||
const index = entities.archetypes.items.len - 1;
|
||||
const ptr = &entities.archetypes.items[index];
|
||||
entities.buckets[bucket_index] = @intCast(index);
|
||||
return .{ .found_existing = false, .hash = hash, .index = @intCast(index), .ptr = ptr };
|
||||
}
|
||||
|
||||
/// Returns a new entity.
|
||||
pub fn new(entities: *Self) !EntityID {
|
||||
const new_id = entities.counter;
|
||||
entities.counter += 1;
|
||||
|
||||
// TODO: could skip this lookup if we store pointer
|
||||
const archetype_entry = try entities.archetypeOrPut(&.{
|
||||
.{
|
||||
.name = entities.id_name,
|
||||
.type_id = comp.typeId(EntityID),
|
||||
.size = @sizeOf(EntityID),
|
||||
.alignment = @alignOf(EntityID),
|
||||
.values = undefined,
|
||||
},
|
||||
});
|
||||
assert(archetype_entry.found_existing);
|
||||
|
||||
var void_archetype = archetype_entry.ptr;
|
||||
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,
|
||||
};
|
||||
errdefer void_archetype.undoAppend();
|
||||
|
||||
try entities.entities.put(entities.allocator, new_id, void_pointer);
|
||||
return new_id;
|
||||
}
|
||||
|
||||
/// Removes an entity.
|
||||
pub fn remove(entities: *Self, 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(archetype.len - 1, entities.id_name, 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);
|
||||
}
|
||||
|
||||
/// Given a component name, returns its ID. A new ID is created if needed.
|
||||
///
|
||||
/// The set of components used is expected to be static for the lifetime of the Entities,
|
||||
/// and as such this function allocates component names but there is no way to release that
|
||||
/// memory until Entities.deinit() is called.
|
||||
pub fn componentName(entities: *Self, name_str: []const u8) StringTable.Index {
|
||||
return entities.component_names.indexOrPut(entities.allocator, name_str) catch @panic("TODO: implement stateful OOM");
|
||||
}
|
||||
|
||||
/// Returns the archetype storage for the given entity.
|
||||
pub inline fn archetypeByID(entities: *Self, entity: EntityID) *Archetype {
|
||||
const ptr = entities.entities.get(entity).?;
|
||||
return &entities.archetypes.items[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: *Self,
|
||||
entity: EntityID,
|
||||
comptime namespace_name: std.meta.FieldEnum(@TypeOf(all_components)),
|
||||
comptime component_name: std.meta.DeclEnum(@field(all_components, @tagName(namespace_name))),
|
||||
component: @field(
|
||||
@field(all_components, @tagName(namespace_name)),
|
||||
@tagName(component_name),
|
||||
),
|
||||
) !void {
|
||||
const name_str = @tagName(namespace_name) ++ "." ++ @tagName(component_name);
|
||||
const name_id = try entities.component_names.indexOrPut(entities.allocator, name_str);
|
||||
|
||||
const prev_archetype_idx = entities.entities.get(entity).?.archetype_index;
|
||||
var prev_archetype = &entities.archetypes.items[prev_archetype_idx];
|
||||
var archetype: ?*Archetype = if (prev_archetype.hasComponent(name_id)) prev_archetype else null;
|
||||
var archetype_idx: u32 = if (archetype != null) prev_archetype_idx else 0;
|
||||
|
||||
if (archetype == null) {
|
||||
// TODO: eliminate the need for allocation and sorting here, since this can occur
|
||||
// if an archetype already exists (found_existing case below)
|
||||
const columns = try entities.allocator.alloc(Archetype.Column, prev_archetype.columns.len + 1);
|
||||
@memcpy(columns[0 .. columns.len - 1], prev_archetype.columns);
|
||||
for (columns) |*column| {
|
||||
column.values = undefined;
|
||||
}
|
||||
columns[columns.len - 1] = .{
|
||||
.name = name_id,
|
||||
.type_id = comp.typeId(@TypeOf(component)),
|
||||
.size = @sizeOf(@TypeOf(component)),
|
||||
.alignment = if (@sizeOf(@TypeOf(component)) == 0) 1 else @alignOf(@TypeOf(component)),
|
||||
.values = undefined,
|
||||
};
|
||||
std.sort.pdq(Archetype.Column, columns, {}, byTypeId);
|
||||
|
||||
const archetype_entry = try entities.archetypeOrPut(columns);
|
||||
if (!archetype_entry.found_existing) {
|
||||
archetype_entry.ptr.* = .{
|
||||
.len = 0,
|
||||
.capacity = 0,
|
||||
.columns = columns,
|
||||
.component_names = entities.component_names,
|
||||
.hash = archetype_entry.hash,
|
||||
};
|
||||
} else {
|
||||
entities.allocator.free(columns);
|
||||
}
|
||||
archetype = archetype_entry.ptr;
|
||||
archetype_idx = archetype_entry.index;
|
||||
}
|
||||
|
||||
// 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.?;
|
||||
|
||||
if (archetype_idx == prev_archetype_idx) {
|
||||
// Update the value of the existing component of the entity.
|
||||
const ptr = entities.entities.get(entity).?;
|
||||
current_archetype_storage.set(ptr.row_index, name_id, 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(new_row, entities.id_name, entity);
|
||||
for (prev_archetype.columns) |column| {
|
||||
if (column.name == entities.id_name) continue;
|
||||
for (current_archetype_storage.columns) |corresponding| {
|
||||
if (column.name == corresponding.name) {
|
||||
const old_value_raw = prev_archetype.getDynamic(old_ptr.row_index, column.name, column.size, column.alignment, column.type_id).?;
|
||||
current_archetype_storage.setDynamic(new_row, corresponding.name, old_value_raw, corresponding.alignment, corresponding.type_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the storage/column for the new component.
|
||||
current_archetype_storage.set(new_row, name_id, component);
|
||||
|
||||
prev_archetype.remove(old_ptr.row_index);
|
||||
if (prev_archetype.len > 0) {
|
||||
const swapped_entity_id = prev_archetype.get(old_ptr.row_index, entities.id_name, EntityID).?;
|
||||
try entities.entities.put(entities.allocator, swapped_entity_id, old_ptr);
|
||||
}
|
||||
|
||||
try entities.entities.put(entities.allocator, entity, Pointer{
|
||||
.archetype_index = archetype_idx,
|
||||
.row_index = new_row,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// For tags, set component.len = 0 and alignment = 1
|
||||
pub fn setComponentDynamic(
|
||||
entities: *Self,
|
||||
entity: EntityID,
|
||||
name_id: StringTable.Index,
|
||||
component: []const u8,
|
||||
alignment: u16,
|
||||
type_id: u32,
|
||||
) !void {
|
||||
const prev_archetype_idx = entities.entities.get(entity).?.archetype_index;
|
||||
var prev_archetype = &entities.archetypes.items[prev_archetype_idx];
|
||||
var archetype: ?*Archetype = if (prev_archetype.hasComponent(name_id)) prev_archetype else null;
|
||||
var archetype_idx: u32 = if (archetype != null) prev_archetype_idx else 0;
|
||||
|
||||
if (archetype == null) {
|
||||
// TODO: eliminate the need for allocation and sorting here, since this can occur
|
||||
// if an archetype already exists (found_existing case below)
|
||||
const columns = try entities.allocator.alloc(Archetype.Column, prev_archetype.columns.len + 1);
|
||||
@memcpy(columns[0 .. columns.len - 1], prev_archetype.columns);
|
||||
for (columns) |*column| {
|
||||
column.values = undefined;
|
||||
}
|
||||
columns[columns.len - 1] = .{
|
||||
.name = name_id,
|
||||
.type_id = type_id,
|
||||
.size = @intCast(component.len),
|
||||
.alignment = alignment,
|
||||
.values = undefined,
|
||||
};
|
||||
std.sort.pdq(Archetype.Column, columns, {}, byTypeId);
|
||||
|
||||
const archetype_entry = try entities.archetypeOrPut(columns);
|
||||
if (!archetype_entry.found_existing) {
|
||||
archetype_entry.ptr.* = .{
|
||||
.len = 0,
|
||||
.capacity = 0,
|
||||
.columns = columns,
|
||||
.component_names = entities.component_names,
|
||||
.hash = archetype_entry.hash,
|
||||
};
|
||||
} else {
|
||||
entities.allocator.free(columns);
|
||||
}
|
||||
archetype = archetype_entry.ptr;
|
||||
archetype_idx = archetype_entry.index;
|
||||
}
|
||||
|
||||
// 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.?;
|
||||
|
||||
if (archetype_idx == prev_archetype_idx) {
|
||||
// Update the value of the existing component of the entity.
|
||||
const ptr = entities.entities.get(entity).?;
|
||||
current_archetype_storage.setDynamic(ptr.row_index, name_id, component, alignment, type_id);
|
||||
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(new_row, entities.id_name, entity);
|
||||
for (prev_archetype.columns) |column| {
|
||||
if (column.name == entities.id_name) continue;
|
||||
for (current_archetype_storage.columns) |corresponding| {
|
||||
if (column.name == corresponding.name) {
|
||||
const old_value_raw = prev_archetype.getDynamic(old_ptr.row_index, column.name, column.size, column.alignment, column.type_id).?;
|
||||
current_archetype_storage.setDynamic(new_row, corresponding.name, old_value_raw, corresponding.alignment, corresponding.type_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the storage/column for the new component.
|
||||
current_archetype_storage.setDynamic(new_row, name_id, component, alignment, type_id);
|
||||
|
||||
prev_archetype.remove(old_ptr.row_index);
|
||||
if (prev_archetype.len > 0) {
|
||||
const swapped_entity_id = prev_archetype.get(old_ptr.row_index, entities.id_name, EntityID).?;
|
||||
try entities.entities.put(entities.allocator, swapped_entity_id, old_ptr);
|
||||
}
|
||||
|
||||
try entities.entities.put(entities.allocator, entity, Pointer{
|
||||
.archetype_index = archetype_idx,
|
||||
.row_index = new_row,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
/// Gets the named component of the given type.
|
||||
/// Returns null if the component does not exist on the entity.
|
||||
pub fn getComponent(
|
||||
entities: *Self,
|
||||
entity: EntityID,
|
||||
comptime namespace_name: std.meta.FieldEnum(@TypeOf(all_components)),
|
||||
comptime component_name: std.meta.DeclEnum(@field(all_components, @tagName(namespace_name))),
|
||||
) ?@field(
|
||||
@field(all_components, @tagName(namespace_name)),
|
||||
@tagName(component_name),
|
||||
) {
|
||||
const Component = comptime @field(
|
||||
@field(all_components, @tagName(namespace_name)),
|
||||
@tagName(component_name),
|
||||
);
|
||||
const name_str = @tagName(namespace_name) ++ "." ++ @tagName(component_name);
|
||||
const name_id = entities.component_names.index(name_str) orelse return null;
|
||||
|
||||
var archetype = entities.archetypeByID(entity);
|
||||
const ptr = entities.entities.get(entity).?;
|
||||
return archetype.get(ptr.row_index, name_id, Component);
|
||||
}
|
||||
|
||||
/// Gets the named component of the given type.
|
||||
/// Returns null if the component does not exist on the entity.
|
||||
///
|
||||
/// For tags, set size = 0 and alignment = 1
|
||||
pub fn getComponentDynamic(
|
||||
entities: *Self,
|
||||
entity: EntityID,
|
||||
name_id: StringTable.Index,
|
||||
size: u32,
|
||||
alignment: u16,
|
||||
type_id: u32,
|
||||
) ?[]u8 {
|
||||
var archetype = entities.archetypeByID(entity);
|
||||
const ptr = entities.entities.get(entity).?;
|
||||
return archetype.getDynamic(ptr.row_index, name_id, size, alignment, type_id);
|
||||
}
|
||||
|
||||
/// Removes the named component from the entity, or noop if it doesn't have such a component.
|
||||
pub fn removeComponent(
|
||||
entities: *Self,
|
||||
entity: EntityID,
|
||||
comptime namespace_name: std.meta.FieldEnum(@TypeOf(all_components)),
|
||||
comptime component_name: std.meta.DeclEnum(@field(all_components, @tagName(namespace_name))),
|
||||
) !void {
|
||||
const name_str = @tagName(namespace_name) ++ "." ++ @tagName(component_name);
|
||||
const name_id = try entities.component_names.indexOrPut(entities.allocator, name_str);
|
||||
return entities.removeComponentDynamic(entity, name_id);
|
||||
}
|
||||
|
||||
/// Removes the named component from the entity, or noop if it doesn't have such a component.
|
||||
pub fn removeComponentDynamic(
|
||||
entities: *Self,
|
||||
entity: EntityID,
|
||||
name_id: StringTable.Index,
|
||||
) !void {
|
||||
const prev_archetype_idx = entities.entities.get(entity).?.archetype_index;
|
||||
var prev_archetype = &entities.archetypes.items[prev_archetype_idx];
|
||||
var archetype: ?*Archetype = if (prev_archetype.hasComponent(name_id)) prev_archetype else return;
|
||||
var archetype_idx: u32 = if (archetype != null) prev_archetype_idx else 0;
|
||||
|
||||
// Determine which archetype the entity will move to.
|
||||
// TODO: eliminate this allocation in the found_existing case below
|
||||
const columns = try entities.allocator.alloc(Archetype.Column, prev_archetype.columns.len - 1);
|
||||
var i: usize = 0;
|
||||
for (prev_archetype.columns) |old_column| {
|
||||
if (old_column.name == name_id) continue;
|
||||
columns[i] = old_column;
|
||||
columns[i].values = undefined;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
const archetype_entry = try entities.archetypeOrPut(columns);
|
||||
if (!archetype_entry.found_existing) {
|
||||
archetype_entry.ptr.* = .{
|
||||
.len = 0,
|
||||
.capacity = 0,
|
||||
.columns = columns,
|
||||
.component_names = entities.component_names,
|
||||
.hash = archetype_entry.hash,
|
||||
};
|
||||
} else {
|
||||
entities.allocator.free(columns);
|
||||
}
|
||||
archetype = archetype_entry.ptr;
|
||||
archetype_idx = archetype_entry.index;
|
||||
|
||||
var current_archetype_storage = archetype.?;
|
||||
|
||||
// Copy 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(new_row, entities.id_name, entity);
|
||||
for (current_archetype_storage.columns) |column| {
|
||||
if (column.name == entities.id_name) continue;
|
||||
for (prev_archetype.columns) |corresponding| {
|
||||
if (column.name == corresponding.name) {
|
||||
const old_value_raw = prev_archetype.getDynamic(old_ptr.row_index, column.name, column.size, column.alignment, column.type_id).?;
|
||||
current_archetype_storage.setDynamic(new_row, column.name, old_value_raw, column.alignment, column.type_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prev_archetype.remove(old_ptr.row_index);
|
||||
if (prev_archetype.len > 0) {
|
||||
const swapped_entity_id = prev_archetype.get(old_ptr.row_index, entities.id_name, EntityID).?;
|
||||
try entities.entities.put(entities.allocator, swapped_entity_id, old_ptr);
|
||||
}
|
||||
|
||||
try entities.entities.put(entities.allocator, entity, Pointer{
|
||||
.archetype_index = archetype_idx,
|
||||
.row_index = new_row,
|
||||
});
|
||||
}
|
||||
|
||||
// Queries for archetypes matching the given query.
|
||||
pub fn query(
|
||||
entities: *Self,
|
||||
q: Query,
|
||||
) ArchetypeIterator(all_components) {
|
||||
return ArchetypeIterator(all_components).init(entities, q);
|
||||
}
|
||||
|
||||
// TODO: queryDynamic
|
||||
|
||||
// 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)
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: move this type somewhere else
|
||||
pub fn ArchetypeIterator(comptime all_components: anytype) type {
|
||||
const EntitiesT = Entities(all_components);
|
||||
return struct {
|
||||
entities: *EntitiesT,
|
||||
query: EntitiesT.Query,
|
||||
index: usize,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(entities: *EntitiesT, query: EntitiesT.Query) Self {
|
||||
return Self{
|
||||
.entities = entities,
|
||||
.query = query,
|
||||
.index = 0,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: all_components is a superset of queried items, not type-safe.
|
||||
pub fn next(iter: *Self) ?comp.ArchetypeSlicer(all_components) {
|
||||
while (iter.index < iter.entities.archetypes.items.len) {
|
||||
const archetype = &iter.entities.archetypes.items[iter.index];
|
||||
iter.index += 1;
|
||||
if (iter.match(archetype)) return comp.ArchetypeSlicer(all_components){ .archetype = archetype };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn match(iter: *Self, consideration: *Archetype) bool {
|
||||
if (consideration.len == 0) return false;
|
||||
var buf: [2048]u8 = undefined;
|
||||
switch (iter.query) {
|
||||
.all => {
|
||||
for (iter.query.all) |namespace| {
|
||||
switch (namespace) {
|
||||
inline else => |components| {
|
||||
for (components) |component| {
|
||||
if (@typeInfo(@TypeOf(component)).Enum.fields.len == 0) continue;
|
||||
const name = switch (component) {
|
||||
inline else => |c| std.fmt.bufPrint(&buf, "{s}.{s}", .{ @tagName(namespace), @tagName(c) }) catch break,
|
||||
};
|
||||
const name_id = iter.entities.componentName(name);
|
||||
var has_column = false;
|
||||
for (consideration.columns) |column| {
|
||||
if (column.name == name_id) {
|
||||
has_column = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!has_column) return false;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
.any => @panic("TODO"),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test {
|
||||
std.testing.refAllDeclsRecursive(Entities(.{}));
|
||||
}
|
||||
|
||||
// TODO: require "one big registration of components" even when using dynamic API? Would alleviate
|
||||
// some of the confusion about using world.componentName, and would perhaps improve GUI editor
|
||||
// compatibility in practice.
|
||||
test "dynamic" {
|
||||
const allocator = testing.allocator;
|
||||
const asBytes = std.mem.asBytes;
|
||||
|
||||
const Location = struct {
|
||||
x: f32 = 0,
|
||||
y: f32 = 0,
|
||||
z: f32 = 0,
|
||||
};
|
||||
|
||||
const Rotation = struct { degrees: f32 };
|
||||
|
||||
// Create a world.
|
||||
var world = try Entities(.{}).init(allocator);
|
||||
defer world.deinit();
|
||||
|
||||
// Create an entity and add dynamic components.
|
||||
const player1 = try world.new();
|
||||
try world.setComponentDynamic(player1, world.componentName("game.name"), "jane", @alignOf([]const u8), 100);
|
||||
try world.setComponentDynamic(player1, world.componentName("game.name"), "joey", @alignOf([]const u8), 100);
|
||||
try world.setComponentDynamic(player1, world.componentName("game.location"), asBytes(&Location{ .x = 1, .y = 2, .z = 3 }), @alignOf(Location), 101);
|
||||
|
||||
// Get components
|
||||
try testing.expect(world.getComponentDynamic(player1, world.componentName("game.rotation"), @sizeOf(Rotation), @alignOf(Rotation), 102) == null);
|
||||
const loc = world.getComponentDynamic(player1, world.componentName("game.location"), @sizeOf(Location), @alignOf(Location), 101);
|
||||
try testing.expectEqual(Location{ .x = 1, .y = 2, .z = 3 }, std.mem.bytesToValue(Location, @as(*[12]u8, @ptrCast(loc.?.ptr))));
|
||||
try testing.expectEqualStrings(world.getComponentDynamic(player1, world.componentName("game.name"), 4, @alignOf([]const u8), 100).?, "joey");
|
||||
}
|
||||
|
||||
test "entity ID size" {
|
||||
try testing.expectEqual(8, @sizeOf(EntityID));
|
||||
}
|
||||
|
||||
test "example" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const Location = struct {
|
||||
x: f32 = 0,
|
||||
y: f32 = 0,
|
||||
z: f32 = 0,
|
||||
};
|
||||
|
||||
const Rotation = struct { degrees: f32 };
|
||||
|
||||
const all_components = .{
|
||||
.entity = struct {
|
||||
pub const id = EntityID;
|
||||
},
|
||||
.game = struct {
|
||||
pub const location = Location;
|
||||
pub const name = []const u8;
|
||||
pub const rotation = Rotation;
|
||||
},
|
||||
};
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Create a world.
|
||||
var world = try Entities(all_components).init(allocator);
|
||||
defer world.deinit();
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Create first player entity.
|
||||
const player1 = try world.new();
|
||||
try world.setComponent(player1, .game, .name, "jane"); // add .name component
|
||||
try world.setComponent(player1, .game, .name, "joe"); // update .name component
|
||||
try world.setComponent(player1, .game, .location, .{}); // add .location component
|
||||
|
||||
// Create second player entity.
|
||||
const player2 = try world.new();
|
||||
try testing.expect(world.getComponent(player2, .game, .location) == null);
|
||||
try testing.expect(world.getComponent(player2, .game, .name) == null);
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// We can add new components at will.
|
||||
try world.setComponent(player2, .game, .rotation, .{ .degrees = 90 });
|
||||
try world.setComponent(player2, .game, .rotation, .{ .degrees = 91 }); // update .rotation component
|
||||
try testing.expect(world.getComponent(player1, .game, .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, .game, .name);
|
||||
try world.removeComponent(player1, .game, .location);
|
||||
// try world.removeComponent(player1, .game, .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.
|
||||
const archetypes = world.archetypes.items;
|
||||
try testing.expectEqual(@as(usize, 5), archetypes.len);
|
||||
// TODO: better table names, based on columns
|
||||
// try testing.expectEqual(@as(u64, 0), archetypes[0].hash);
|
||||
// try testing.expectEqual(@as(u32, 4), archetypes[1].name);
|
||||
// try testing.expectEqual(@as(u32, 14), archetypes[2].name);
|
||||
// try testing.expectEqual(@as(u32, 28), archetypes[3].name);
|
||||
// try testing.expectEqual(@as(u32, 14), archetypes[4].name);
|
||||
|
||||
// Number of (living) entities stored in an archetype table.
|
||||
try testing.expectEqual(@as(usize, 1), archetypes[0].len);
|
||||
try testing.expectEqual(@as(usize, 0), archetypes[1].len);
|
||||
try testing.expectEqual(@as(usize, 0), archetypes[2].len);
|
||||
try testing.expectEqual(@as(usize, 1), archetypes[3].len);
|
||||
|
||||
// Resolve archetype by entity ID and print column names
|
||||
const columns = world.archetypeByID(player2).columns;
|
||||
try testing.expectEqual(@as(usize, 2), columns.len);
|
||||
try testing.expectEqualStrings("id", world.component_names.string(columns[0].name));
|
||||
try testing.expectEqualStrings("game.rotation", world.component_names.string(columns[1].name));
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Query for archetypes that have all of the given components
|
||||
var iter = world.query(.{ .all = &.{
|
||||
.{ .game = &.{.rotation} },
|
||||
} });
|
||||
while (iter.next()) |archetype| {
|
||||
const ids = archetype.slice(.entity, .id);
|
||||
try testing.expectEqual(@as(usize, 1), ids.len);
|
||||
try testing.expectEqual(player2, ids[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);
|
||||
}
|
||||
|
||||
test "empty_world" {
|
||||
const allocator = testing.allocator;
|
||||
//-------------------------------------------------------------------------
|
||||
var world = try Entities(.{}).init(allocator);
|
||||
// Create a world.
|
||||
defer world.deinit();
|
||||
}
|
||||
|
||||
test "many entities" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const Location = struct {
|
||||
x: f32 = 0,
|
||||
y: f32 = 0,
|
||||
z: f32 = 0,
|
||||
};
|
||||
|
||||
const Rotation = struct { degrees: f32 };
|
||||
|
||||
const all_components = .{
|
||||
.entity = struct {
|
||||
pub const id = EntityID;
|
||||
},
|
||||
.game = struct {
|
||||
pub const location = Location;
|
||||
pub const name = []const u8;
|
||||
pub const rotation = Rotation;
|
||||
},
|
||||
};
|
||||
|
||||
// Create many entities
|
||||
var world = try Entities(all_components).init(allocator);
|
||||
defer world.deinit();
|
||||
for (0..8192) |_| {
|
||||
const player = try world.new();
|
||||
try world.setComponent(player, .game, .name, "jane");
|
||||
try world.setComponent(player, .game, .location, .{});
|
||||
}
|
||||
|
||||
// Confirm the number of archetypes created
|
||||
const archetypes = world.archetypes.items;
|
||||
try testing.expectEqual(@as(usize, 3), archetypes.len);
|
||||
|
||||
// Confirm archetypes
|
||||
var columns = archetypes[0].columns;
|
||||
try testing.expectEqual(@as(usize, 1), columns.len);
|
||||
try testing.expectEqualStrings("id", world.component_names.string(columns[0].name));
|
||||
|
||||
columns = archetypes[1].columns;
|
||||
try testing.expectEqual(@as(usize, 2), columns.len);
|
||||
try testing.expectEqualStrings("id", world.component_names.string(columns[0].name));
|
||||
try testing.expectEqualStrings("game.name", world.component_names.string(columns[1].name));
|
||||
|
||||
columns = archetypes[2].columns;
|
||||
try testing.expectEqual(@as(usize, 3), columns.len);
|
||||
try testing.expectEqualStrings("id", world.component_names.string(columns[0].name));
|
||||
try testing.expectEqualStrings("game.name", world.component_names.string(columns[1].name));
|
||||
try testing.expectEqualStrings("game.location", world.component_names.string(columns[2].name));
|
||||
}
|
||||
119
src/ecs/main.zig
Normal file
119
src/ecs/main.zig
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
//! mach/ecs is an Entity component system implementation.
|
||||
//!
|
||||
//! ## Design principles:
|
||||
//!
|
||||
//! * Initially a 100% clean-room implementation, working from first-principles. Later informed by
|
||||
//! research into how other ECS work, with advice from e.g. Bevy and Flecs authors at different
|
||||
//! points (thank you!)
|
||||
//! * Solve the problems ECS solves, in a way that is natural to Zig and leverages Zig comptime.
|
||||
//! * Fast. Optimal for CPU caches, multi-threaded, leverage comptime as much as is reasonable.
|
||||
//! * Simple. Small API footprint, should be natural and fun - not like you're writing boilerplate.
|
||||
//! * Enable other libraries to provide tracing, editors, visualizers, profilers, etc.
|
||||
//!
|
||||
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
pub const EntityID = @import("entities.zig").EntityID;
|
||||
pub const Entities = @import("entities.zig").Entities;
|
||||
pub const Archetype = @import("Archetype.zig");
|
||||
|
||||
pub const Module = @import("modules.zig").Module;
|
||||
pub const Modules = @import("modules.zig").Modules;
|
||||
pub const World = @import("systems.zig").World;
|
||||
|
||||
// TODO:
|
||||
// * Iteration
|
||||
// * Querying
|
||||
// * Multi threading
|
||||
// * Multiple entities having one value
|
||||
// * Sparse storage?
|
||||
|
||||
test "inclusion" {
|
||||
std.testing.refAllDeclsRecursive(@This());
|
||||
std.testing.refAllDeclsRecursive(@import("Archetype.zig"));
|
||||
std.testing.refAllDeclsRecursive(@import("entities.zig"));
|
||||
std.testing.refAllDeclsRecursive(@import("query.zig"));
|
||||
std.testing.refAllDeclsRecursive(@import("StringTable.zig"));
|
||||
std.testing.refAllDeclsRecursive(@import("systems.zig"));
|
||||
std.testing.refAllDeclsRecursive(@import("modules.zig"));
|
||||
}
|
||||
|
||||
test "example" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
comptime var Renderer = type;
|
||||
comptime var Physics = type;
|
||||
Physics = Module(struct {
|
||||
pointer: u8,
|
||||
|
||||
pub const name = .physics;
|
||||
pub const components = struct {
|
||||
pub const id = u32;
|
||||
};
|
||||
|
||||
pub fn tick(physics: *World(.{ Renderer, Physics }).Mod(Physics)) !void {
|
||||
_ = physics;
|
||||
}
|
||||
});
|
||||
|
||||
Renderer = Module(struct {
|
||||
pub const name = .renderer;
|
||||
pub const components = struct {
|
||||
pub const id = u16;
|
||||
};
|
||||
|
||||
pub fn tick(
|
||||
physics: *World(.{ Renderer, Physics }).Mod(Physics),
|
||||
renderer: *World(.{ Renderer, Physics }).Mod(Renderer),
|
||||
) !void {
|
||||
_ = renderer;
|
||||
_ = physics;
|
||||
}
|
||||
});
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Create a world.
|
||||
var world = try World(.{ Renderer, Physics }).init(allocator);
|
||||
defer world.deinit();
|
||||
|
||||
// Initialize module state.
|
||||
var physics = &world.mod.physics;
|
||||
var renderer = &world.mod.renderer;
|
||||
physics.state = .{ .pointer = 123 };
|
||||
_ = physics.state.pointer; // == 123
|
||||
|
||||
const player1 = try physics.newEntity();
|
||||
const player2 = try physics.newEntity();
|
||||
const player3 = try physics.newEntity();
|
||||
try physics.set(player1, .id, 1001);
|
||||
try renderer.set(player1, .id, 1001);
|
||||
|
||||
try physics.set(player2, .id, 1002);
|
||||
try physics.set(player3, .id, 1003);
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Querying
|
||||
var iter = world.entities.query(.{ .all = &.{
|
||||
.{ .physics = &.{.id} },
|
||||
} });
|
||||
|
||||
var archetype = iter.next().?;
|
||||
var ids = archetype.slice(.physics, .id);
|
||||
try testing.expectEqual(@as(usize, 2), ids.len);
|
||||
try testing.expectEqual(@as(usize, 1002), ids[0]);
|
||||
try testing.expectEqual(@as(usize, 1003), ids[1]);
|
||||
|
||||
archetype = iter.next().?;
|
||||
ids = archetype.slice(.physics, .id);
|
||||
try testing.expectEqual(@as(usize, 1), ids.len);
|
||||
try testing.expectEqual(@as(usize, 1001), ids[0]);
|
||||
|
||||
// TODO: can't write @as type here easily due to generic parameter, should be exposed
|
||||
// ?comp.ArchetypeSlicer(all_components)
|
||||
try testing.expectEqual(iter.next(), null);
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Send events to modules
|
||||
try world.send(null, .tick, .{});
|
||||
}
|
||||
166
src/ecs/modules.zig
Normal file
166
src/ecs/modules.zig
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const StructField = std.builtin.Type.StructField;
|
||||
|
||||
const EntityID = @import("entities.zig").EntityID;
|
||||
|
||||
/// Verifies that T matches the expected layout of an ECS module
|
||||
pub fn Module(comptime T: type) type {
|
||||
if (@typeInfo(T) != .Struct) @compileError("Module must be a struct type. Found:" ++ @typeName(T));
|
||||
if (!@hasDecl(T, "name")) @compileError("Module must have `pub const name = .foobar;`");
|
||||
if (@typeInfo(@TypeOf(T.name)) != .EnumLiteral) @compileError("Module must have `pub const name = .foobar;`, found type:" ++ @typeName(T.name));
|
||||
if (@hasDecl(T, "components")) {
|
||||
if (@typeInfo(T.components) != .Struct) @compileError("Module.components must be `pub const components = struct { ... };`, found type:" ++ @typeName(T.components));
|
||||
}
|
||||
return T;
|
||||
}
|
||||
|
||||
fn NamespacedComponents(comptime modules: anytype) type {
|
||||
var fields: []const StructField = &[0]StructField{};
|
||||
inline for (modules) |M| {
|
||||
const components = if (@hasDecl(M, "components")) M.components else struct {};
|
||||
fields = fields ++ [_]std.builtin.Type.StructField{.{
|
||||
.name = @tagName(M.name),
|
||||
.type = type,
|
||||
.default_value = &components,
|
||||
.is_comptime = true,
|
||||
.alignment = @alignOf(@TypeOf(components)),
|
||||
}};
|
||||
}
|
||||
|
||||
// Builtin components
|
||||
const entity_components = struct {
|
||||
pub const id = EntityID;
|
||||
};
|
||||
fields = fields ++ [_]std.builtin.Type.StructField{.{
|
||||
.name = "entity",
|
||||
.type = type,
|
||||
.default_value = &entity_components,
|
||||
.is_comptime = true,
|
||||
.alignment = @alignOf(@TypeOf(entity_components)),
|
||||
}};
|
||||
|
||||
return @Type(.{
|
||||
.Struct = .{
|
||||
.layout = .Auto,
|
||||
.is_tuple = false,
|
||||
.fields = fields,
|
||||
.decls = &[_]std.builtin.Type.Declaration{},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fn NamespacedState(comptime modules: anytype) type {
|
||||
var fields: []const StructField = &[0]StructField{};
|
||||
inline for (modules) |M| {
|
||||
const state_fields = std.meta.fields(M);
|
||||
const State = if (state_fields.len > 0) @Type(.{
|
||||
.Struct = .{
|
||||
.layout = .Auto,
|
||||
.is_tuple = false,
|
||||
.fields = state_fields,
|
||||
.decls = &[_]std.builtin.Type.Declaration{},
|
||||
},
|
||||
}) else struct {};
|
||||
fields = fields ++ [_]std.builtin.Type.StructField{.{
|
||||
.name = @tagName(M.name),
|
||||
.type = State,
|
||||
.default_value = null,
|
||||
.is_comptime = false,
|
||||
.alignment = @alignOf(State),
|
||||
}};
|
||||
}
|
||||
return @Type(.{
|
||||
.Struct = .{
|
||||
.layout = .Auto,
|
||||
.is_tuple = false,
|
||||
.fields = fields,
|
||||
.decls = &[_]std.builtin.Type.Declaration{},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub fn Modules(comptime mods: anytype) type {
|
||||
inline for (mods) |M| _ = Module(M);
|
||||
return struct {
|
||||
pub const modules = mods;
|
||||
|
||||
pub const components = NamespacedComponents(mods){};
|
||||
|
||||
pub const State = NamespacedState(mods);
|
||||
};
|
||||
}
|
||||
|
||||
test "module" {
|
||||
_ = Module(struct {
|
||||
// Physics module state
|
||||
pointer: usize,
|
||||
|
||||
// Globally unique module name
|
||||
pub const name = .engine_physics;
|
||||
|
||||
/// Physics module components
|
||||
pub const components = struct {
|
||||
/// A location component
|
||||
pub const location = @Vector(3, f32);
|
||||
};
|
||||
|
||||
pub fn tick(adapter: anytype) void {
|
||||
_ = adapter;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test "modules" {
|
||||
const Physics = Module(struct {
|
||||
// Physics module state
|
||||
pointer: usize,
|
||||
|
||||
// Globally unique module name
|
||||
pub const name = .engine_physics;
|
||||
|
||||
/// Physics module components
|
||||
pub const components = struct {
|
||||
/// A location component
|
||||
pub const location = @Vector(3, f32);
|
||||
};
|
||||
|
||||
pub fn tick(adapter: anytype) void {
|
||||
_ = adapter;
|
||||
}
|
||||
});
|
||||
|
||||
const Renderer = Module(struct {
|
||||
pub const name = .engine_renderer;
|
||||
|
||||
/// Renderer module components
|
||||
pub const components = struct {};
|
||||
|
||||
pub fn tick(adapter: anytype) void {
|
||||
_ = adapter;
|
||||
}
|
||||
});
|
||||
|
||||
const Sprite2D = Module(struct {
|
||||
pub const name = .engine_sprite2d;
|
||||
});
|
||||
|
||||
const modules = Modules(.{
|
||||
Physics,
|
||||
Renderer,
|
||||
Sprite2D,
|
||||
});
|
||||
testing.refAllDeclsRecursive(modules);
|
||||
testing.refAllDeclsRecursive(Physics);
|
||||
testing.refAllDeclsRecursive(Renderer);
|
||||
testing.refAllDeclsRecursive(Sprite2D);
|
||||
|
||||
// access namespaced components
|
||||
try testing.expectEqual(Physics.components.location, modules.components.engine_physics.location);
|
||||
try testing.expectEqual(Renderer.components, modules.components.engine_renderer);
|
||||
|
||||
// implicitly generated
|
||||
_ = modules.components.entity.id;
|
||||
|
||||
Physics.tick(null);
|
||||
}
|
||||
116
src/ecs/query.zig
Normal file
116
src/ecs/query.zig
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
pub const QueryTag = enum {
|
||||
any,
|
||||
all,
|
||||
};
|
||||
|
||||
/// A complex query for entities matching a given criteria
|
||||
pub fn Query(comptime all_components: anytype) type {
|
||||
return union(QueryTag) {
|
||||
/// Enum matching a namespace. e.g. `.game` or `.physics2d`
|
||||
pub const Namespace = std.meta.FieldEnum(@TypeOf(all_components));
|
||||
|
||||
/// Enum matching a component within a namespace
|
||||
/// e.g. `var a: Component(.physics2d) = .location`
|
||||
pub fn Component(comptime namespace: Namespace) type {
|
||||
const components = @field(all_components, @tagName(namespace));
|
||||
if (@typeInfo(components).Struct.decls.len == 0) return enum {};
|
||||
return std.meta.DeclEnum(components);
|
||||
}
|
||||
|
||||
/// Slice of enums matching a component within a namespace
|
||||
/// e.g. `&.{.location, .rotation}`
|
||||
pub fn ComponentList(comptime namespace: Namespace) type {
|
||||
return []const Component(namespace);
|
||||
}
|
||||
|
||||
/// Tagged union of namespaces matching lists of components
|
||||
/// e.g. `.physics2d = &.{ .location, .rotation }`
|
||||
pub const NamespaceComponent = T: {
|
||||
const namespaces = std.meta.fields(Namespace);
|
||||
var fields: [namespaces.len]std.builtin.Type.UnionField = undefined;
|
||||
for (namespaces, 0..) |namespace, i| {
|
||||
const ns = std.meta.stringToEnum(Namespace, namespace.name).?;
|
||||
fields[i] = .{
|
||||
.name = namespace.name,
|
||||
.type = ComponentList(ns),
|
||||
.alignment = @alignOf(ComponentList(ns)),
|
||||
};
|
||||
}
|
||||
|
||||
break :T @Type(.{ .Union = .{
|
||||
.layout = .Auto,
|
||||
.tag_type = Namespace,
|
||||
.fields = &fields,
|
||||
.decls = &.{},
|
||||
} });
|
||||
};
|
||||
|
||||
/// Matches any of these components
|
||||
any: []const NamespaceComponent,
|
||||
|
||||
/// Matches all of these components
|
||||
all: []const NamespaceComponent,
|
||||
};
|
||||
}
|
||||
|
||||
test "query" {
|
||||
const Location = struct {
|
||||
x: f32 = 0,
|
||||
y: f32 = 0,
|
||||
z: f32 = 0,
|
||||
};
|
||||
|
||||
const Rotation = struct { degrees: f32 };
|
||||
|
||||
const all_components = .{
|
||||
.game = struct {
|
||||
pub const name = []const u8;
|
||||
},
|
||||
.physics = struct {
|
||||
pub const location = Location;
|
||||
pub const rotation = Rotation;
|
||||
},
|
||||
.renderer = struct {},
|
||||
};
|
||||
|
||||
const Q = Query(all_components);
|
||||
|
||||
// Namespace type lets us select a single namespace.
|
||||
try testing.expectEqual(@as(Q.Namespace, .game), .game);
|
||||
try testing.expectEqual(@as(Q.Namespace, .physics), .physics);
|
||||
|
||||
// Component type lets us select a single component within a namespace.
|
||||
try testing.expectEqual(@as(Q.Component(.physics), .location), .location);
|
||||
try testing.expectEqual(@as(Q.Component(.game), .name), .name);
|
||||
|
||||
// ComponentList type lets us select multiple components within a namespace.
|
||||
const x: Q.ComponentList(.physics) = &.{
|
||||
.location,
|
||||
.rotation,
|
||||
};
|
||||
_ = x;
|
||||
|
||||
// NamespaceComponent lets us select multiple components within multiple namespaces.
|
||||
const y: []const Q.NamespaceComponent = &.{
|
||||
.{ .physics = &.{ .location, .rotation } },
|
||||
.{ .game = &.{.name} },
|
||||
};
|
||||
_ = y;
|
||||
|
||||
// Query matching entities with *any* of these components
|
||||
const z: Q = .{ .any = &.{
|
||||
.{ .physics = &.{ .location, .rotation } },
|
||||
.{ .game = &.{.name} },
|
||||
} };
|
||||
_ = z;
|
||||
|
||||
// Query matching entities with *all* of these components.
|
||||
const w: Q = .{ .all = &.{
|
||||
.{ .physics = &.{ .location, .rotation } },
|
||||
.{ .game = &.{.name} },
|
||||
} };
|
||||
_ = w;
|
||||
}
|
||||
200
src/ecs/systems.zig
Normal file
200
src/ecs/systems.zig
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const StructField = std.builtin.Type.StructField;
|
||||
|
||||
const Entities = @import("entities.zig").Entities;
|
||||
const Modules = @import("modules.zig").Modules;
|
||||
const EntityID = @import("entities.zig").EntityID;
|
||||
const comp = @import("comptime.zig");
|
||||
|
||||
pub fn World(comptime mods: anytype) type {
|
||||
const modules = Modules(mods);
|
||||
|
||||
return struct {
|
||||
allocator: mem.Allocator,
|
||||
entities: Entities(modules.components),
|
||||
mod: Mods(),
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn Mod(comptime Module: anytype) type {
|
||||
const module_tag = Module.name;
|
||||
const State = @TypeOf(@field(@as(modules.State, undefined), @tagName(module_tag)));
|
||||
const components = @field(modules.components, @tagName(module_tag));
|
||||
return struct {
|
||||
state: State,
|
||||
entities: *Entities(modules.components),
|
||||
allocator: mem.Allocator,
|
||||
|
||||
/// 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 inline fn set(
|
||||
m: *@This(),
|
||||
entity: EntityID,
|
||||
comptime component_name: std.meta.DeclEnum(components),
|
||||
component: @field(components, @tagName(component_name)),
|
||||
) !void {
|
||||
const mod_ptr: *Self.Mods() = @alignCast(@fieldParentPtr(Mods(), @tagName(module_tag), m));
|
||||
const world = @fieldParentPtr(Self, "mod", mod_ptr);
|
||||
try world.entities.setComponent(entity, module_tag, component_name, component);
|
||||
}
|
||||
|
||||
/// 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 inline fn get(
|
||||
m: *@This(),
|
||||
entity: EntityID,
|
||||
comptime component_name: std.meta.DeclEnum(components),
|
||||
) ?@field(components, @tagName(component_name)) {
|
||||
const mod_ptr: *Self.Mods() = @alignCast(@fieldParentPtr(Mods(), @tagName(module_tag), m));
|
||||
const world = @fieldParentPtr(Self, "mod", mod_ptr);
|
||||
return world.entities.getComponent(entity, module_tag, component_name);
|
||||
}
|
||||
|
||||
/// Removes the named component from the entity, or noop if it doesn't have such a component.
|
||||
pub inline fn remove(
|
||||
m: *@This(),
|
||||
entity: EntityID,
|
||||
comptime component_name: std.meta.DeclEnum(components),
|
||||
) !void {
|
||||
const mod_ptr: *Self.Mods() = @alignCast(@fieldParentPtr(Mods(), @tagName(module_tag), m));
|
||||
const world = @fieldParentPtr(Self, "mod", mod_ptr);
|
||||
try world.entities.removeComponent(entity, module_tag, component_name);
|
||||
}
|
||||
|
||||
pub fn send(m: *@This(), comptime msg_tag: anytype, args: anytype) !void {
|
||||
const mod_ptr: *Self.Mods() = @alignCast(@fieldParentPtr(Mods(), @tagName(module_tag), m));
|
||||
const world = @fieldParentPtr(Self, "mod", mod_ptr);
|
||||
return world.sendStr(module_tag, @tagName(msg_tag), args);
|
||||
}
|
||||
|
||||
/// Returns a new entity.
|
||||
pub fn newEntity(m: *@This()) !EntityID {
|
||||
const mod_ptr: *Self.Mods() = @alignCast(@fieldParentPtr(Mods(), @tagName(module_tag), m));
|
||||
const world = @fieldParentPtr(Self, "mod", mod_ptr);
|
||||
return world.entities.new();
|
||||
}
|
||||
|
||||
/// Removes an entity.
|
||||
pub fn removeEntity(m: *@This(), entity: EntityID) !void {
|
||||
const mod_ptr: *Self.Mods() = @alignCast(@fieldParentPtr(Mods(), @tagName(module_tag), m));
|
||||
const world = @fieldParentPtr(Self, "mod", mod_ptr);
|
||||
try world.entities.removeEntity(entity);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn Mods() type {
|
||||
var fields: []const StructField = &[0]StructField{};
|
||||
inline for (modules.modules) |M| {
|
||||
fields = fields ++ [_]std.builtin.Type.StructField{.{
|
||||
.name = @tagName(M.name),
|
||||
.type = Mod(M),
|
||||
.default_value = null,
|
||||
.is_comptime = false,
|
||||
.alignment = @alignOf(Mod(M)),
|
||||
}};
|
||||
}
|
||||
return @Type(.{
|
||||
.Struct = .{
|
||||
.layout = .Auto,
|
||||
.is_tuple = false,
|
||||
.fields = fields,
|
||||
.decls = &[_]std.builtin.Type.Declaration{},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub fn init(allocator: mem.Allocator) !Self {
|
||||
return Self{
|
||||
.allocator = allocator,
|
||||
.entities = try Entities(modules.components).init(allocator),
|
||||
.mod = undefined,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(world: *Self) void {
|
||||
world.entities.deinit();
|
||||
}
|
||||
|
||||
/// Broadcasts an event to all modules that are subscribed to it.
|
||||
///
|
||||
/// The message tag corresponds with the handler method name to be invoked. For example,
|
||||
/// if `send(.tick)` is invoked, all modules which declare a `pub fn tick` will be invoked.
|
||||
///
|
||||
/// Events sent by Mach itself, or the application itself, may be single words. To prevent
|
||||
/// name conflicts, events sent by modules provided by a library should prefix their events
|
||||
/// with their module name. For example, a module named `.ziglibs_imgui` should use event
|
||||
/// names like `.ziglibsImguiClick`, `.ziglibsImguiFoobar`.
|
||||
pub fn send(world: *Self, comptime optional_module_tag: anytype, comptime msg_tag: anytype, args: anytype) !void {
|
||||
return world.sendStr(optional_module_tag, @tagName(msg_tag), args);
|
||||
}
|
||||
|
||||
pub fn sendStr(world: *Self, comptime optional_module_tag: anytype, comptime msg: anytype, args: anytype) !void {
|
||||
// Check for any module that has a handler function named msg (e.g. `fn init` would match "init")
|
||||
inline for (modules.modules) |M| {
|
||||
const EventHandlers = blk: {
|
||||
switch (@typeInfo(@TypeOf(optional_module_tag))) {
|
||||
.Null => break :blk M,
|
||||
.EnumLiteral => {
|
||||
// Send this message only to the specified module
|
||||
if (M.name != optional_module_tag) continue;
|
||||
if (!@hasDecl(M, "local")) @compileError("Module ." ++ @tagName(M.name) ++ " does not have a `pub const local` event handler for message ." ++ msg);
|
||||
if (!@hasDecl(M.local, msg)) @compileError("Module ." ++ @tagName(M.name) ++ " does not have a `pub const local` event handler for message ." ++ msg);
|
||||
break :blk M.local;
|
||||
},
|
||||
.Optional => if (optional_module_tag) |v| {
|
||||
// Send this message only to the specified module
|
||||
if (M.name != v) continue;
|
||||
if (!@hasDecl(M, "local")) @compileError("Module ." ++ @tagName(M.name) ++ " does not have a `pub const local` event handler for message ." ++ msg);
|
||||
if (!@hasDecl(M.local, msg)) @compileError("Module ." ++ @tagName(M.name) ++ " does not have a `pub const local` event handler for message ." ++ msg);
|
||||
break :blk M.local;
|
||||
},
|
||||
else => @panic("unexpected optional_module_tag type: " ++ @typeName(@TypeOf(optional_module_tag))),
|
||||
}
|
||||
};
|
||||
if (!@hasDecl(EventHandlers, msg)) continue;
|
||||
|
||||
// Determine which parameters the handler function wants. e.g.:
|
||||
//
|
||||
// pub fn init(eng: *mach.Engine) !void
|
||||
// pub fn init(eng: *mach.Engine, mach: *mach.Engine.Mod) !void
|
||||
//
|
||||
const handler = @field(EventHandlers, msg);
|
||||
|
||||
// Build a tuple of parameters that we can pass to the function, based on what
|
||||
// *mach.Mod(T) types it expects as arguments.
|
||||
var params: std.meta.ArgsTuple(@TypeOf(handler)) = undefined;
|
||||
comptime var argIndex = 0;
|
||||
inline for (@typeInfo(@TypeOf(params)).Struct.fields) |param| {
|
||||
comptime var found = false;
|
||||
inline for (@typeInfo(Mods()).Struct.fields) |f| {
|
||||
if (param.type == *f.type) {
|
||||
// TODO: better initialization place for modules
|
||||
@field(@field(world.mod, f.name), "entities") = &world.entities;
|
||||
@field(@field(world.mod, f.name), "allocator") = world.allocator;
|
||||
|
||||
@field(params, param.name) = &@field(world.mod, f.name);
|
||||
found = true;
|
||||
break;
|
||||
} else if (param.type == *Self) {
|
||||
@field(params, param.name) = world;
|
||||
found = true;
|
||||
break;
|
||||
} else if (param.type == f.type) {
|
||||
@compileError("Module handler " ++ @tagName(M.name) ++ "." ++ msg ++ " should be *T not T: " ++ @typeName(param.type));
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
@field(params, param.name) = args[argIndex];
|
||||
argIndex += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke the handler
|
||||
try @call(.auto, handler, params);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue