all: move standalone libraries to libs/ subdirectory
The root dir of our repository has grown quite a lot the past few months.
I'd like to make it more clear where the bulk of the engine lives (`src/`) and
also make it more clear which Mach libraries are consumable as standalone projects.
As for the name of this directory, `libs` was my first choice but there's a bit of
a convention of that being external libraries in Zig projects _today_, while these
are libraries maintained as part of Mach in this repository - not external ones.
We will name this directory `libs`, and if we have a need for external libraries
we will use `external` or `deps` for that directory name. I considered other names
such as `components`, `systems`, `modules` (which are bad as they overlap with
major ECS / engine concepts), and it seems likely the official Zig package manager
will break the convention of using a `libs` dir anyway.
Performed via:
```sh
mkdir libs/
git mv freetype libs/
git mv basisu libs/
git mv gamemode libs/
git mv glfw libs/
git mv gpu libs/
git mv gpu-dawn libs/
git mv sysaudio libs/
git mv sysjs libs/
git mv ecs libs/
```
git-subtree-dir: glfw
git-subtree-mainline: 0d5b853443
git-subtree-split: 572d1144f11b353abdb64fff828b25a4f0fbb7ca
Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
git mv ecs libs/
Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
parent
79ec61396f
commit
0645429df9
240 changed files with 6 additions and 6 deletions
870
libs/ecs/src/entities.zig
Normal file
870
libs/ecs/src/entities.zig
Normal file
|
|
@ -0,0 +1,870 @@
|
|||
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;
|
||||
}
|
||||
|
||||
if (storage.capacity > 0) {
|
||||
gpa.free(storage.block);
|
||||
}
|
||||
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 fn Entities(all_components: anytype) type {
|
||||
// TODO: validate all_components is a namespaced component set in the form we expect
|
||||
_ = all_components;
|
||||
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) = .{},
|
||||
|
||||
/// A mapping of archetype hash to their storage.
|
||||
///
|
||||
/// Database equivalent: table name -> tables representing entities.
|
||||
archetypes: std.AutoArrayHashMapUnmanaged(u64, ArchetypeStorage) = .{},
|
||||
|
||||
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[ptr.archetype_index].rows[ptr.row_index]
|
||||
/// ```
|
||||
///
|
||||
pub const Pointer = struct {
|
||||
archetype_index: u16,
|
||||
row_index: u32,
|
||||
};
|
||||
|
||||
pub const Query = Query: {
|
||||
const namespaces = std.meta.fields(@TypeOf(all_components));
|
||||
var fields: [namespaces.len]std.builtin.Type.UnionField = undefined;
|
||||
inline for (namespaces) |namespace, i| {
|
||||
const component_enum = std.meta.FieldEnum(namespace.field_type);
|
||||
fields[i] = .{
|
||||
.name = namespace.name,
|
||||
.field_type = component_enum,
|
||||
.alignment = @alignOf(component_enum),
|
||||
};
|
||||
}
|
||||
|
||||
// need type_info variable (rather than embedding in @Type() call)
|
||||
// to work around stage 1 bug
|
||||
const type_info = std.builtin.Type{
|
||||
.Union = .{
|
||||
.layout = .Auto,
|
||||
.tag_type = std.meta.FieldEnum(@TypeOf(all_components)),
|
||||
.fields = &fields,
|
||||
.decls = &.{},
|
||||
},
|
||||
};
|
||||
break :Query @Type(type_info);
|
||||
};
|
||||
|
||||
fn fullComponentName(comptime q: Query) []const u8 {
|
||||
return @tagName(q) ++ "." ++ @tagName(@field(q, @tagName(std.meta.activeTag(q))));
|
||||
}
|
||||
|
||||
pub fn Iter(comptime components: []const Query) type {
|
||||
return struct {
|
||||
entities: *Self,
|
||||
archetype_index: usize = 0,
|
||||
row_index: u32 = 0,
|
||||
|
||||
const Iterator = @This();
|
||||
|
||||
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 (!hasComponents(archetype, 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 };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn hasComponents(storage: ArchetypeStorage, comptime components: []const Query) bool {
|
||||
var archetype = storage;
|
||||
if (components.len == 0) return false;
|
||||
inline for (components) |component| {
|
||||
if (!archetype.hasComponent(fullComponentName(component))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn query(entities: *Self, comptime components: []const Query) Iter(components) {
|
||||
return Iter(components){
|
||||
.entities = entities,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init(allocator: Allocator) !Self {
|
||||
var entities = Self{ .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: *Self) 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: *Self) !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: *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(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: *Self, 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: *Self,
|
||||
entity: EntityID,
|
||||
comptime namespace_name: std.meta.FieldEnum(@TypeOf(all_components)),
|
||||
comptime component_name: std.meta.FieldEnum(@TypeOf(@field(all_components, @tagName(namespace_name)))),
|
||||
component: @field(
|
||||
@field(all_components, @tagName(namespace_name)),
|
||||
@tagName(component_name),
|
||||
),
|
||||
) !void {
|
||||
const name = @tagName(namespace_name) ++ "." ++ @tagName(component_name);
|
||||
|
||||
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);
|
||||
|
||||
// getOrPut allocated, so the archetype we retrieved earlier may no longer be a valid
|
||||
// pointer. Refresh it now:
|
||||
archetype = entities.archetypeByID(entity);
|
||||
|
||||
if (!archetype_entry.found_existing) {
|
||||
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: *Self,
|
||||
entity: EntityID,
|
||||
comptime namespace_name: std.meta.FieldEnum(@TypeOf(all_components)),
|
||||
comptime component_name: std.meta.FieldEnum(@TypeOf(@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 = @tagName(namespace_name) ++ "." ++ @tagName(component_name);
|
||||
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: *Self,
|
||||
entity: EntityID,
|
||||
comptime namespace_name: std.meta.FieldEnum(@TypeOf(all_components)),
|
||||
comptime component_name: std.meta.FieldEnum(@TypeOf(@field(all_components, @tagName(namespace_name)))),
|
||||
) !void {
|
||||
const name = @tagName(namespace_name) ++ "." ++ @tagName(component_name);
|
||||
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);
|
||||
|
||||
// getOrPut allocated, so the archetype we retrieved earlier may no longer be a valid
|
||||
// pointer. Refresh it now:
|
||||
archetype = entities.archetypeByID(entity);
|
||||
|
||||
if (!archetype_entry.found_existing) {
|
||||
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;
|
||||
|
||||
const Location = struct {
|
||||
x: f32 = 0,
|
||||
y: f32 = 0,
|
||||
z: f32 = 0,
|
||||
};
|
||||
|
||||
const Rotation = struct { degrees: f32 };
|
||||
|
||||
const all_components = .{
|
||||
.game = .{
|
||||
.location = Location,
|
||||
.name = []const u8,
|
||||
.rotation = Rotation,
|
||||
},
|
||||
};
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Create a world.
|
||||
var world = try Entities(all_components).init(allocator);
|
||||
defer world.deinit();
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Create first player entity.
|
||||
var player1 = try world.new();
|
||||
try world.setComponent(player1, .game, .name, "jane"); // add Name component
|
||||
try world.setComponent(player1, .game, .location, .{}); // add Location component
|
||||
|
||||
// Create second player entity.
|
||||
var 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 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.
|
||||
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, 10567852867187873021), archetypes[1]);
|
||||
try testing.expectEqual(@as(u64, 14072552683119202344), archetypes[2]);
|
||||
try testing.expectEqual(@as(u64, 17945105277702244199), archetypes[3]);
|
||||
try testing.expectEqual(@as(u64, 12546098194442238762), 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("game.location", columns[0].name);
|
||||
try testing.expectEqualStrings("game.name", columns[1].name);
|
||||
try testing.expectEqualStrings("id", columns[2].name);
|
||||
|
||||
// Archetype resolved via entity ID
|
||||
var player2_archetype = world.archetypeByID(player2);
|
||||
try testing.expectEqual(@as(u64, 4263961864502127795), 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);
|
||||
}
|
||||
99
libs/ecs/src/main.zig
Normal file
99
libs/ecs/src/main.zig
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
//! mach/ecs is an Entity component system implementation.
|
||||
//!
|
||||
//! ## Design principles:
|
||||
//!
|
||||
//! * Clean-room implementation (author has not read any other ECS implementation code.)
|
||||
//! * Solve the problems ECS solves, in a way that is natural to Zig and leverages Zig comptime.
|
||||
//! * Avoid patent infringement upon Unity ECS patent claims.
|
||||
//! * Fast. Optimal for CPU caches, multi-threaded, leverage comptime as much as is reasonable.
|
||||
//! * Simple. Small API footprint, should be natural and fun - not like you're writing boilerplate.
|
||||
//! * Enable other libraries to provide tracing, editors, visualizers, profilers, etc.
|
||||
//!
|
||||
//! ## Copyright & patent mitigation
|
||||
//!
|
||||
//! The initial implementation was a clean-room implementation by Stephen Gutekanst without having
|
||||
//! read other ECS implementations' code, but with speaking to people familiar with other ECS
|
||||
//! implementations. Contributions past the initial implementation may be made by individuals in
|
||||
//! non-clean-room settings.
|
||||
//!
|
||||
//! Critically, this entity component system stores components for a classified archetype using
|
||||
//! independent arrays allocated per component as well as hashmaps for sparse component data as an
|
||||
//! optimization. This is a novel and fundamentally different process than what is described in
|
||||
//! Unity Software Inc's patent US 10,599,560. This is not legal advice.
|
||||
//!
|
||||
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
pub const EntityID = @import("entities.zig").EntityID;
|
||||
pub const Entities = @import("entities.zig").Entities;
|
||||
|
||||
pub const Module = @import("systems.zig").Module;
|
||||
pub const Modules = @import("systems.zig").Modules;
|
||||
pub const Messages = @import("systems.zig").Messages;
|
||||
pub const MessagesTag = @import("systems.zig").MessagesTag;
|
||||
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());
|
||||
}
|
||||
|
||||
test "example" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const PhysicsMsg = Messages(.{
|
||||
.tick = void,
|
||||
});
|
||||
const physicsUpdate = (struct {
|
||||
pub fn physicsUpdate(msg: PhysicsMsg) void {
|
||||
switch (msg) {
|
||||
.tick => std.debug.print("\nphysics tick!\n", .{}),
|
||||
}
|
||||
}
|
||||
}).physicsUpdate;
|
||||
|
||||
const modules = Modules(.{
|
||||
.physics = Module(.{
|
||||
.components = .{
|
||||
.id = u32,
|
||||
},
|
||||
.globals = struct {
|
||||
pointer: u8,
|
||||
},
|
||||
.messages = PhysicsMsg,
|
||||
.update = physicsUpdate,
|
||||
}),
|
||||
.renderer = Module(.{
|
||||
.components = .{
|
||||
.id = u16,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Create a world.
|
||||
var world = try World(modules).init(allocator);
|
||||
defer world.deinit();
|
||||
|
||||
// Initialize globals.
|
||||
world.set(.physics, .pointer, 123);
|
||||
_ = world.get(.physics, .pointer); // == 123
|
||||
|
||||
const player1 = try world.entities.new();
|
||||
const player2 = try world.entities.new();
|
||||
const player3 = try world.entities.new();
|
||||
try world.entities.setComponent(player1, .physics, .id, 1234);
|
||||
try world.entities.setComponent(player1, .renderer, .id, 1234);
|
||||
|
||||
try world.entities.setComponent(player2, .physics, .id, 1234);
|
||||
try world.entities.setComponent(player3, .physics, .id, 1234);
|
||||
|
||||
world.tick();
|
||||
}
|
||||
291
libs/ecs/src/systems.zig
Normal file
291
libs/ecs/src/systems.zig
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const Allocator = mem.Allocator;
|
||||
const testing = std.testing;
|
||||
const math = std.math;
|
||||
const StructField = std.builtin.Type.StructField;
|
||||
const EnumField = std.builtin.Type.EnumField;
|
||||
const UnionField = std.builtin.Type.UnionField;
|
||||
|
||||
const Entities = @import("entities.zig").Entities;
|
||||
|
||||
/// An ECS module can provide components, systems, and global values.
|
||||
pub fn Module(comptime Params: anytype) @TypeOf(Params) {
|
||||
// TODO: validate the type
|
||||
return Params;
|
||||
}
|
||||
|
||||
/// Describes a set of ECS modules, each of which can provide components, systems, and more.
|
||||
pub fn Modules(modules: anytype) @TypeOf(modules) {
|
||||
// TODO: validate the type
|
||||
return modules;
|
||||
}
|
||||
|
||||
/// Returns a tagged union representing the messages, turning this:
|
||||
///
|
||||
/// ```
|
||||
/// .{ .tick = void, .foo = i32 }
|
||||
/// ```
|
||||
///
|
||||
/// Into `T`:
|
||||
///
|
||||
/// ```
|
||||
/// const T = union(MessagesTag(messages)) {
|
||||
/// .tick = void,
|
||||
/// .foo = i32,
|
||||
/// };
|
||||
/// ```
|
||||
pub fn Messages(messages: anytype) type {
|
||||
var fields: []const UnionField = &[0]UnionField{};
|
||||
const message_fields = std.meta.fields(@TypeOf(messages));
|
||||
inline for (message_fields) |message_field| {
|
||||
const message_type = @field(messages, message_field.name);
|
||||
fields = fields ++ [_]std.builtin.Type.UnionField{.{
|
||||
.name = message_field.name,
|
||||
.field_type = message_type,
|
||||
.alignment = if (message_type == void) 0 else @alignOf(message_type),
|
||||
}};
|
||||
}
|
||||
|
||||
// Hack to workaround stage1 compiler bug. https://github.com/ziglang/zig/issues/8114
|
||||
//
|
||||
// return @Type(.{
|
||||
// .Union = .{
|
||||
// .layout = .Auto,
|
||||
// .tag_type = MessagesTag(messages),
|
||||
// .fields = fields,
|
||||
// .decls = &[_]std.builtin.Type.Declaration{},
|
||||
// },
|
||||
// });
|
||||
//
|
||||
const Ref = union(enum) { temp };
|
||||
var info = @typeInfo(Ref);
|
||||
info.Union.tag_type = MessagesTag(messages);
|
||||
info.Union.fields = fields;
|
||||
return @Type(info);
|
||||
}
|
||||
|
||||
/// Returns the tag enum for a tagged union representing the messages, turning this:
|
||||
///
|
||||
/// ```
|
||||
/// .{ .tick = void, .foo = i32 }
|
||||
/// ```
|
||||
///
|
||||
/// Into this:
|
||||
///
|
||||
/// ```
|
||||
/// enum { .tick, .foo };
|
||||
/// ```
|
||||
pub fn MessagesTag(messages: anytype) type {
|
||||
var fields: []const EnumField = &[0]EnumField{};
|
||||
const message_fields = std.meta.fields(@TypeOf(messages));
|
||||
inline for (message_fields) |message_field, index| {
|
||||
fields = fields ++ [_]std.builtin.Type.EnumField{.{
|
||||
.name = message_field.name,
|
||||
.value = index,
|
||||
}};
|
||||
}
|
||||
|
||||
return @Type(.{
|
||||
.Enum = .{
|
||||
.layout = .Auto,
|
||||
.tag_type = std.meta.Int(.unsigned, @floatToInt(u16, math.ceil(math.log2(@intToFloat(f64, message_fields.len))))),
|
||||
.fields = fields,
|
||||
.decls = &[_]std.builtin.Type.Declaration{},
|
||||
.is_exhaustive = true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the namespaced components struct **type**.
|
||||
//
|
||||
/// Consult `namespacedComponents` for how a value of this type looks.
|
||||
fn NamespacedComponents(comptime modules: anytype) type {
|
||||
var fields: []const StructField = &[0]StructField{};
|
||||
inline for (std.meta.fields(@TypeOf(modules))) |module_field| {
|
||||
const module = @field(modules, module_field.name);
|
||||
if (@hasField(@TypeOf(module), "components")) {
|
||||
fields = fields ++ [_]std.builtin.Type.StructField{.{
|
||||
.name = module_field.name,
|
||||
.field_type = @TypeOf(module.components),
|
||||
.default_value = null,
|
||||
.is_comptime = false,
|
||||
.alignment = @alignOf(@TypeOf(module.components)),
|
||||
}};
|
||||
}
|
||||
}
|
||||
return @Type(.{
|
||||
.Struct = .{
|
||||
.layout = .Auto,
|
||||
.is_tuple = false,
|
||||
.fields = fields,
|
||||
.decls = &[_]std.builtin.Type.Declaration{},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/// Extracts namespaces components from modules like this:
|
||||
///
|
||||
/// ```
|
||||
/// .{
|
||||
/// .renderer = .{
|
||||
/// .components = .{
|
||||
/// .location = Vec3,
|
||||
/// .rotation = Vec3,
|
||||
/// },
|
||||
/// ...
|
||||
/// },
|
||||
/// .physics2d = .{
|
||||
/// .components = .{
|
||||
/// .location = Vec2
|
||||
/// .velocity = Vec2,
|
||||
/// },
|
||||
/// ...
|
||||
/// },
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Returning a namespaced components value like this:
|
||||
///
|
||||
/// ```
|
||||
/// .{
|
||||
/// .renderer = .{
|
||||
/// .location = Vec3,
|
||||
/// .rotation = Vec3,
|
||||
/// },
|
||||
/// .physics2d = .{
|
||||
/// .location = Vec2
|
||||
/// .velocity = Vec2,
|
||||
/// },
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
fn namespacedComponents(comptime modules: anytype) NamespacedComponents(modules) {
|
||||
var x: NamespacedComponents(modules) = undefined;
|
||||
inline for (std.meta.fields(@TypeOf(modules))) |module_field| {
|
||||
const module = @field(modules, module_field.name);
|
||||
if (@hasField(@TypeOf(module), "components")) {
|
||||
@field(x, module_field.name) = module.components;
|
||||
}
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
/// Extracts namespaced globals from modules like this:
|
||||
///
|
||||
/// ```
|
||||
/// .{
|
||||
/// .renderer = .{
|
||||
/// .globals = struct{
|
||||
/// foo: *Bar,
|
||||
/// baz: Bam,
|
||||
/// },
|
||||
/// ...
|
||||
/// },
|
||||
/// .physics2d = .{
|
||||
/// .globals = struct{
|
||||
/// foo: *Instance,
|
||||
/// },
|
||||
/// ...
|
||||
/// },
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Into a namespaced global type like this:
|
||||
///
|
||||
/// ```
|
||||
/// struct{
|
||||
/// renderer: struct{
|
||||
/// foo: *Bar,
|
||||
/// baz: Bam,
|
||||
/// },
|
||||
/// physics2d: struct{
|
||||
/// foo: *Instance,
|
||||
/// },
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
fn NamespacedGlobals(comptime modules: anytype) type {
|
||||
var fields: []const StructField = &[0]StructField{};
|
||||
inline for (std.meta.fields(@TypeOf(modules))) |module_field| {
|
||||
const module = @field(modules, module_field.name);
|
||||
if (@hasField(@TypeOf(module), "globals")) {
|
||||
fields = fields ++ [_]std.builtin.Type.StructField{.{
|
||||
.name = module_field.name,
|
||||
.field_type = module.globals,
|
||||
.default_value = null,
|
||||
.is_comptime = false,
|
||||
.alignment = @alignOf(module.globals),
|
||||
}};
|
||||
}
|
||||
}
|
||||
return @Type(.{
|
||||
.Struct = .{
|
||||
.layout = .Auto,
|
||||
.is_tuple = false,
|
||||
.fields = fields,
|
||||
.decls = &[_]std.builtin.Type.Declaration{},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub fn World(comptime modules: anytype) type {
|
||||
const all_components = namespacedComponents(modules);
|
||||
return struct {
|
||||
allocator: Allocator,
|
||||
entities: Entities(all_components),
|
||||
globals: NamespacedGlobals(modules),
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(allocator: Allocator) !Self {
|
||||
return Self{
|
||||
.allocator = allocator,
|
||||
.entities = try Entities(all_components).init(allocator),
|
||||
.globals = undefined,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(world: *Self) void {
|
||||
world.entities.deinit();
|
||||
}
|
||||
|
||||
/// Gets a global value called `.global_tag` from the module named `.module_tag`
|
||||
pub fn get(world: *Self, module_tag: anytype, global_tag: anytype) @TypeOf(@field(
|
||||
@field(world.globals, @tagName(module_tag)),
|
||||
@tagName(global_tag),
|
||||
)) {
|
||||
return comptime @field(
|
||||
@field(world.globals, @tagName(module_tag)),
|
||||
@tagName(global_tag),
|
||||
);
|
||||
}
|
||||
|
||||
/// Sets a global value called `.global_tag` in the module named `.module_tag`
|
||||
pub fn set(
|
||||
world: *Self,
|
||||
comptime module_tag: anytype,
|
||||
comptime global_tag: anytype,
|
||||
value: @TypeOf(@field(
|
||||
@field(world.globals, @tagName(module_tag)),
|
||||
@tagName(global_tag),
|
||||
)),
|
||||
) void {
|
||||
comptime @field(
|
||||
@field(world.globals, @tagName(module_tag)),
|
||||
@tagName(global_tag),
|
||||
) = value;
|
||||
}
|
||||
|
||||
/// Tick sends the global 'tick' message to all modules that are subscribed to it.
|
||||
pub fn tick(world: *Self) void {
|
||||
_ = world;
|
||||
inline for (std.meta.fields(@TypeOf(modules))) |module_field| {
|
||||
const module = @field(modules, module_field.name);
|
||||
if (@hasField(@TypeOf(module), "messages")) {
|
||||
if (@hasField(module.messages, "tick")) module.update(.tick);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue