diff --git a/zig-ecs/src/ecs.zig b/zig-ecs/src/ecs.zig index 6a21a3a..85123ac 100644 --- a/zig-ecs/src/ecs.zig +++ b/zig-ecs/src/ecs.zig @@ -8,6 +8,8 @@ pub const Entity = @import("ecs/registry.zig").Entity; pub const Registry = @import("ecs/registry.zig").Registry; pub const BasicView = @import("ecs/views.zig").BasicView; pub const BasicMultiView = @import("ecs/views.zig").BasicMultiView; +pub const BasicGroup = @import("ecs/groups.zig").BasicGroup; +pub const OwningGroup = @import("ecs/groups.zig").OwningGroup; // signals pub const Signal = @import("signals/signal.zig").Signal; diff --git a/zig-ecs/src/ecs/component_storage.zig b/zig-ecs/src/ecs/component_storage.zig index c31e3a6..c214e2a 100644 --- a/zig-ecs/src/ecs/component_storage.zig +++ b/zig-ecs/src/ecs/component_storage.zig @@ -162,9 +162,11 @@ pub fn ComponentStorage(comptime Component: type, comptime Entity: type) type { pub usingnamespace if (is_empty_struct) struct { - /// Sort Entities according to the given comparison function - pub fn sort(self: Self, comptime sortFn: fn (void, Entity, Entity) bool) void { - self.set.sort(sortFn); + /// Sort Entities according to the given comparison function. Only T == Entity is allowed. The constraint param only exists for + /// parity with non-empty Components + pub fn sort(self: Self, comptime T: type, context: var, comptime lessThan: fn (@TypeOf(context), T, T) bool) void { + std.debug.assert(T == Entity); + self.set.sort(context, lessThan); } } else @@ -200,16 +202,12 @@ pub fn ComponentStorage(comptime Component: type, comptime Entity: type) type { } /// Sort Entities or Components according to the given comparison function - pub fn sort(self: *Self, comptime T: type, comptime lessThan: fn (void, T, T) bool) void { + pub fn sort(self: Self, comptime T: type, context: var, comptime lessThan: fn (@TypeOf(context), T, T) bool) void { std.debug.assert(T == Entity or T == Component); if (T == Entity) { - self.set.sortSub(lessThan, Component, self.instances.items); + self.set.sortSub(context, lessThan, Component, self.instances.items); } else if (T == Component) { - self.set.sortSubSub({}, Component, lessThan, self.instances.items); - // fn sorter(self: Self, a: T, b: T, sortFn) bool { - // return sortFn(self.instances[a], self.instances[b]); - // } - //return compare(std::as_const(instances[underlying_type::index(lhs)]), std::as_const(instances[underlying_type::index(rhs)])); + self.set.sortSubSub(context, Component, lessThan, self.instances.items); } } }; @@ -339,13 +337,13 @@ test "sort empty component" { store.add(0, Empty{}); comptime const asc_u32 = std.sort.asc(u32); - store.sort(asc_u32); + store.sort(u32, {}, asc_u32); for (store.data()) |e, i| { std.testing.expectEqual(@intCast(u32, i), e); } comptime const desc_u32 = std.sort.desc(u32); - store.sort(desc_u32); + store.sort(u32, {}, desc_u32); var counter: u32 = 2; for (store.data()) |e, i| { std.testing.expectEqual(counter, e); @@ -353,7 +351,7 @@ test "sort empty component" { } } -test "sort component" { +test "sort by entity" { std.debug.warn("\n", .{}); var store = ComponentStorage(f32, u32).initPtr(std.testing.allocator); @@ -363,8 +361,37 @@ test "sort component" { store.add(11, @as(f32, 1.1)); store.add(33, @as(f32, 3.3)); - comptime const desc_u32 = std.sort.desc(f32); - store.sort(f32, desc_u32); + const SortContext = struct{ + store: *ComponentStorage(f32, u32), + + fn sort(this: @This(), a: u32, b: u32) bool { + const real_a = this.store.getConst(a); + const real_b = this.store.getConst(b); + return real_a > real_b; + } + }; + const context = SortContext{.store = store}; + store.sort(u32, context, SortContext.sort); + + var compare: f32 = 5; + for (store.raw()) |val, i| { + std.testing.expect(compare > val); + compare = val; + } +} + +test "sort by component" { + std.debug.warn("\n", .{}); + + var store = ComponentStorage(f32, u32).initPtr(std.testing.allocator); + defer store.deinit(); + + store.add(22, @as(f32, 2.2)); + store.add(11, @as(f32, 1.1)); + store.add(33, @as(f32, 3.3)); + + comptime const desc_f32 = std.sort.desc(f32); + store.sort(f32, {}, desc_f32); var compare: f32 = 5; for (store.raw()) |val, i| { diff --git a/zig-ecs/src/ecs/groups.zig b/zig-ecs/src/ecs/groups.zig index d80a2f6..7c85e44 100644 --- a/zig-ecs/src/ecs/groups.zig +++ b/zig-ecs/src/ecs/groups.zig @@ -9,40 +9,59 @@ const Entity = @import("registry.zig").Entity; /// BasicGroups do not own any components. Internally, they keep a SparseSet that is always kept up-to-date with the matching /// entities. pub const BasicGroup = struct { - const Self = @This(); - registry: *Registry, group_data: *Registry.GroupData, - pub fn init(registry: *Registry, group_data: *Registry.GroupData) Self { - return Self{ + pub fn init(registry: *Registry, group_data: *Registry.GroupData) BasicGroup { + return .{ .registry = registry, .group_data = group_data, }; } - pub fn len(self: Self) usize { + pub fn len(self: BasicGroup) usize { return self.group_data.entity_set.len(); } /// Direct access to the array of entities - pub fn data(self: Self) []const Entity { + pub fn data(self: BasicGroup) []const Entity { return self.group_data.entity_set.data(); } - pub fn get(self: *Self, comptime T: type, entity: Entity) *T { + pub fn get(self: *BasicGroup, comptime T: type, entity: Entity) *T { return self.registry.assure(T).get(entity); } - pub fn getConst(self: *Self, comptime T: type, entity: Entity) T { + pub fn getConst(self: *BasicGroup, comptime T: type, entity: Entity) T { return self.registry.assure(T).getConst(entity); } /// iterates the matched entities backwards, so the current entity can always be removed safely /// and newly added entities wont affect it. - pub fn iterator(self: Self) utils.ReverseSliceIterator(Entity) { + pub fn iterator(self: BasicGroup) utils.ReverseSliceIterator(Entity) { return self.group_data.entity_set.reverseIterator(); } + + pub fn sort(self: *BasicGroup, comptime T: type, context: var, comptime lessThan: fn (@TypeOf(context), T, T) bool) void { + if (T == Entity) { + self.group_data.entity_set.sort(context, lessThan); + } else { + // TODO: in debug mode, validate that T is present in the group + const SortContext = struct{ + group: *BasicGroup, + wrapped_context: @TypeOf(context), + lessThan: fn (@TypeOf(context), T, T) bool, + + fn sort(this: @This(), a: Entity, b: Entity) bool { + const real_a = this.group.getConst(T, a); + const real_b = this.group.getConst(T, b); + return this.lessThan(this.wrapped_context, real_a, real_b); + } + }; + var wrapper = SortContext{.group = self, .wrapped_context = context, .lessThan = lessThan}; + self.group_data.entity_set.sort(wrapper, SortContext.sort); + } + } }; pub const OwningGroup = struct { diff --git a/zig-ecs/src/ecs/registry.zig b/zig-ecs/src/ecs/registry.zig index 3363f87..f285780 100644 --- a/zig-ecs/src/ecs/registry.zig +++ b/zig-ecs/src/ecs/registry.zig @@ -412,10 +412,10 @@ pub const Registry = struct { return self.singletons; } - pub fn sort(self: *Registry, comptime T: type) void { + pub fn sort(self: *Registry, comptime T: type, comptime lessThan: fn (void, T, T) bool) void { const comp = self.assure(T); std.debug.assert(comp.super == 0); - unreachable; + comp.sort(T, lessThan); } /// Checks whether the given component belongs to any group. If so, it is not sortable directly. @@ -563,7 +563,8 @@ pub const Registry = struct { new_group_data.entity_set.add(entity); } } else { - // ??? why not? we cannot iterate backwards because we want to leave behind valid entities in case of owned types + // we cannot iterate backwards because we want to leave behind valid entities in case of owned types + // ??? why not? var first_owned_storage = self.assure(owned[0]); for (first_owned_storage.data()) |entity| { new_group_data.maybeValidIf(entity); diff --git a/zig-ecs/src/ecs/sparse_set.zig b/zig-ecs/src/ecs/sparse_set.zig index d3e239e..2c27a97 100644 --- a/zig-ecs/src/ecs/sparse_set.zig +++ b/zig-ecs/src/ecs/sparse_set.zig @@ -143,8 +143,8 @@ pub fn SparseSet(comptime SparseT: type) type { } /// Sort elements according to the given comparison function - pub fn sort(self: *Self, comptime sortFn: fn (void, SparseT, SparseT) bool) void { - std.sort.insertionSort(SparseT, self.dense.items, {}, sortFn); + pub fn sort(self: *Self, context: var, comptime lessThan: fn (@TypeOf(context), SparseT, SparseT) bool) void { + std.sort.insertionSort(SparseT, self.dense.items, context, lessThan); for (self.dense.items) |sparse, i| { // sparse[page(packed[pos])][offset(packed[pos])] = entity_type(pos); @@ -153,12 +153,20 @@ pub fn SparseSet(comptime SparseT: type) type { } /// Sort elements according to the given comparison function and keeps sub_items with the same sort - pub fn sortSub(self: *Self, comptime sortFn: fn (void, SparseT, SparseT) bool, comptime T: type, sub_items: []T) void { - utils.sortSub(SparseT, T, self.dense.items, sub_items, sortFn); + pub fn sortSub(self: *Self, context: var, comptime lessThan: fn (@TypeOf(context), SparseT, SparseT) bool, comptime T: type, sub_items: []T) void { + std.sort.insertionSort(SparseT, self.dense.items, context, lessThan); - for (self.dense.items) |sparse, i| { - // sparse[page(packed[pos])][offset(packed[pos])] = entity_type(pos); - self.sparse.items[self.dense.items[self.page(@intCast(SparseT, i))]] = @intCast(SparseT, i); + for (self.dense.items) |sparse, pos| { + var curr = @intCast(SparseT, pos); + var next = self.index(self.dense.items[curr]); + + while (curr != next) { + std.mem.swap(T, &sub_items[self.index(self.dense.items[curr])], &sub_items[self.index(self.dense.items[next])]); + self.sparse.items[self.dense.items[self.page(@intCast(SparseT, curr))]] = @intCast(SparseT, curr); + + curr = next; + next = self.index(self.dense.items[curr]); + } } } @@ -323,7 +331,7 @@ test "respect 2" { set.add(1); set.add(3); - set.sort(desc_u32); + set.sort({}, desc_u32); for (set.dense.items) |item, i| { if (i < set.dense.items.len - 1) { diff --git a/zig-ecs/tests/groups_test.zig b/zig-ecs/tests/groups_test.zig index cdfaf05..98f0adf 100644 --- a/zig-ecs/tests/groups_test.zig +++ b/zig-ecs/tests/groups_test.zig @@ -2,6 +2,7 @@ const std = @import("std"); const warn = std.debug.warn; const ecs = @import("ecs"); const Registry = @import("ecs").Registry; +const BasicGroup = @import("ecs").BasicGroup; const Velocity = struct { x: f32 = 0, y: f32 = 0 }; const Position = struct { x: f32 = 0, y: f32 = 0 }; @@ -14,31 +15,79 @@ const Rotation = struct { x: f32 = 0 }; fn printStore(store: var, name: []const u8) void { warn("--- {} ---\n", .{name}); for (store.set.dense.items) |e, i| { - warn("e[{}] s[{}]{}", .{e, store.set.page(store.set.dense.items[i]), store.set.sparse.items[store.set.page(store.set.dense.items[i])]}); + warn("e[{}] s[{}]{}", .{ e, store.set.page(store.set.dense.items[i]), store.set.sparse.items[store.set.page(store.set.dense.items[i])] }); warn(" ({d:.2}) ", .{store.instances.items[i]}); } warn("\n", .{}); } +test "sort BasicGroup by Entity" { + var reg = Registry.init(std.testing.allocator); + defer reg.deinit(); + + var group = reg.group(.{}, .{Sprite, Renderable}, .{}); + + var i: usize = 0; + while (i < 5) : (i += 1) { + var e = reg.create(); + reg.add(e, Sprite{ .x = @intToFloat(f32, i) }); + reg.add(e, Renderable{ .x = @intToFloat(f32, i) }); + } + + const SortContext = struct{ + group: BasicGroup, + + fn sort(this: *@This(), a: ecs.Entity, b: ecs.Entity) bool { + const real_a = this.group.getConst(Sprite, a); + const real_b = this.group.getConst(Sprite, b); + return real_a.x > real_b.x; + } + }; + + var context = SortContext{.group = group}; + group.sort(ecs.Entity, &context, SortContext.sort); + + var val: f32 = 0; + var iter = group.iterator(); + while (iter.next()) |entity| { + std.testing.expectEqual(val, group.getConst(Sprite, entity).x); + val += 1; + } +} -test "sort component" { - var store = ecs.ComponentStorage(f32, u32).initPtr(std.testing.allocator); - defer store.deinit(); +test "sort BasicGroup by Component" { + var reg = Registry.init(std.testing.allocator); + defer reg.deinit(); - store.add(22, @as(f32, 2.2)); - store.add(11, @as(f32, 1.1)); - store.add(33, @as(f32, 3.3)); + var group = reg.group(.{}, .{Sprite, Renderable}, .{}); - comptime const desc_u32 = std.sort.desc(f32); - store.sort(f32, desc_u32); + var i: usize = 0; + while (i < 5) : (i += 1) { + var e = reg.create(); + reg.add(e, Sprite{ .x = @intToFloat(f32, i) }); + reg.add(e, Renderable{ .x = @intToFloat(f32, i) }); + } - var compare: f32 = 5; - for (store.raw()) |val, i| { - std.testing.expect(compare > val); - compare = val; + const SortContext = struct{ + fn sort(this: void, a: Sprite, b: Sprite) bool { + return a.x > b.x; + } + }; + group.sort(Sprite, {}, SortContext.sort); + + var val: f32 = 0; + var iter = group.iterator(); + while (iter.next()) |entity| { + std.testing.expectEqual(val, group.getConst(Sprite, entity).x); + val += 1; } } +test "sort OwningGroup by Entity" { + var reg = Registry.init(std.testing.allocator); + defer reg.deinit(); +} + test "nested OwningGroups add/remove components" { var reg = Registry.init(std.testing.allocator); defer reg.deinit(); @@ -87,7 +136,7 @@ test "nested OwningGroups entity order" { var transform_store = reg.assure(Transform); // printStore(sprite_store, "Sprite"); - reg.add(1, Transform{.x = 1}); + reg.add(1, Transform{ .x = 1 }); // printStore(sprite_store, "Sprite"); // printStore(transform_store, "Transform");