diff --git a/build.zig b/build.zig index e871dfc..44cd699 100644 --- a/build.zig +++ b/build.zig @@ -6,13 +6,12 @@ pub fn build(b: *Builder) void { lib.setBuildMode(mode); lib.install(); - var component_store_test = b.addTest("src/component_store.zig"); - component_store_test.setBuildMode(mode); - - var entity_manager_test = b.addTest("src/entity_manager.zig"); + var entity_manager_test = b.addTest("src/encompass.zig"); entity_manager_test.setBuildMode(mode); + var component_manager_test = b.addTest("src/component_manager.zig"); + component_manager_test.setBuildMode(mode); + const test_step = b.step("test", "Run library tests"); - test_step.dependOn(&component_store_test.step); test_step.dependOn(&entity_manager_test.step); } diff --git a/src/component_manager.zig b/src/component_manager.zig new file mode 100644 index 0000000..9bde171 --- /dev/null +++ b/src/component_manager.zig @@ -0,0 +1,134 @@ +const ComponentStore = @import("component_store.zig").ComponentStore; +const TypeStore = @import("type_store.zig").TypeStore; +const Entity = @import("entity.zig").Entity; + +const std = @import("std"); +const mem = std.mem; +const testing = std.testing; +const Allocator = mem.Allocator; + +pub const ComponentManager = struct { + const Self = @This(); + + allocator: *Allocator, + component_stores: TypeStore, + + pub fn init(allocator: *Allocator) Self { + return Self { + .allocator = allocator, + .component_stores = TypeStore.init(allocator), + }; + } + + pub fn deinit(self: *Self) void { + self.component_stores.deinit(); + } + + pub fn register(self: *Self, comptime TComponent: type) void { + self.component_stores.add(ComponentStore(TComponent).init(self.allocator)); + } + + pub fn add_component(self: *Self, entityID: usize, component: anytype) void { + self.component_stores.get(ComponentStore(@TypeOf(component))).*.set(entityID, component); + } + + pub fn get_component(self: *Self, comptime TComponent: type, entityID: usize) !TComponent { + return self.component_stores.get(ComponentStore(TComponent)).*.get(entityID) catch |err| return err; + } + + pub fn read_components(self: *Self, comptime TComponent: type) []TComponent { + return self.component_stores.get(ComponentStore(TComponent)).*.all_components(); + } + + pub fn read_entities(self: *Self, comptime TComponent: type) []Entity { + return self.component_stores.get(ComponentStore(TComponent)).*.all_entities(); + } +}; + +const TestComponent = struct { + number: i32, +}; + +test "add component" { + const component = TestComponent { + .number = 3 + }; + + var component_manager = ComponentManager.init(std.testing.allocator); + defer component_manager.deinit(); + + component_manager.register(TestComponent); + + component_manager.add_component(2, component); + var expectedComponent = try component_manager.get_component(TestComponent, 2); + expectedComponent.number = 5; // should be immutable! + + expectedComponent = try component_manager.get_component(TestComponent, 2); + testing.expect(expectedComponent.number == 3); +} + +test "read components" { + const component = TestComponent { + .number = 3 + }; + + const componentTwo = TestComponent { + .number = 8 + }; + + const componentThree = TestComponent { + .number = 14 + }; + + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + + const allocator = &arena.allocator; + + var component_manager = ComponentManager.init(allocator); + + component_manager.register(TestComponent); + + component_manager.add_component(2, component); + component_manager.add_component(5, componentTwo); + component_manager.add_component(52, componentThree); + + var components = component_manager.read_components(TestComponent); + + testing.expect(components[0].number == 3); + testing.expect(components[1].number == 8); + testing.expect(components[2].number == 14); +} + +test "read entities" { + const component = TestComponent { + .number = 3 + }; + + const componentTwo = TestComponent { + .number = 8 + }; + + const componentThree = TestComponent { + .number = 14 + }; + + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + + const allocator = &arena.allocator; + + var component_manager = ComponentManager.init(allocator); + + component_manager.register(TestComponent); + + component_manager.add_component(2, component); + component_manager.add_component(5, componentTwo); + component_manager.add_component(52, componentThree); + + var entities = component_manager.read_entities(TestComponent); + + testing.expect(entities[0].ID == 2); + testing.expect(entities[1].ID == 5); + testing.expect(entities[2].ID == 52); +} diff --git a/src/component_store.zig b/src/component_store.zig index 7cd57d0..50b026f 100644 --- a/src/component_store.zig +++ b/src/component_store.zig @@ -1,3 +1,5 @@ +const Entity = @import("entity.zig").Entity; + const std = @import("std"); const mem = std.mem; const testing = std.testing; @@ -10,21 +12,21 @@ pub fn ComponentStore(comptime TComponent: type) type { nextIndex: usize = 0, allocator: *Allocator, entityIDToIndex: std.AutoArrayHashMap(usize, usize), - indexToEntityID: std.ArrayList(usize), + indexToEntity: std.ArrayList(Entity), components: std.ArrayList(TComponent), pub fn init(allocator: *Allocator) Self { return Self { .allocator = allocator, .entityIDToIndex = std.AutoArrayHashMap(usize, usize).init(allocator), - .indexToEntityID = std.ArrayList(usize).init(allocator), + .indexToEntity = std.ArrayList(Entity).init(allocator), .components = std.ArrayList(TComponent).init(allocator), }; } pub fn deinit(self: *Self) void { self.entityIDToIndex.deinit(); - self.indexToEntityID.deinit(); + self.indexToEntity.deinit(); self.components.deinit(); } @@ -40,7 +42,7 @@ pub fn ComponentStore(comptime TComponent: type) type { // FIXME: actually handle OOM self.components.append(component) catch unreachable; self.entityIDToIndex.put(entityID, index) catch unreachable; - self.indexToEntityID.append(entityID) catch unreachable; + self.indexToEntity.append(Entity { .ID = entityID }) catch unreachable; } else { @@ -60,12 +62,12 @@ pub fn ComponentStore(comptime TComponent: type) type { pub fn remove(self: *Self, entityID: usize) void { if (self.entityIDToIndex.contains(entityID)) { var storageIndex = self.entityIDToIndex.get(entityID).?; - var lastEntity = self.indexToEntityID.items[self.nextIndex - 1]; + var lastEntity = self.indexToEntity.items[self.nextIndex - 1]; - self.entityIDToIndex.put(lastEntity, storageIndex) catch unreachable; + self.entityIDToIndex.put(lastEntity.ID, storageIndex) catch unreachable; var removedComponent = self.components.swapRemove(storageIndex); - var removedEntityID = self.indexToEntityID.swapRemove(storageIndex); + var removedEntityID = self.indexToEntity.swapRemove(storageIndex); var removedEntry = self.entityIDToIndex.remove(entityID); self.nextIndex -= 1; @@ -76,8 +78,8 @@ pub fn ComponentStore(comptime TComponent: type) type { return self.components.items; } - pub fn all_entities(self: Self) []usize { - return self.indexToEntityID.items(); + pub fn all_entities(self: Self) []Entity { + return self.indexToEntity.items; } }; } diff --git a/src/encompass.zig b/src/encompass.zig index dcd593f..b3d13fa 100644 --- a/src/encompass.zig +++ b/src/encompass.zig @@ -1,2 +1,9 @@ -pub const Entity = @import("entity.zig").Entity; -pub const ComponentStore = @import("component_store.zig").ComponentStore; +pub const EntityManager = @import("entity_manager.zig").EntityManager; +pub const ComponentManager = @import("component_manager.zig").ComponentManager; + +const std = @import("std"); +const testing = std.testing; + +test "" { + testing.refAllDecls(@This()); +} diff --git a/src/type_store.zig b/src/type_store.zig new file mode 100644 index 0000000..0688a8b --- /dev/null +++ b/src/type_store.zig @@ -0,0 +1,105 @@ +// adapted from https://github.com/prime31/zig-ecs/blob/master/src/ecs/utils.zig + +const std = @import("std"); + +/// stores a single object of type T for each T added +pub const TypeStore = struct { + map: std.AutoHashMap(u32, []u8), + allocator: *std.mem.Allocator, + + pub fn init(allocator: *std.mem.Allocator) TypeStore { + return TypeStore{ + .map = std.AutoHashMap(u32, []u8).init(allocator), + .allocator = allocator, + }; + } + + pub fn deinit(self: *TypeStore) void { + var iter = self.map.iterator(); + while (iter.next()) |kv| { + self.allocator.free(kv.value); + } + self.map.deinit(); + } + + /// adds instance, returning a pointer to the item as it lives in the store + pub fn add(self: *TypeStore, instance: anytype) void { + var bytes = self.allocator.alloc(u8, @sizeOf(@TypeOf(instance))) catch unreachable; + std.mem.copy(u8, bytes, std.mem.asBytes(&instance)); + _ = self.map.put(typeId(@TypeOf(instance)), bytes) catch unreachable; + } + + pub fn get(self: *TypeStore, comptime T: type) *T { + if (self.map.get(typeId(T))) |bytes| { + return @ptrCast(*T, @alignCast(@alignOf(T), bytes)); + } + unreachable; + } + + pub fn getConst(self: *TypeStore, comptime T: type) T { + return self.get(T).*; + } + + pub fn getOrAdd(self: *TypeStore, comptime T: type) *T { + if (!self.has(T)) { + var instance = std.mem.zeroes(T); + self.add(instance); + } + return self.get(T); + } + + pub fn remove(self: *TypeStore, comptime T: type) void { + if (self.map.get(typeId(T))) |bytes| { + self.allocator.free(bytes); + _ = self.map.remove(typeId(T)); + } + } + + pub fn has(self: *TypeStore, comptime T: type) bool { + return self.map.contains(typeId(T)); + } + + fn typeId(comptime T: type) u32 { + comptime return hashStringFnv(u32, @typeName(T)); + } + + fn hashStringFnv(comptime ReturnType: type, comptime str: []const u8) ReturnType { + std.debug.assert(ReturnType == u32 or ReturnType == u64); + + const prime = if (ReturnType == u32) @as(u32, 16777619) else @as(u64, 1099511628211); + var value = if (ReturnType == u32) @as(u32, 2166136261) else @as(u64, 14695981039346656037); + for (str) |c| { + value = (value ^ @intCast(u32, c)) *% prime; + } + return value; + } +}; + +test "TypeStore" { + const Vector = struct { x: f32 = 0, y: f32 = 0, z: f32 = 0 }; + + var store = TypeStore.init(std.testing.allocator); + defer store.deinit(); + + var orig = Vector{ .x = 5, .y = 6, .z = 8 }; + store.add(orig); + std.testing.expect(store.has(Vector)); + std.testing.expectEqual(store.get(Vector).*, orig); + + var v = store.get(Vector); + std.testing.expectEqual(v.*, Vector{ .x = 5, .y = 6, .z = 8 }); + v.*.x = 666; + + var v2 = store.get(Vector); + std.testing.expectEqual(v2.*, Vector{ .x = 666, .y = 6, .z = 8 }); + + store.remove(Vector); + std.testing.expect(!store.has(Vector)); + + var v3 = store.getOrAdd(u32); + std.testing.expectEqual(v3.*, 0); + v3.* = 777; + + var v4 = store.get(u32); + std.testing.expectEqual(v3.*, 777); +}