initial commit

master
Mike 5 years ago
commit 5589130bfd

@ -0,0 +1 @@
zig-cache

@ -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);
}

@ -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});
}
}

@ -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;

@ -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(&reg);
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(&reg);
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);
}

@ -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);
}

@ -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));
}

@ -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));
}

@ -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);
}

@ -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);
}

@ -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);
}

@ -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,
};
}

@ -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);
}

@ -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);
}

@ -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);
}
};

@ -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());
}

@ -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;
}
}
}
};
}

@ -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");
}

@ -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);
}

@ -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));
}

@ -0,0 +1,4 @@
test "ecs test suite" {
_ = @import("dispatcher_test.zig");
_ = @import("registry_test.zig");
}
Loading…
Cancel
Save