commit 5589130bfdaca7ee8a21edf4153cdb9f343c25a4 Author: Mike Date: Sun May 31 21:28:29 2020 -0700 initial commit diff --git a/zig-ecs/.gitignore b/zig-ecs/.gitignore new file mode 100644 index 0000000..2040c29 --- /dev/null +++ b/zig-ecs/.gitignore @@ -0,0 +1 @@ +zig-cache diff --git a/zig-ecs/build.zig b/zig-ecs/build.zig new file mode 100644 index 0000000..d182520 --- /dev/null +++ b/zig-ecs/build.zig @@ -0,0 +1,72 @@ +const Builder = @import("std").build.Builder; +const builtin = @import("builtin"); + +pub fn build(b: *Builder) void { + const buildMode = b.standardReleaseOptions(); + + const examples = [_][2][]const u8{ + [_][]const u8{ "simple", "examples/simple.zig" }, + // [_][]const u8{ "mesh", "examples/mesh.zig" }, + }; + + for (examples) |example, i| { + const name = example[0]; + const source = example[1]; + + var exe = b.addExecutable(name, source); + exe.setBuildMode(b.standardReleaseOptions()); + exe.addPackagePath("ecs", "src/ecs.zig"); + + const run_cmd = exe.run(); + const exe_step = b.step(name, b.fmt("run {}.zig", .{name})); + exe_step.dependOn(&run_cmd.step); + + // first element in the list is added as "run" so "zig build run" works + if (i == 0) { + const run_exe_step = b.step("run", b.fmt("run {}.zig", .{name})); + run_exe_step.dependOn(&run_cmd.step); + } + } + + // internal tests + const internal_test_step = b.addTest("src/tests.zig"); + internal_test_step.setBuildMode(buildMode); + + // public api tests + const test_step = b.addTest("tests/tests.zig"); + test_step.addPackagePath("ecs", "src/ecs.zig"); + test_step.setBuildMode(buildMode); + + const test_cmd = b.step("test", "Run the tests"); + test_cmd.dependOn(&internal_test_step.step); + test_cmd.dependOn(&test_step.step); +} + +pub const LibType = enum(i32) { + static, + dynamic, // requires DYLD_LIBRARY_PATH to point to the dylib path + exe_compiled, +}; + +/// rel_path is used to add package paths. It should be the the same path used to include this build file +pub fn linkArtifact(b: *Builder, artifact: *std.build.LibExeObjStep, target: std.build.Target, lib_type: LibType, rel_path: []const u8) void { + switch (lib_type) { + .static => { + const lib = b.addStaticLibrary("ecs", "ecs.zig"); + lib.setBuildMode(buildMode); + lib.install(); + + artifact.linkLibrary(lib); + }, + .dynamic => { + const lib = b.addSharedLibrary("ecs", "ecs.zig", null); + lib.setBuildMode(buildMode); + lib.install(); + + artifact.linkLibrary(lib); + }, + else => {}, + } + + artifact.addPackagePath("ecs", std.fs.path.join(b.allocator, &[_][]const u8{ rel_path, "ecs.zig" }) catch unreachable); +} \ No newline at end of file diff --git a/zig-ecs/examples/simple.zig b/zig-ecs/examples/simple.zig new file mode 100644 index 0000000..07b488e --- /dev/null +++ b/zig-ecs/examples/simple.zig @@ -0,0 +1,41 @@ +const std = @import("std"); +const ecs = @import("ecs"); + +// override the EntityTraits used by ecs +pub const EntityTraits = ecs.EntityTraitsType(.small); + +pub const Velocity = struct { x: f32, y: f32 }; +pub const Position = struct { x: f32, y: f32 }; + +pub fn main() !void { + var reg = ecs.Registry.init(std.testing.allocator); + defer reg.deinit(); + + var e1 = reg.create(); + reg.add(e1, Position{ .x = 0, .y = 0 }); + reg.add(e1, Velocity{ .x = 5, .y = 7 }); + + var e2 = reg.create(); + reg.add(e2, Position{ .x = 10, .y = 10 }); + reg.add(e2, Velocity{ .x = 15, .y = 17 }); + + var view = reg.view(.{Velocity, Position}); + + var iter = view.iterator(); + while (iter.next()) |entity| { + var pos = view.get(Position, entity); + const vel = view.getConst(Velocity, entity); + std.debug.warn("entity: {}, pos: {d}, vel: {d}\n", .{entity, pos.*, vel}); + pos.*.x += vel.x; + pos.*.y += vel.y; + } + + std.debug.warn("---- resetting iter\n", .{}); + + iter.reset(); + while (iter.next()) |entity| { + const pos = view.getConst(Position, entity); + const vel = view.getConst(Velocity, entity); + std.debug.warn("entity: {}, pos: {d}, vel: {d}\n", .{entity, pos, vel}); + } +} \ No newline at end of file diff --git a/zig-ecs/src/ecs.zig b/zig-ecs/src/ecs.zig new file mode 100644 index 0000000..24c2edb --- /dev/null +++ b/zig-ecs/src/ecs.zig @@ -0,0 +1,11 @@ +// ecs +pub const EntityTraitsType = @import("ecs/entity.zig").EntityTraitsType; + +pub const Entity = @import("ecs/registry.zig").Entity; +pub const Registry = @import("ecs/registry.zig").Registry; +pub const BasicView = @import("ecs/view.zig").BasicView; +pub const BasicMultiView = @import("ecs/view.zig").BasicMultiView; + +// signals +pub const Signal = @import("signals/signal.zig").Signal; +pub const Dispatcher = @import("signals/dispatcher.zig").Dispatcher; \ No newline at end of file diff --git a/zig-ecs/src/ecs/actor.zig b/zig-ecs/src/ecs/actor.zig new file mode 100644 index 0000000..b570b80 --- /dev/null +++ b/zig-ecs/src/ecs/actor.zig @@ -0,0 +1,88 @@ +const std = @import("std"); +const Registry = @import("registry.zig").Registry; +const Entity = @import("registry.zig").Entity; + +pub const Actor = struct { + registry: *Registry, + entity: Entity = undefined, + + pub fn init(registry: *Registry) Actor { + var reg = registry; + return .{ + .registry = registry, + .entity = reg.create(), + }; + } + + pub fn deinit(self: *Actor) void { + self.registry.destroy(self.entity); + } + + pub fn add(self: *Actor, value: var) void { + self.registry.add(self.entity, value); + } + + pub fn addTyped(self: *Actor, comptime T: type, value: T) void { + self.registry.addTyped(T, self.entity, value); + } + + pub fn remove(self: *Actor, comptime T: type) void { + self.registry.remove(T, self.entity); + } + + pub fn has(self: *Actor, comptime T: type) bool { + return self.registry.has(T, self.entity); + } + + pub fn get(self: *Actor, comptime T: type) *T { + return self.registry.get(T, self.entity); + } + + pub fn tryGet(self: *Actor, comptime T: type) ?*T { + return self.registry.tryGet(T, self.entity); + } +}; + +test "actor" { + var reg = Registry.init(std.testing.allocator); + defer reg.deinit(); + + var actor = Actor.init(®); + defer actor.deinit(); + + std.debug.assert(!actor.has(f32)); + actor.addTyped(f32, 67.45); + if (actor.tryGet(f32)) |val| { + std.testing.expectEqual(val.*, 67.45); + } + + actor.addTyped(u64, 8888); + std.testing.expectEqual(actor.get(u64).*, 8888); + std.debug.assert(actor.has(u64)); + + actor.remove(u64); + std.debug.assert(!actor.has(u64)); +} + +test "actor structs" { + const Velocity = struct { x: f32, y: f32 }; + const Position = struct { x: f32 = 0, y: f32 = 0 }; + + var reg = Registry.init(std.testing.allocator); + defer reg.deinit(); + + var actor = Actor.init(®); + defer actor.deinit(); + + actor.add(Velocity{ .x = 5, .y = 10 }); + actor.add(Position{}); + + var vel = actor.get(Velocity); + var pos = actor.get(Position); + + pos.*.x += vel.x; + pos.*.y += vel.y; + + std.testing.expectEqual(actor.get(Position).*.x, 5); + std.testing.expectEqual(actor.get(Position).*.y, 10); +} diff --git a/zig-ecs/src/ecs/component_storage.zig b/zig-ecs/src/ecs/component_storage.zig new file mode 100644 index 0000000..773d554 --- /dev/null +++ b/zig-ecs/src/ecs/component_storage.zig @@ -0,0 +1,213 @@ +const std = @import("std"); +const warn = std.debug.warn; +const utils = @import("utils.zig"); + +const SparseSet = @import("sparse_set.zig").SparseSet; + +pub fn ComponentStorage(comptime CompT: type, comptime EntityT: type, comptime DenseT: type) type { + std.debug.assert(!utils.isComptime(CompT)); + + // empty (zero-sized) structs will not have an array created + comptime const is_empty_struct = @sizeOf(CompT) == 0; + + // HACK: due to this being stored as untyped ptrs, when deinit is called we are casted to a CompT of some random + // non-zero sized type. That will make is_empty_struct false in deinit always so we can't use it. Instead, we stick + // a small dummy struct in the instances ArrayList so it can safely be deallocated. + // Perhaps we should just allocate instances with a dummy allocator or the tmp allocator? + comptime var CompOrAlmostEmptyT = CompT; + if (is_empty_struct) + CompOrAlmostEmptyT = struct { dummy: u1 }; + + return struct { + const Self = @This(); + + set: *SparseSet(EntityT, DenseT), + instances: std.ArrayList(CompOrAlmostEmptyT), + allocator: ?*std.mem.Allocator, + safe_deinit: fn (*Self) void, + + pub fn init(allocator: *std.mem.Allocator) Self { + var store = Self{ + .set = SparseSet(EntityT, DenseT).init(allocator), + .instances = undefined, + .safe_deinit = struct { + fn deinit(self: *Self) void { + std.debug.warn("------ (inner) T: {}, size: {}, is_empty_struct: {}\n", .{ @typeName(@TypeOf(self)), @sizeOf(CompT), is_empty_struct }); + if (!is_empty_struct) + self.instances.deinit(); + } + }.deinit, + .allocator = null, + }; + + if (!is_empty_struct) + store.instances = std.ArrayList(CompOrAlmostEmptyT).init(allocator); + + return store; + } + + pub fn initPtr(allocator: *std.mem.Allocator) *Self { + var store = allocator.create(Self) catch unreachable; + store.set = SparseSet(EntityT, DenseT).init(allocator); + if (!is_empty_struct) + store.instances = std.ArrayList(CompOrAlmostEmptyT).init(allocator); + store.allocator = allocator; + + // since we are stored as a pointer, we need to catpure this + store.safe_deinit = struct { + fn deinit(self: *Self) void { + std.debug.warn("------ (inner) T: {}, size: {}, is_empty_struct: {}\n", .{ @typeName(@TypeOf(self)), @sizeOf(CompT), is_empty_struct }); + if (!is_empty_struct) + self.instances.deinit(); + } + }.deinit; + + return store; + } + + pub fn deinit(self: *Self) void { + // great care must be taken here. Due to how Registry keeps this struct as pointers anything touching a type + // will be wrong since it has to cast to a random struct when deiniting. Because of all that, is_empty_struct + // will allways be false here so we have to deinit the instances no matter what. + std.debug.warn("\n------ (deinit) T: {}, size: {}, is_empty_struct: {}\n", .{ @typeName(@TypeOf(self)), @sizeOf(CompT), is_empty_struct }); + self.safe_deinit(self); + self.set.deinit(); + + if (self.allocator) |allocator| + allocator.destroy(self); + } + + /// Increases the capacity of a component storage + pub fn reserve(self: *Self, cap: usize) void { + self.set.reserve(cap); + if (!is_empty_struct) + self.instances.items.reserve(cap); + } + + /// Assigns an entity to a storage and constructs its object + pub fn add(self: *Self, entity: EntityT, value: CompT) void { + if (!is_empty_struct) + _ = self.instances.append(value) catch unreachable; + self.set.add(entity); + } + + /// Checks if a view contains an entity + pub fn contains(self: Self, entity: EntityT) bool { + return self.set.contains(entity); + } + + pub fn len(self: Self) usize { + return self.set.len(); + } + + pub usingnamespace if (is_empty_struct) + struct {} + else + struct { + /// Direct access to the array of objects + pub fn raw(self: Self) []CompT { + return self.instances.items; + } + + /// Returns the object associated with an entity + pub fn get(self: *Self, entity: EntityT) *CompT { + std.debug.assert(self.contains(entity)); + return &self.instances.items[self.set.index(entity)]; + } + + pub fn getConst(self: *Self, entity: EntityT) CompT { + return self.instances.items[self.set.index(entity)]; + } + + /// Returns a pointer to the object associated with an entity, if any. + pub fn tryGet(self: *Self, entity: EntityT) ?*CompT { + return if (self.set.contains(entity)) &self.instances.items[self.set.index(entity)] else null; + } + + pub fn tryGetConst(self: *Self, entity: EntityT) ?CompT { + return if (self.set.contains(entity)) self.instances.items[self.set.index(entity)] else null; + } + }; + + /// Direct access to the array of entities + pub fn data(self: Self) *const []EntityT { + return self.set.data(); + } + + /// Removes an entity from a storage + pub fn remove(self: *Self, entity: EntityT) void { + if (!is_empty_struct) + _ = self.instances.swapRemove(self.set.index(entity)); + self.set.remove(entity); + } + + /// Swaps entities and objects in the internal packed arrays + pub fn swap(self: *Self, lhs: EntityT, rhs: EntityT) void { + if (!is_empty_struct) + std.mem.swap(CompT, &self.instances[self.set.index(lhs)], &self.instances[self.set.index(rhs)]); + self.set.swap(lhs, rhs); + } + + pub fn clear(self: *Self) void { + if (!is_empty_struct) + self.instances.items.len = 0; + self.set.clear(); + } + }; +} + +test "add/try-get/remove/clear" { + var store = ComponentStorage(f32, u32, u8).init(std.testing.allocator); + defer store.deinit(); + + store.add(3, 66.45); + std.testing.expectEqual(store.tryGetConst(3).?, 66.45); + if (store.tryGet(3)) |found| std.testing.expectEqual(@as(f32, 66.45), found.*); + + store.remove(3); + + var val_null = store.tryGet(3); + std.testing.expectEqual(val_null, null); + + store.clear(); +} + +test "add/get/remove" { + var store = ComponentStorage(f32, u32, u8).init(std.testing.allocator); + defer store.deinit(); + + store.add(3, 66.45); + if (store.tryGet(3)) |found| std.testing.expectEqual(@as(f32, 66.45), found.*); + std.testing.expectEqual(store.tryGetConst(3).?, 66.45); + + store.remove(3); + std.testing.expectEqual(store.tryGet(3), null); +} + +test "iterate" { + var store = ComponentStorage(f32, u32, u8).initPtr(std.testing.allocator); + defer store.deinit(); + + store.add(3, 66.45); + store.add(5, 66.45); + store.add(7, 66.45); + + for (store.data().*) |entity, i| { + if (i == 0) + std.testing.expectEqual(entity, 3); + if (i == 1) + std.testing.expectEqual(entity, 5); + if (i == 2) + std.testing.expectEqual(entity, 7); + } +} + +test "empty component" { + const Empty = struct {}; + + var store = ComponentStorage(Empty, u32, u8).initPtr(std.testing.allocator); + defer store.deinit(); + + store.add(3, Empty{}); + store.remove(3); +} diff --git a/zig-ecs/src/ecs/entity.zig b/zig-ecs/src/ecs/entity.zig new file mode 100644 index 0000000..c8f505a --- /dev/null +++ b/zig-ecs/src/ecs/entity.zig @@ -0,0 +1,47 @@ +const std = @import("std"); + +/// default EntityTraitsDefinition with reasonable sizes suitable for most situations +pub const EntityTraits = EntityTraitsType(.medium); + +pub const EntityTraitsSize = enum { small, medium, large }; + +pub fn EntityTraitsType(comptime size: EntityTraitsSize) type { + return switch (size) { + .small => EntityTraitsDefinition(u16, u12, u4), + .medium => EntityTraitsDefinition(u32, u20, u12), + .large => EntityTraitsDefinition(u64, u32, u32), + }; +} + +fn EntityTraitsDefinition(comptime EntityType: type, comptime IndexType: type, comptime VersionType: type) type { + std.debug.assert(@typeInfo(EntityType) == .Int and !EntityType.is_signed); + std.debug.assert(@typeInfo(IndexType) == .Int and !IndexType.is_signed); + std.debug.assert(@typeInfo(VersionType) == .Int and !VersionType.is_signed); + + if (@bitSizeOf(IndexType) + @bitSizeOf(VersionType) != @bitSizeOf(EntityType)) + @compileError("IndexType and VersionType must sum to EntityType's bit count"); + + return struct { + entity_type: type = EntityType, + index_type: type = IndexType, + version_type: type = VersionType, + /// Mask to use to get the entity index number out of an identifier + entity_mask: EntityType = std.math.maxInt(IndexType), + /// Mask to use to get the version out of an identifier + version_mask: EntityType = std.math.maxInt(VersionType), + + pub fn init() @This() { + return @This(){}; + } + }; +} + +test "entity traits" { + const sm = EntityTraitsType(.small).init(); + const m = EntityTraitsType(.medium).init(); + const l = EntityTraitsType(.large).init(); + + std.testing.expectEqual(sm.entity_mask, std.math.maxInt(sm.index_type)); + std.testing.expectEqual(m.entity_mask, std.math.maxInt(m.index_type)); + std.testing.expectEqual(l.entity_mask, std.math.maxInt(l.index_type)); +} diff --git a/zig-ecs/src/ecs/handles.zig b/zig-ecs/src/ecs/handles.zig new file mode 100644 index 0000000..cbf71d1 --- /dev/null +++ b/zig-ecs/src/ecs/handles.zig @@ -0,0 +1,135 @@ +const std = @import("std"); + +/// generates versioned "handles" (https://floooh.github.io/2018/06/17/handles-vs-pointers.html) +/// you choose the type of the handle (aka its size) and how much of that goes to the index and the version. +/// the bitsize of version + id must equal the handle size. +pub fn Handles(comptime HandleType: type, comptime IndexType: type, comptime VersionType: type) type { + std.debug.assert(@typeInfo(HandleType) == .Int and !HandleType.is_signed); + std.debug.assert(@typeInfo(IndexType) == .Int and !IndexType.is_signed); + std.debug.assert(@typeInfo(VersionType) == .Int and !VersionType.is_signed); + + if (@bitSizeOf(IndexType) + @bitSizeOf(VersionType) != @bitSizeOf(HandleType)) + @compileError("IndexType and VersionType must sum to HandleType's bit count"); + + return struct { + const Self = @This(); + + handles: []HandleType, + append_cursor: IndexType = 0, + last_destroyed: ?IndexType = null, + allocator: *std.mem.Allocator, + + const invalid_id = std.math.maxInt(IndexType); + + pub fn init(allocator: *std.mem.Allocator) Self { + return initWithCapacity(allocator, 32); + } + + pub fn initWithCapacity(allocator: *std.mem.Allocator, capacity: usize) Self { + return Self{ + .handles = allocator.alloc(HandleType, capacity) catch unreachable, + .allocator = allocator, + }; + } + + pub fn deinit(self: Self) void { + self.allocator.free(self.handles); + } + + pub fn extractId(self: Self, handle: HandleType) IndexType { + return @truncate(IndexType, handle); + } + + pub fn extractVersion(self: Self, handle: HandleType) VersionType { + return @truncate(VersionType, handle >> @bitSizeOf(IndexType)); + } + + fn forge(id: IndexType, version: VersionType) HandleType { + return id | @as(HandleType, version) << @bitSizeOf(IndexType); + } + + pub fn create(self: *Self) HandleType { + if (self.last_destroyed == null) { + // ensure capacity and grow if needed + if (self.handles.len - 1 == self.append_cursor) { + self.handles = self.allocator.realloc(self.handles, self.handles.len * 2) catch unreachable; + } + + const id = self.append_cursor; + const handle = forge(self.append_cursor, 0); + self.handles[id] = handle; + + self.append_cursor += 1; + return handle; + } + + const version = self.extractVersion(self.handles[self.last_destroyed.?]); + const destroyed_id = self.extractId(self.handles[self.last_destroyed.?]); + + const handle = forge(self.last_destroyed.?, version); + self.handles[self.last_destroyed.?] = handle; + + self.last_destroyed = if (destroyed_id == invalid_id) null else destroyed_id; + + return handle; + } + + pub fn remove(self: *Self, handle: HandleType) !void { + const id = self.extractId(handle); + if (id > self.append_cursor or self.handles[id] != handle) + return error.RemovedInvalidHandle; + + const next_id = self.last_destroyed orelse invalid_id; + if (next_id == id) return error.ExhaustedEntityRemoval; + + const version = self.extractVersion(handle); + self.handles[id] = forge(next_id, version +% 1); + + self.last_destroyed = id; + } + + pub fn isAlive(self: Self, handle: HandleType) bool { + const id = self.extractId(handle); + return id < self.append_cursor and self.handles[id] == handle; + } + }; +} + +test "handles" { + var hm = Handles(u32, u20, u12).init(std.testing.allocator); + defer hm.deinit(); + + const e0 = hm.create(); + const e1 = hm.create(); + const e2 = hm.create(); + + std.debug.assert(hm.isAlive(e0)); + std.debug.assert(hm.isAlive(e1)); + std.debug.assert(hm.isAlive(e2)); + + hm.remove(e1) catch unreachable; + std.debug.assert(!hm.isAlive(e1)); + + std.testing.expectError(error.RemovedInvalidHandle, hm.remove(e1)); + + var e_tmp = hm.create(); + std.debug.assert(hm.isAlive(e_tmp)); + + hm.remove(e_tmp) catch unreachable; + std.debug.assert(!hm.isAlive(e_tmp)); + + hm.remove(e0) catch unreachable; + std.debug.assert(!hm.isAlive(e0)); + + hm.remove(e2) catch unreachable; + std.debug.assert(!hm.isAlive(e2)); + + e_tmp = hm.create(); + std.debug.assert(hm.isAlive(e_tmp)); + + e_tmp = hm.create(); + std.debug.assert(hm.isAlive(e_tmp)); + + e_tmp = hm.create(); + std.debug.assert(hm.isAlive(e_tmp)); +} diff --git a/zig-ecs/src/ecs/registry.zig b/zig-ecs/src/ecs/registry.zig new file mode 100644 index 0000000..0201e13 --- /dev/null +++ b/zig-ecs/src/ecs/registry.zig @@ -0,0 +1,306 @@ +const std = @import("std"); +const assert = std.debug.assert; +const utils = @import("utils.zig"); + +const Handles = @import("handles.zig").Handles; +const SparseSet = @import("sparse_set.zig").SparseSet; +const TypeMap = @import("type_map.zig").TypeMap; +const ComponentStorage = @import("component_storage.zig").ComponentStorage; + +// allow overriding EntityTraits by setting in root via: EntityTraits = EntityTraitsType(.medium); +const root = @import("root"); +const entity_traits = if (@hasDecl(root, "EntityTraits")) root.EntityTraits.init() else @import("entity.zig").EntityTraits.init(); + +// setup the Handles type based on the type set in EntityTraits +const EntityHandles = Handles(entity_traits.entity_type, entity_traits.index_type, entity_traits.version_type); +pub const Entity = entity_traits.entity_type; + +pub const BasicView = @import("view.zig").BasicView; +pub const BasicMultiView = @import("view.zig").BasicMultiView; + +/// Stores an ArrayList of components. The max amount that can be stored is based on the type below +pub fn Storage(comptime CompT: type) type { + return ComponentStorage(CompT, Entity, u16); // 65,535 components +} + +/// the registry is the main gateway to all ecs functionality. It assumes all internal allocations will succeed and returns +/// no errors to keep the API clean and because if a component array cant be allocated you've got bigger problems. +/// Stores a maximum of u8 (256) component Storage(T). +pub const Registry = struct { + typemap: TypeMap, + handles: EntityHandles, + components: std.AutoHashMap(u8, usize), + component_contexts: std.AutoHashMap(u8, usize), + context: usize = 0, + allocator: *std.mem.Allocator, + + pub fn init(allocator: *std.mem.Allocator) Registry { + return Registry{ + .typemap = TypeMap.init(allocator), + .handles = EntityHandles.init(allocator), + .components = std.AutoHashMap(u8, usize).init(allocator), + .component_contexts = std.AutoHashMap(u8, usize).init(allocator), + .allocator = allocator, + }; + } + + pub fn deinit(self: *Registry) void { + var it = self.components.iterator(); + while (it.next()) |ptr| { + // HACK: we dont know the Type here but we need to call deinit + var storage = @intToPtr(*Storage(u1), ptr.value); + storage.deinit(); + } + + self.components.deinit(); + self.component_contexts.deinit(); + self.typemap.deinit(); + self.handles.deinit(); + } + + pub fn assure(self: *Registry, comptime T: type) *Storage(T) { + var type_id: u8 = undefined; + if (!self.typemap.getOrPut(T, &type_id)) { + var comp_set = Storage(T).initPtr(self.allocator); + var comp_set_ptr = @ptrToInt(comp_set); + _ = self.components.put(type_id, comp_set_ptr) catch unreachable; + return comp_set; + } + + const ptr = self.components.getValue(type_id).?; + return @intToPtr(*Storage(T), ptr); + } + + pub fn prepare(self: *Registry, comptime T: type) void { + unreachable; + } + + pub fn len(self: *Registry, comptime T: type) usize { + self.assure(T).len(); + } + + pub fn raw(self: Registry, comptime T: type) []T { + return self.assure(T).raw(); + } + + pub fn reserve(self: *Self, comptime T: type, cap: usize) void { + self.assure(T).reserve(cap); + } + + pub fn valid(self: *Registry, entity: Entity) bool { + return self.handles.isAlive(entity); + } + + /// Returns the entity identifier without the version + pub fn entityId(self: Registry, entity: Entity) Entity { + return entity & entity_traits.entity_mask; + } + + /// Returns the version stored along with an entity identifier + pub fn version(self: *Registry, entity: Entity) entity_traits.version_type { + return @truncate(entity_traits.version_type, entity >> @bitSizeOf(entity_traits.index_type)); + } + + /// Creates a new entity and returns it + pub fn create(self: *Registry) Entity { + return self.handles.create(); + } + + /// Destroys an entity + pub fn destroy(self: *Registry, entity: Entity) void { + assert(self.valid(entity)); + self.removeAll(entity); + self.handles.remove(entity) catch unreachable; + } + + pub fn add(self: *Registry, entity: Entity, value: var) void { + assert(self.valid(entity)); + self.assure(@TypeOf(value)).add(entity, value); + } + + /// shortcut for adding raw comptime_int/float without having to @as cast + pub fn addTyped(self: *Registry, comptime T: type, entity: Entity, value: T) void { + self.add(entity, value); + } + + pub fn replace(self: *Registry, entity: Entity, value: var) void { + assert(self.valid(entity)); + var ptr = self.assure(@TypeOf(value)).get(entity); + ptr.* = value; + } + + /// shortcut for replacing raw comptime_int/float without having to @as cast + pub fn replaceTyped(self: *Registry, comptime T: type, entity: Entity, value: T) void { + self.replace(entity, value); + } + + pub fn addOrReplace(self: *Registry, entity: Entity, value: var) void { + assert(self.valid(entity)); + + const store = self.assure(@TypeOf(value)); + if (store.tryGet(entity)) |found| { + found.* = value; + } else { + store.add(entity, value); + } + } + + /// shortcut for add-or-replace raw comptime_int/float without having to @as cast + pub fn addOrReplaceTyped(self: *Registry, T: type, entity: Entity, value: T) void { + self.addOrReplace(entity, value); + } + + /// Removes the given component from an entity + pub fn remove(self: *Registry, comptime T: type, entity: Entity) void { + assert(self.valid(entity)); + self.assure(T).remove(entity); + } + + pub fn removeIfExists(self: *Registry, comptime T: type, entity: Entity) void { + assert(self.valid(entity)); + var store = self.assure(T); + if (store.contains(entity)) + store.remove(entity); + } + + /// Removes all the components from an entity and makes it orphaned + pub fn removeAll(self: *Registry, entity: Entity) void { + assert(self.valid(entity)); + // unreachable; + } + + pub fn has(self: *Registry, comptime T: type, entity: Entity) bool { + assert(self.valid(entity)); + return self.assure(T).set.contains(entity); + } + + pub fn get(self: *Registry, comptime T: type, entity: Entity) *T { + assert(self.valid(entity)); + return self.assure(T).get(entity); + } + + pub fn getConst(self: *Registry, comptime T: type, entity: Entity) T { + assert(self.valid(entity)); + return self.assure(T).getConst(entity); + } + + /// Returns a reference to the given component for an entity + pub fn getOrAdd(self: *Registry, comptime T: type, entity: Entity) *T { + if (self.has(T, entity)) return self.get(T, entity); + self.add(T, entity, std.mem.zeros(T)); + return self.get(T, type); + } + + pub fn tryGet(self: *Registry, comptime T: type, entity: Entity) ?*T { + return self.assure(T).tryGet(entity); + } + + /// Binds an object to the context of the registry + pub fn setContext(self: *Registry, context: var) void { + std.debug.assert(@typeInfo(@TypeOf(context)) == .Pointer); + self.context = @ptrToInt(context); + } + + /// Unsets a context variable if it exists + pub fn unsetContext(self: *Registry) void { + self.context = 0; + } + + /// Returns a pointer to an object in the context of the registry + pub fn getContext(self: *Registry, comptime T: type) ?*T { + return if (self.context > 0) @intToPtr(*T, self.context) else null; + } + + /// Binds an object to the context of the Component type + pub fn setComponentContext(self: *Registry, comptime Component: type, context: var) void { + std.debug.assert(@typeInfo(@TypeOf(context)) == .Pointer); + + var type_id: u8 = undefined; + _ = self.typemap.getOrPut(Component, &type_id); + _ = self.component_contexts.put(type_id, @ptrToInt(context)) catch unreachable; + } + + /// Unsets a context variable associated with a Component type if it exists + pub fn unsetComponentContext(self: *Registry, comptime Component: type) void { + var type_id: u8 = undefined; + _ = self.typemap.getOrPut(Component, &type_id); + _ = self.component_contexts.put(type_id, 0) catch unreachable; + } + + /// Returns a pointer to an object in the context of the Component type + pub fn getComponentContext(self: *Registry, comptime Component: type, comptime T: type) ?*T { + var type_id: u8 = undefined; + _ = self.typemap.getOrPut(Component, &type_id); + return if (self.component_contexts.get(type_id)) |ptr| + return if (ptr.value > 0) @intToPtr(*T, ptr.value) else null + else + null; + } + + pub fn view(self: *Registry, comptime includes: var) ViewType(includes) { + std.debug.assert(includes.len > 0); + + if (includes.len == 1) + return BasicView(includes[0]).init(self.assure(includes[0])); + + var arr: [includes.len]u32 = undefined; + inline for (includes) |t, i| { + _ = self.assure(t); + arr[i] = @as(u32, self.typemap.get(t)); + } + + return BasicMultiView(includes.len).init(arr, self); + } + + fn ViewType(comptime includes: var) type { + if (includes.len == 1) return BasicView(includes[0]); + return BasicMultiView(includes.len); + } +}; + +const Position = struct { x: f32, y: f32 }; + +test "context get/set/unset" { + var reg = Registry.init(std.testing.allocator); + defer reg.deinit(); + + var ctx = reg.getContext(Position); + std.testing.expectEqual(ctx, null); + + var pos = Position{ .x = 5, .y = 5 }; + reg.setContext(&pos); + ctx = reg.getContext(Position); + std.testing.expectEqual(ctx.?, &pos); + + reg.unsetContext(); + ctx = reg.getContext(Position); + std.testing.expectEqual(ctx, null); +} + +// this test should fail +test "context not pointer" { + var reg = Registry.init(std.testing.allocator); + defer reg.deinit(); + + var pos = Position{ .x = 5, .y = 5 }; + // reg.setContext(pos); +} + +test "component context get/set/unset" { + const SomeType = struct { dummy: u1}; + + var reg = Registry.init(std.testing.allocator); + defer reg.deinit(); + + var ctx = reg.getComponentContext(Position, SomeType); + std.testing.expectEqual(ctx, null); + + var pos = SomeType{ .dummy = 0 }; + reg.setComponentContext(Position, &pos); + ctx = reg.getComponentContext(Position, SomeType); + std.testing.expectEqual(ctx.?, &pos); + + reg.unsetComponentContext(Position); + ctx = reg.getComponentContext(Position, SomeType); + std.testing.expectEqual(ctx, null); +} \ No newline at end of file diff --git a/zig-ecs/src/ecs/sparse_set.zig b/zig-ecs/src/ecs/sparse_set.zig new file mode 100644 index 0000000..bc0fc28 --- /dev/null +++ b/zig-ecs/src/ecs/sparse_set.zig @@ -0,0 +1,258 @@ +const std = @import("std"); +const warn = std.debug.warn; + +fn printSet(set: var) void { + warn("-------- sparse to dense (len: {}, cap: {}) ------\n", .{ set.len(), set.capacity() }); + var dense = set.toDenseSlice(); + for (dense) |item| { + warn("[{}] ", .{item}); + } + + warn("\n", .{}); + + var sparse = set.data(); + for (sparse) |item| { + warn("[{}] ", .{item}); + } + warn("\n", .{}); +} + +pub fn main() !void { + var set = SparseSet(u32, u8).init(); + defer set.deinit(); + + warn("add 0, 3\n", .{}); + set.add(1); + printSet(set); + set.add(3); + printSet(set); + warn("contains 0: {}, index: {}\n", .{ set.contains(1), set.index(1) }); + warn("contains 3: {}, index: {}\n", .{ set.contains(3), set.index(3) }); + warn("contains 2: {}\n", .{set.contains(2)}); + + printSet(set); + set.swap(1, 3); + warn("----- swap! ----\n", .{}); + warn("contains 0: {}, index: {}\n", .{ set.contains(1), set.index(1) }); + warn("contains 3: {}, index: {}\n", .{ set.contains(3), set.index(3) }); + + printSet(set); + + warn("remove 1\n", .{}); + set.remove(1); + warn("contains 3: {}, index: {}\n", .{ set.contains(3), set.index(3) }); + printSet(set); + + warn("clear\n", .{}); + set.clear(); + printSet(set); + + warn("dense cap: {}\n", .{set.dense.capacity}); +} + +// TODO: fix entity_mask. it should come from EntityTraitsDefinition. +pub fn SparseSet(comptime SparseT: type, comptime DenseT: type) type { + return struct { + const Self = @This(); + + sparse: std.ArrayList(DenseT), + dense: std.ArrayList(SparseT), + entity_mask: SparseT, + allocator: *std.mem.Allocator, + + pub fn init(allocator: *std.mem.Allocator) *Self { + var set = allocator.create(Self) catch unreachable; + + set.sparse = std.ArrayList(DenseT).init(allocator); + set.dense = std.ArrayList(SparseT).init(allocator); + set.entity_mask = std.math.maxInt(SparseT); + set.allocator = allocator; + + return set; + } + + pub fn deinit(self: *Self) void { + self.dense.deinit(); + self.sparse.deinit(); + self.allocator.destroy(self); + } + + fn page(self: Self, sparse: SparseT) usize { + // TODO: support paging + // return (sparse & EntityTraits.entity_mask) / sparse_per_page; + return sparse & self.entity_mask; + } + + fn assure(self: *Self, pos: usize) []DenseT { + // TODO: support paging + if (self.sparse.capacity < pos or self.sparse.capacity == 0) { + const amount = pos + 1 - self.sparse.capacity; + + // expand and fill with maxInt as an identifier + const old_len = self.sparse.items.len; + self.sparse.resize(self.sparse.items.len + amount) catch unreachable; + self.sparse.expandToCapacity(); + std.mem.set(DenseT, self.sparse.items[old_len..self.sparse.items.len], std.math.maxInt(DenseT)); + } + + return self.sparse.items; + } + + fn offset(self: Self, sparse: SparseT) usize { + // TODO: support paging + // return entt & (sparse_per_page - 1) + return sparse & self.entity_mask; + } + + /// Increases the capacity of a sparse set. + pub fn reserve(self: *Self, cap: usize) void { + self.dense.resize(cap); + } + + /// Returns the number of elements that a sparse set has currently allocated space for + pub fn capacity(self: *Self) usize { + return self.dense.capacity; + } + + /// Returns the number of elements in a sparse set + pub fn len(self: *Self) usize { + return self.dense.items.len; + } + + pub fn empty(self: *Self) bool { + return self.dense.items.len == 0; + } + + pub fn data(self: Self) *const []SparseT { + return &self.dense.items; + } + + pub fn contains(self: Self, sparse: SparseT) bool { + const curr = self.page(sparse); + if (curr >= self.sparse.items.len) + return false; + + // testing against maxInt permits to avoid accessing the packed array + return curr < self.sparse.items.len and self.sparse.items[curr] != std.math.maxInt(DenseT); + } + + /// Returns the position of an entity in a sparse set + pub fn index(self: Self, sparse: SparseT) DenseT { + std.debug.assert(self.contains(sparse)); + return self.sparse.items[self.offset(sparse)]; + } + + /// Assigns an entity to a sparse set + pub fn add(self: *Self, sparse: SparseT) void { + std.debug.assert(!self.contains(sparse)); + + // assure(page(entt))[offset(entt)] = packed.size() + self.assure(self.page(sparse))[self.offset(sparse)] = @intCast(DenseT, self.dense.items.len); + _ = self.dense.append(sparse) catch unreachable; + } + + /// Removes an entity from a sparse set + pub fn remove(self: *Self, sparse: SparseT) void { + std.debug.assert(self.contains(sparse)); + + const curr = self.page(sparse); + const pos = self.offset(sparse); + const last_dense = self.dense.items[self.dense.items.len - 1]; + + self.dense.items[self.sparse.items[curr]] = last_dense; + self.sparse.items[self.page(last_dense)] = self.sparse.items[curr]; + self.sparse.items[curr] = std.math.maxInt(DenseT); + + _ = self.dense.pop(); + } + + /// Swaps two entities in the internal packed array + pub fn swap(self: *Self, sparse_l: SparseT, sparse_r: SparseT) void { + var from = &self.sparse.items[sparse_l]; + var to = &self.sparse.items[sparse_r]; + + std.mem.swap(SparseT, &self.dense.items[from.*], &self.dense.items[to.*]); + std.mem.swap(DenseT, from, to); + } + + /// Sort elements according to the given comparison function + pub fn sort(self: *Self) void { + unreachable; + } + + /// Sort entities according to their order in another sparse set + pub fn respect(self: *Self, other: *Self) void { + unreachable; + } + + pub fn clear(self: *Self) void { + self.sparse.items.len = 0; + self.dense.items.len = 0; + } + + pub fn toDenseSlice(self: Self) []DenseT { + return self.sparse.items; + } + }; +} + +test "add/remove/clear" { + var set = SparseSet(u32, u8).init(std.testing.allocator); + defer set.deinit(); + + set.add(4); + set.add(3); + std.testing.expectEqual(set.len(), 2); + std.testing.expectEqual(set.index(4), 0); + std.testing.expectEqual(set.index(3), 1); + + set.remove(4); + std.testing.expectEqual(set.len(), 1); + + set.clear(); + std.testing.expectEqual(set.len(), 0); +} + +test "grow" { + var set = SparseSet(u32, u8).init(std.testing.allocator); + defer set.deinit(); + + var i = @as(usize, std.math.maxInt(u8)); + while (i > 0) : (i -= 1) { + set.add(@intCast(u32, i)); + } + + std.testing.expectEqual(set.len(), std.math.maxInt(u8)); +} + +test "swap" { + var set = SparseSet(u32, u8).init(std.testing.allocator); + defer set.deinit(); + + set.add(4); + set.add(3); + std.testing.expectEqual(set.index(4), 0); + std.testing.expectEqual(set.index(3), 1); + + set.swap(4, 3); + std.testing.expectEqual(set.index(3), 0); + std.testing.expectEqual(set.index(4), 1); +} + +test "data() synced" { + var set = SparseSet(u32, u8).init(std.testing.allocator); + defer set.deinit(); + + set.add(0); + set.add(1); + set.add(2); + set.add(3); + + var data = set.data(); + std.testing.expectEqual(data.*[1], 1); + std.testing.expectEqual(set.len(), data.len); + + set.remove(0); + set.remove(1); + std.testing.expectEqual(set.len(), data.len); +} diff --git a/zig-ecs/src/ecs/type_map.zig b/zig-ecs/src/ecs/type_map.zig new file mode 100644 index 0000000..c0c675b --- /dev/null +++ b/zig-ecs/src/ecs/type_map.zig @@ -0,0 +1,53 @@ +const std = @import("std"); +const utils = @import("utils.zig"); + +pub const TypeMap = struct { + map: std.AutoHashMap(u32, u8), + counter: u8 = 0, + + pub fn init(allocator: *std.mem.Allocator) TypeMap { + return TypeMap{ + .map = std.AutoHashMap(u32, u8).init(allocator), + }; + } + + pub fn deinit(self: TypeMap) void { + self.map.deinit(); + } + + pub fn contains(self: TypeMap, comptime T: type) bool { + return self.map.contains(@truncate(u32, utils.typeHash(T))); + } + + /// gets the value for T. It MUST exist if you use this method to get it. + pub fn get(self: *TypeMap, comptime T: type) u8 { + return self.map.get(@truncate(u32, utils.typeHash(T))).?.value; + } + + /// gets the value for T if it exists. If it doesnt, it is registered and the value returned. + pub fn getOrPut(self: *TypeMap, comptime T: type, type_id: *u8) bool { + // TODO: is it safe to truncate to u32 here? + var res = self.map.getOrPut(@truncate(u32, utils.typeHash(T))) catch unreachable; + if (!res.found_existing) { + res.kv.value = self.counter; + self.counter += 1; + } + type_id.* = res.kv.value; + return res.found_existing; + } +}; + +test "TypeMap" { + var map = TypeMap.init(std.testing.allocator); + defer map.deinit(); + + var type_id: u8 = undefined; + _ = map.getOrPut(usize, &type_id); + std.testing.expectEqual(@as(u8, 0), type_id); + + _ = map.getOrPut(f32, &type_id); + std.testing.expectEqual(@as(u8, 1), type_id); + + _ = map.getOrPut(usize, &type_id); + std.testing.expectEqual(@as(u8, 0), type_id); +} diff --git a/zig-ecs/src/ecs/utils.zig b/zig-ecs/src/ecs/utils.zig new file mode 100644 index 0000000..2269f3c --- /dev/null +++ b/zig-ecs/src/ecs/utils.zig @@ -0,0 +1,38 @@ + +/// sorts items using lessThan and keeps sub_items with the same sort +pub fn sortSub(comptime T1: type, comptime T2: type, items: []T1, sub_items: []T2, lessThan: fn (lhs: T1, rhs: T1) bool) void { + var i: usize = 1; + while (i < items.len) : (i += 1) { + const x = items[i]; + const y = sub_items[i]; + var j: usize = i; + while (j > 0 and lessThan(x, items[j - 1])) : (j -= 1) { + items[j] = items[j - 1]; + sub_items[j] = sub_items[j - 1]; + } + items[j] = x; + sub_items[j] = y; + } +} + +/// comptime string hashing for the type names +pub fn typeHash(comptime T: type) comptime_int { + return stringHash(@typeName(T)); +} + +/// comptime string hashing, djb2 by Dan Bernstein +pub fn stringHash(comptime str: []const u8) comptime_int { + var hash: comptime_int = 5381; + for (str) |c| { + hash = ((hash << 5) + hash) + @intCast(comptime_int, c); + } + + return hash; +} + +pub fn isComptime(comptime T: type) bool { + return switch (@typeInfo(T)) { + .ComptimeInt, .ComptimeFloat => true, + else => false, + }; +} \ No newline at end of file diff --git a/zig-ecs/src/ecs/view.zig b/zig-ecs/src/ecs/view.zig new file mode 100644 index 0000000..e921556 --- /dev/null +++ b/zig-ecs/src/ecs/view.zig @@ -0,0 +1,217 @@ +const std = @import("std"); +const utils = @import("utils.zig"); + +const Registry = @import("registry.zig").Registry; +const Storage = @import("registry.zig").Storage; +const Entity = @import("registry.zig").Entity; + + +/// single item view. Iterating raw() directly is the fastest way to get at the data. +pub fn BasicView(comptime T: type) type { + return struct { + const Self = @This(); + + storage: *Storage(T), + + pub fn init(storage: *Storage(T)) Self { + return Self { + .storage = storage, + }; + } + + pub fn len(self: Self) usize { + return self.storage.len(); + } + + /// Direct access to the array of components + pub fn raw(self: Self) []T { + return self.storage.raw(); + } + + /// Direct access to the array of entities + pub fn data(self: Self) *const []Entity { + return self.storage.data(); + } + + /// Returns the object associated with an entity + pub fn get(self: Self, entity: Entity) *T { + return self.storage.get(entity); + } + }; +} + +pub fn BasicMultiView(comptime n: usize) type { + return struct { + const Self = @This(); + + type_ids: [n]u32, + registry: *Registry, + + pub const Iterator = struct { + view: *Self, + index: usize = 0, + entities: *const []Entity, + + pub fn init(view: *Self) Iterator { + const ptr = view.registry.components.getValue(@intCast(u8, view.type_ids[0])).?; + return .{ + .view = view, + .entities = @intToPtr(*Storage(u8), ptr).data(), + }; + } + + pub fn next(it: *Iterator) ?Entity { + if (it.index >= it.entities.len) return null; + + blk: while (it.index < it.entities.len) : (it.index += 1) { + const entity = it.entities.*[it.index]; + + // entity must be in all other Storages + for (it.view.type_ids) |tid| { + const ptr = it.view.registry.components.getValue(@intCast(u8, tid)).?; + if (!@intToPtr(*Storage(u8), ptr).contains(entity)) { + break :blk; + } + } + it.index += 1; + return entity; + } + + return null; + } + + // Reset the iterator to the initial index + pub fn reset(it: *Iterator) void { + it.index = 0; + } + }; + + pub fn init(type_ids: [n]u32, registry: *Registry) Self { + return Self{ + .type_ids = type_ids, + .registry = registry, + }; + } + + pub fn get(self: *Self, comptime T: type, entity: Entity) *T { + const type_id = self.registry.typemap.get(T); + const ptr = self.registry.components.getValue(type_id).?; + const store = @intToPtr(*Storage(T), ptr); + + std.debug.assert(store.contains(entity)); + return store.get(entity); + } + + pub fn getConst(self: *Self, comptime T: type, entity: Entity) T { + const type_id = self.registry.typemap.get(T); + const ptr = self.registry.components.getValue(type_id).?; + const store = @intToPtr(*Storage(T), ptr); + + std.debug.assert(store.contains(entity)); + return store.getConst(entity); + } + + fn sort(self: *Self) void { + // get our component counts in an array so we can sort the type_ids based on how many entities are in each + var sub_items: [n]usize = undefined; + for (self.type_ids) |tid, i| { + const ptr = self.registry.components.getValue(@intCast(u8, tid)).?; + const store = @intToPtr(*Storage(u8), ptr); + sub_items[i] = store.len(); + } + + utils.sortSub(usize, u32, sub_items[0..], self.type_ids[0..], std.sort.asc(usize)); + } + + pub fn iterator(self: *Self) Iterator { + self.sort(); + return Iterator.init(self); + } + }; +} + + +test "single basic view" { + var store = Storage(f32).init(std.testing.allocator); + defer store.deinit(); + + store.add(3, 30); + store.add(5, 50); + store.add(7, 70); + + var view = BasicView(f32).init(&store); + std.testing.expectEqual(view.len(), 3); + + store.remove(7); + std.testing.expectEqual(view.len(), 2); +} + +test "single basic view data" { + var store = Storage(f32).init(std.testing.allocator); + defer store.deinit(); + + store.add(3, 30); + store.add(5, 50); + store.add(7, 70); + + var view = BasicView(f32).init(&store); + + std.testing.expectEqual(view.get(3).*, 30); + + for (view.data().*) |entity, i| { + if (i == 0) + std.testing.expectEqual(entity, 3); + if (i == 1) + std.testing.expectEqual(entity, 5); + if (i == 2) + std.testing.expectEqual(entity, 7); + } + + for (view.raw()) |data, i| { + if (i == 0) + std.testing.expectEqual(data, 30); + if (i == 1) + std.testing.expectEqual(data, 50); + if (i == 2) + std.testing.expectEqual(data, 70); + } + + std.testing.expectEqual(view.len(), 3); +} + +test "basic multi view" { + var reg = Registry.init(std.testing.allocator); + defer reg.deinit(); + + var e0 = reg.create(); + var e1 = reg.create(); + var e2 = reg.create(); + + reg.add(e0, @as(i32, -0)); + reg.add(e1, @as(i32, -1)); + reg.add(e2, @as(i32, -2)); + + reg.add(e0, @as(u32, 0)); + reg.add(e2, @as(u32, 2)); + + var single_view = reg.view(.{u32}); + var view = reg.view(.{ i32, u32 }); + + var iterated_entities: usize = 0; + var iter = view.iterator(); + while (iter.next()) |entity| { + iterated_entities += 1; + } + + std.testing.expectEqual(iterated_entities, 2); + iterated_entities = 0; + + reg.remove(u32, e0); + + iter.reset(); + while (iter.next()) |entity| { + iterated_entities += 1; + } + + std.testing.expectEqual(iterated_entities, 1); +} \ No newline at end of file diff --git a/zig-ecs/src/signals/delegate.zig b/zig-ecs/src/signals/delegate.zig new file mode 100644 index 0000000..a0724b5 --- /dev/null +++ b/zig-ecs/src/signals/delegate.zig @@ -0,0 +1,87 @@ +const std = @import("std"); + +/// wraps either a free function or a bound function that takes an Event as a parameter +pub fn Delegate(comptime Event: type) type { + return struct { + const Self = @This(); + + ctx_ptr_address: usize = 0, + callback: union(enum) { + free: fn (Event) void, + bound: fn (usize, Event) void, + }, + + /// sets a bound function as the Delegate callback + pub fn initBound(ctx: var, comptime fn_name: []const u8) Self { + std.debug.assert(@ptrToInt(ctx) != 0); + std.debug.assert(@typeInfo(@TypeOf(ctx)) == .Pointer); + + const T = @TypeOf(ctx); + return Self{ + .ctx_ptr_address = @ptrToInt(ctx), + .callback = .{ + .bound = struct { + fn cb(self: usize, param: Event) void { + return @call(.{ .modifier = .always_inline }, @field(@intToPtr(T, self), fn_name), .{param}); + } + }.cb, + }, + }; + } + + /// sets a free function as the Delegate callback + pub fn initFree(func: fn (Event) void) Self { + return Self{ + .callback = .{ .free = func }, + }; + } + + pub fn trigger(self: Self, param: Event) void { + switch (self.callback) { + .free => |func| @call(.{}, func, .{param}), + .bound => |func| @call(.{}, func, .{ self.ctx_ptr_address, param }), + } + } + + pub fn containsFree(self: Self, callback: fn (Event) void) bool { + return switch (self.callback) { + .free => |func| func == callback, + else => false, + }; + } + + pub fn containsBound(self: Self, ctx: var) bool { + std.debug.assert(@ptrToInt(ctx) != 0); + std.debug.assert(@typeInfo(@TypeOf(ctx)) == .Pointer); + + return switch (self.callback) { + .bound => @ptrToInt(ctx) == self.ctx_ptr_address, + else => false, + }; + } + }; +} + +fn tester(param: u32) void { + std.testing.expectEqual(@as(u32, 666), param); +} + +const Thing = struct { + field: f32 = 0, + + pub fn tester(self: *Thing, param: u32) void { + std.testing.expectEqual(@as(u32, 777), param); + } +}; + +test "free Delegate" { + var d = Delegate(u32).initFree(tester); + d.trigger(666); +} + +test "bound Delegate" { + var thing = Thing{}; + + var d = Delegate(u32).initBound(&thing, "tester"); + d.trigger(777); +} diff --git a/zig-ecs/src/signals/dispatcher.zig b/zig-ecs/src/signals/dispatcher.zig new file mode 100644 index 0000000..f70945c --- /dev/null +++ b/zig-ecs/src/signals/dispatcher.zig @@ -0,0 +1,51 @@ +const std = @import("std"); +const Sink = @import("sink.zig").Sink; +const Signal = @import("signal.zig").Signal; +const TypeMap = @import("../ecs/type_map.zig").TypeMap; + +pub const Dispatcher = struct { + typemap: TypeMap, + signals: std.AutoHashMap(u8, usize), + allocator: *std.mem.Allocator, + + pub fn init(allocator: *std.mem.Allocator) Dispatcher { + return Dispatcher { + .typemap = TypeMap.init(allocator), + .signals = std.AutoHashMap(u8, usize).init(allocator), + .allocator = allocator, + }; + } + + pub fn deinit(self: Dispatcher) void { + var it = self.signals.iterator(); + while (it.next()) |ptr| { + // HACK: we dont know the Type here but we need to call deinit + var signal = @intToPtr(*Signal(void), ptr.value); + signal.deinit(); + } + + self.typemap.deinit(); + self.signals.deinit(); + } + + fn assure(self: *Dispatcher, comptime T: type) *Signal(T) { + var type_id: u8 = undefined; + if (!self.typemap.getOrPut(T, &type_id)) { + var signal = Signal(T).create(self.allocator); + var signal_ptr = @ptrToInt(signal); + _ = self.signals.put(type_id, signal_ptr) catch unreachable; + return signal; + } + + const ptr = self.signals.getValue(type_id).?; + return @intToPtr(*Signal(T), ptr); + } + + pub fn sink(self: *Dispatcher, comptime T: type) Sink(T) { + return self.assure(T).sink(); + } + + pub fn trigger(self: *Dispatcher, comptime T: type, value: T) void { + self.assure(T).publish(value); + } +}; diff --git a/zig-ecs/src/signals/signal.zig b/zig-ecs/src/signals/signal.zig new file mode 100644 index 0000000..f8b923f --- /dev/null +++ b/zig-ecs/src/signals/signal.zig @@ -0,0 +1,95 @@ +const std = @import("std"); +const Sink = @import("sink.zig").Sink; +const Delegate = @import("delegate.zig").Delegate; + +pub fn Signal(comptime Event: type) type { + return struct { + const Self = @This(); + + calls: std.ArrayList(Delegate(Event)), + allocator: ?*std.mem.Allocator = null, + + pub fn init(allocator: *std.mem.Allocator) Self { + // we purposely do not store the allocator locally in this case! + return Self{ + .calls = std.ArrayList(Delegate(Event)).init(allocator), + }; + } + + /// heap allocates a Signal + pub fn create(allocator: *std.mem.Allocator) *Self { + var signal = allocator.create(Self) catch unreachable; + signal.calls = std.ArrayList(Delegate(Event)).init(allocator); + signal.allocator = allocator; + return signal; + } + + pub fn deinit(self: *Self) void { + self.calls.deinit(); + + // optionally destroy ourself as well if we came from an allocator + if (self.allocator) |allocator| allocator.destroy(self); + } + + pub fn size(self: Self) usize { + return self.calls.items.len; + } + + pub fn empty(self: Self) bool { + return self.size == 0; + } + + /// Disconnects all the listeners from a signal + pub fn clear(self: *Self) void { + self.calls.items.len = 0; + } + + pub fn publish(self: Self, arg: Event) void { + for (self.calls.items) |call| { + call.trigger(arg); + } + } + + /// Constructs a sink that is allowed to modify a given signal + pub fn sink(self: *Self) Sink(Event) { + return Sink(Event).init(self); + } + }; +} + +fn tester(param: u32) void { + std.testing.expectEqual(@as(u32, 666), param); +} + +const Thing = struct { + field: f32 = 0, + + pub fn tester(self: *Thing, param: u32) void { + std.testing.expectEqual(@as(u32, 666), param); + } +}; + +test "Signal/Sink" { + var signal = Signal(u32).init(std.testing.allocator); + defer signal.deinit(); + + var sink = signal.sink(); + sink.connect(tester); + std.testing.expectEqual(@as(usize, 1), signal.size()); + + sink.connect(tester); + std.testing.expectEqual(@as(usize, 1), signal.size()); + + // bound listener + var thing = Thing{}; + sink.connectBound(&thing, "tester"); + + signal.publish(666); + + sink.disconnect(tester); + signal.publish(666); + std.testing.expectEqual(@as(usize, 1), signal.size()); + + sink.disconnectBound(&thing); + std.testing.expectEqual(@as(usize, 0), signal.size()); +} diff --git a/zig-ecs/src/signals/sink.zig b/zig-ecs/src/signals/sink.zig new file mode 100644 index 0000000..d915ab9 --- /dev/null +++ b/zig-ecs/src/signals/sink.zig @@ -0,0 +1,47 @@ +const std = @import("std"); +const Signal = @import("signal.zig").Signal; +const Delegate = @import("delegate.zig").Delegate; + +/// helper used to connect and disconnect listeners on the fly from a Signal. Listeners are wrapped in Delegates +/// and can be either free functions or functions bound to a struct. +pub fn Sink(comptime Event: type) type { + return struct { + const Self = @This(); + + /// the Signal this Sink is temporarily wrapping + var owning_signal: *Signal(Event) = undefined; + + pub fn init(signal: *Signal(Event)) Self { + owning_signal = signal; + return Self{}; + } + + pub fn connect(self: Self, callback: fn (Event) void) void { + self.disconnect(callback); + _ = owning_signal.calls.append(Delegate(Event).initFree(callback)) catch unreachable; + } + + pub fn connectBound(self: Self, ctx: var, comptime fn_name: []const u8) void { + self.disconnectBound(ctx); + _ = owning_signal.calls.append(Delegate(Event).initBound(ctx, fn_name)) catch unreachable; + } + + pub fn disconnect(self: Self, callback: fn (Event) void) void { + for (owning_signal.calls.items) |call, i| { + if (call.containsFree(callback)) { + _ = owning_signal.calls.swapRemove(i); + break; + } + } + } + + pub fn disconnectBound(self: Self, ctx: var) void { + for (owning_signal.calls.items) |call, i| { + if (call.containsBound(ctx)) { + _ = owning_signal.calls.swapRemove(i); + break; + } + } + } + }; +} diff --git a/zig-ecs/src/tests.zig b/zig-ecs/src/tests.zig new file mode 100644 index 0000000..391c5f8 --- /dev/null +++ b/zig-ecs/src/tests.zig @@ -0,0 +1,15 @@ +// include all files with tests +comptime { + // ecs + _ = @import("ecs/actor.zig"); + _ = @import("ecs/component_storage.zig"); + _ = @import("ecs/entity.zig"); + _ = @import("ecs/handles.zig"); + _ = @import("ecs/sparse_set.zig"); + _ = @import("ecs/type_map.zig"); + _ = @import("ecs/view.zig"); + + // signals + _ = @import("signals/delegate.zig"); + _ = @import("signals/signal.zig"); +} diff --git a/zig-ecs/tests/dispatcher_test.zig b/zig-ecs/tests/dispatcher_test.zig new file mode 100644 index 0000000..63cc4a2 --- /dev/null +++ b/zig-ecs/tests/dispatcher_test.zig @@ -0,0 +1,40 @@ +const std = @import("std"); +const Dispatcher = @import("ecs").Dispatcher; + +fn tester(param: u32) void { + std.testing.expectEqual(@as(u32, 666), param); +} + +fn tester2(param: i32) void { + std.testing.expectEqual(@as(i32, -543), param); +} + +const Thing = struct { + field: f32 = 0, + + pub fn testU32(self: *Thing, param: u32) void { + std.testing.expectEqual(@as(u32, 666), param); + } + + pub fn testI32(self: *Thing, param: i32) void { + std.testing.expectEqual(@as(i32, -543), param); + } +}; + + +test "Dispatcher" { + var thing = Thing{}; + + var d = Dispatcher.init(std.testing.allocator); + defer d.deinit(); + + var sink = d.sink(u32); + sink.connect(tester); + sink.connectBound(&thing, "testU32"); + d.trigger(u32, 666); + + var sink2 = d.sink(i32); + sink2.connect(tester2); + sink2.connectBound(&thing, "testI32"); + d.trigger(i32, -543); +} \ No newline at end of file diff --git a/zig-ecs/tests/registry_test.zig b/zig-ecs/tests/registry_test.zig new file mode 100644 index 0000000..9a118f2 --- /dev/null +++ b/zig-ecs/tests/registry_test.zig @@ -0,0 +1,28 @@ +const std = @import("std"); +const ecs = @import("ecs"); +const Registry = @import("ecs").Registry; + +const Velocity = struct { x: f32, y: f32 }; +const Position = struct { x: f32, y: f32 }; +const Empty = struct {}; +const BigOne = struct { pos: Position, vel: Velocity, accel: Velocity }; + +test "entity traits" { + const traits = ecs.EntityTraitsType(.large).init(); +} + +test "Registry" { + var reg = Registry.init(std.testing.allocator); + defer reg.deinit(); + + var e1 = reg.create(); + + reg.add(e1, Empty{}); + reg.add(e1, Position{ .x = 5, .y = 5 }); + reg.add(e1, BigOne{ .pos = Position{ .x = 5, .y = 5 }, .vel = Velocity{ .x = 5, .y = 5 }, .accel = Velocity{ .x = 5, .y = 5 } }); + + std.testing.expect(reg.has(Empty, e1)); + + reg.remove(Empty, e1); + std.testing.expect(!reg.has(Empty, e1)); +} diff --git a/zig-ecs/tests/tests.zig b/zig-ecs/tests/tests.zig new file mode 100644 index 0000000..2f11302 --- /dev/null +++ b/zig-ecs/tests/tests.zig @@ -0,0 +1,4 @@ +test "ecs test suite" { + _ = @import("dispatcher_test.zig"); + _ = @import("registry_test.zig"); +}