Skip to content

Instantly share code, notes, and snippets.

@slimsag

slimsag/ecs.zig Secret

Last active January 26, 2024 16:21
Show Gist options
  • Save slimsag/477f9f4c68667e71fbe584a700cfd87d to your computer and use it in GitHub Desktop.
Save slimsag/477f9f4c68667e71fbe584a700cfd87d to your computer and use it in GitHub Desktop.
"Let's build an Entity Component System (part 2): databases" PARTIAL code (creating entities only)
const std = @import("std");
const testing = std.testing;
const Allocator = std.mem.Allocator;
/// An entity ID uniquely identifies an entity globally within an Entities set.
pub const EntityID = u64;
pub const void_archetype_hash = std.math.maxInt(u64);
/// Represents the storage for a single type of component within a single type of entity.
///
/// Database equivalent: a column within a table.
pub fn ComponentStorage(comptime Component: type) type {
return struct {
/// A reference to the total number of entities with the same type as is being stored here.
total_rows: *usize,
/// The actual densely stored component data.
data: std.ArrayListUnmanaged(Component) = .{},
const Self = @This();
pub fn deinit(storage: *Self, allocator: Allocator) void {
storage.data.deinit(allocator);
}
};
}
/// A type-erased representation of ComponentStorage(T) (where T is unknown).
pub const ErasedComponentStorage = struct {
ptr: *anyopaque,
deinit: fn (erased: *anyopaque, allocator: Allocator) void,
// Casts this `ErasedComponentStorage` into `*ComponentStorage(Component)` with the given type
// (unsafe).
pub fn cast(ptr: *anyopaque, comptime Component: type) *ComponentStorage(Component) {
var aligned = @alignCast(@alignOf(*ComponentStorage(Component)), ptr);
return @ptrCast(*ComponentStorage(Component), aligned);
}
};
pub const ArchetypeStorage = struct {
allocator: Allocator,
/// The hash of every component name in this archetype, i.e. the name of this archetype.
hash: u64,
/// A mapping of rows in the table to entity IDs.
///
/// Doubles as the counter of total number of rows that have been reserved within this
/// archetype table.
entity_ids: std.ArrayListUnmanaged(EntityID) = .{},
/// A string hashmap of component_name -> type-erased *ComponentStorage(Component)
components: std.StringArrayHashMapUnmanaged(ErasedComponentStorage),
pub fn deinit(storage: *ArchetypeStorage) void {
for (storage.components.values()) |erased| {
erased.deinit(erased.ptr, storage.allocator);
}
storage.entity_ids.deinit(storage.allocator);
storage.components.deinit(storage.allocator);
}
/// New reserves a row for storing an entity within this archetype table.
pub fn new(storage: *ArchetypeStorage, entity: EntityID) !u32 {
// Return a new row index
const new_row_index = storage.entity_ids.items.len;
try storage.entity_ids.append(storage.allocator, entity);
return @intCast(u32, new_row_index);
}
/// Undoes the last call to the new() operation, effectively unreserving the row that was last
/// reserved.
pub fn undoNew(storage: *ArchetypeStorage) void {
_ = storage.entity_ids.pop();
}
};
pub const Entities = struct {
allocator: Allocator,
/// TODO!
counter: EntityID = 0,
/// A mapping of entity IDs (array indices) to where an entity's component values are actually
/// stored.
entities: std.AutoHashMapUnmanaged(EntityID, Pointer) = .{},
/// A mapping of archetype hash to their storage.
///
/// Database equivalent: table name -> tables representing entities.
archetypes: std.AutoArrayHashMapUnmanaged(u64, ArchetypeStorage) = .{},
/// Points to where an entity is stored, specifically in which archetype table and in which row
/// of that table. That is, the entity's component values are stored at:
///
/// ```
/// Entities.archetypes[ptr.archetype_index].rows[ptr.row_index]
/// ```
///
pub const Pointer = struct {
archetype_index: u16,
row_index: u32,
};
pub fn init(allocator: Allocator) !Entities {
var entities = Entities{ .allocator = allocator };
try entities.archetypes.put(allocator, void_archetype_hash, ArchetypeStorage{
.allocator = allocator,
.components = .{},
.hash = void_archetype_hash,
});
return entities;
}
pub fn deinit(entities: *Entities) void {
entities.entities.deinit(entities.allocator);
var iter = entities.archetypes.iterator();
while (iter.next()) |entry| {
entry.value_ptr.deinit();
}
entities.archetypes.deinit(entities.allocator);
}
pub fn initErasedStorage(entities: *const Entities, total_rows: *usize, comptime Component: type) !ErasedComponentStorage {
var new_ptr = try entities.allocator.create(ComponentStorage(Component));
new_ptr.* = ComponentStorage(Component){ .total_rows = total_rows };
return ErasedComponentStorage{
.ptr = new_ptr,
.deinit = (struct {
pub fn deinit(erased: *anyopaque, allocator: Allocator) void {
var ptr = ErasedComponentStorage.cast(erased, Component);
ptr.deinit(allocator);
allocator.destroy(ptr);
}
}).deinit,
};
}
/// Returns a new entity.
pub fn new(entities: *Entities) !EntityID {
const new_id = entities.counter;
entities.counter += 1;
var void_archetype = entities.archetypes.getPtr(void_archetype_hash).?;
const new_row = try void_archetype.new(new_id);
const void_pointer = Pointer{
.archetype_index = 0, // void archetype is guaranteed to be first index
.row_index = new_row,
};
entities.entities.put(entities.allocator, new_id, void_pointer) catch |err| {
void_archetype.undoNew();
return err;
};
return new_id;
}
};
test "ecs" {
const allocator = testing.allocator;
var world = try Entities.init(allocator);
defer world.deinit();
const player = try world.new();
_ = player;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment