diff --git a/zig-ecs/src/ecs.zig b/zig-ecs/src/ecs.zig index 62c55e1..5fc134a 100644 --- a/zig-ecs/src/ecs.zig +++ b/zig-ecs/src/ecs.zig @@ -1,6 +1,8 @@ // ecs pub const EntityTraitsType = @import("ecs/entity.zig").EntityTraitsType; +pub const ComponentStorage = @import("ecs/component_storage.zig").ComponentStorage; + pub const Entity = @import("ecs/registry.zig").Entity; pub const Registry = @import("ecs/registry.zig").Registry; pub const BasicView = @import("ecs/views.zig").BasicView; diff --git a/zig-ecs/src/ecs/component_storage.zig b/zig-ecs/src/ecs/component_storage.zig index 954241a..de9ee8f 100644 --- a/zig-ecs/src/ecs/component_storage.zig +++ b/zig-ecs/src/ecs/component_storage.zig @@ -17,9 +17,7 @@ pub fn ComponentStorage(comptime CompT: type, comptime EntityT: type) type { // 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 }; + comptime var CompOrAlmostEmptyT = if (is_empty_struct) struct { dummy: u1 } else CompT; return struct { const Self = @This(); @@ -27,7 +25,8 @@ pub fn ComponentStorage(comptime CompT: type, comptime EntityT: type) type { set: *SparseSet(EntityT), instances: std.ArrayList(CompOrAlmostEmptyT), allocator: ?*std.mem.Allocator, - super: usize = 0, /// doesnt really belong here...used to denote group ownership + /// doesnt really belong here...used to denote group ownership + super: usize = 0, safe_deinit: fn (*Self) void, safe_swap: fn (*Self, EntityT, EntityT) void, construction: Signal(EntityT), @@ -40,14 +39,16 @@ pub fn ComponentStorage(comptime CompT: type, comptime EntityT: type) type { .instances = undefined, .safe_deinit = struct { fn deinit(self: *Self) void { - if (!is_empty_struct) + if (!is_empty_struct) { self.instances.deinit(); + } } }.deinit, .safe_swap = struct { fn swap(self: *Self, lhs: EntityT, rhs: EntityT) void { - if (!is_empty_struct) + if (!is_empty_struct) { std.mem.swap(CompT, &self.instances.items[self.set.index(lhs)], &self.instances.items[self.set.index(rhs)]); + } self.set.swap(lhs, rhs); } }.swap, @@ -57,8 +58,9 @@ pub fn ComponentStorage(comptime CompT: type, comptime EntityT: type) type { .destruction = Signal(EntityT).init(allocator), }; - if (!is_empty_struct) + if (!is_empty_struct) { store.instances = std.ArrayList(CompOrAlmostEmptyT).init(allocator); + } return store; } @@ -66,8 +68,9 @@ pub fn ComponentStorage(comptime CompT: type, comptime EntityT: type) type { pub fn initPtr(allocator: *std.mem.Allocator) *Self { var store = allocator.create(Self) catch unreachable; store.set = SparseSet(EntityT).initPtr(allocator); - if (!is_empty_struct) + if (!is_empty_struct) { store.instances = std.ArrayList(CompOrAlmostEmptyT).init(allocator); + } store.allocator = allocator; store.super = 0; store.construction = Signal(EntityT).init(allocator); @@ -77,8 +80,9 @@ pub fn ComponentStorage(comptime CompT: type, comptime EntityT: type) type { // since we are stored as a pointer, we need to catpure this store.safe_deinit = struct { fn deinit(self: *Self) void { - if (!is_empty_struct) + if (!is_empty_struct) { self.instances.deinit(); + } } }.deinit; @@ -157,7 +161,12 @@ pub fn ComponentStorage(comptime CompT: type, comptime EntityT: type) type { } pub usingnamespace if (is_empty_struct) - struct {} + struct { + /// Sort Entities according to the given comparison function + pub fn sort(self: Self, comptime sortFn: fn (void, EntityT, EntityT) bool) void { + self.set.sort(sortFn); + } + } else struct { /// Direct access to the array of objects @@ -189,6 +198,21 @@ pub fn ComponentStorage(comptime CompT: type, comptime EntityT: type) type { pub fn tryGetConst(self: *Self, entity: EntityT) ?CompT { return if (self.set.contains(entity)) self.instances.items[self.set.index(entity)] else null; } + + /// Sort Entities or Components according to the given comparison function + pub fn sort(self: Self, comptime T: type, comptime sortFn: fn (void, T, T) bool) void { + std.debug.assert(T == EntityT or T == CompT); + if (T == EntityT) { + self.set.sortSub(sortFn, CompT, self.instances.items); + } else if (T == CompT) { + // essentially need to be able to call a sort method with a bound fn. That fn would then use sortFn along + // with self.instances. + // 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)])); + } + } }; /// Direct access to the array of entities @@ -207,8 +231,9 @@ pub fn ComponentStorage(comptime CompT: type, comptime EntityT: type) type { } pub fn clear(self: *Self) void { - if (!is_empty_struct) + if (!is_empty_struct) { self.instances.items.len = 0; + } self.set.clear(); } }; @@ -251,12 +276,15 @@ test "iterate" { store.add(7, 66.45); for (store.data()) |entity, i| { - if (i == 0) + if (i == 0) { std.testing.expectEqual(entity, 3); - if (i == 1) + } + if (i == 1) { std.testing.expectEqual(entity, 5); - if (i == 2) + } + if (i == 2) { std.testing.expectEqual(entity, 7); + } } } @@ -300,3 +328,54 @@ test "signals" { store.replace(4, 45.64); store.remove(4); } + +const asc_u32 = std.sort.asc(u32); +const desc_u32 = std.sort.desc(u32); + +test "sort empty component" { + const Empty = struct {}; + + var store = ComponentStorage(Empty, u32).initPtr(std.testing.allocator); + defer store.deinit(); + + store.add(1, Empty{}); + store.add(2, Empty{}); + store.add(0, Empty{}); + + store.sort(asc_u32); + for (store.data()) |e, i| { + std.testing.expectEqual(@intCast(u32, i), e); + } + + store.sort(desc_u32); + var counter: u32 = 2; + for (store.data()) |e, i| { + std.testing.expectEqual(counter, e); + if (counter > 0) counter -= 1; + } +} + +const asc_f32 = std.sort.asc(f32); +const desc_f32 = std.sort.desc(f32); + +test "sort component" { + std.debug.warn("\n", .{}); + + var store = ComponentStorage(f32, u32).initPtr(std.testing.allocator); + defer store.deinit(); + + store.add(1, @as(f32, 1.1)); + store.add(2, @as(f32, 2.2)); + store.add(0, @as(f32, 0.0)); + + store.sort(f32, asc_f32); + for (store.raw()) |e, i| { + // std.debug.warn("{}: {}\n", .{i, e}); + // std.testing.expectEqual(@intCast(u32, i), e); + } + + store.sort(f32, desc_f32); + for (store.raw()) |e, i| { + // std.testing.expectEqual(counter, e); + } +} \ No newline at end of file diff --git a/zig-ecs/src/ecs/registry.zig b/zig-ecs/src/ecs/registry.zig index 0b6b6c8..3363f87 100644 --- a/zig-ecs/src/ecs/registry.zig +++ b/zig-ecs/src/ecs/registry.zig @@ -78,7 +78,7 @@ pub const Registry = struct { allocator.destroy(self); } - fn maybeValidIf(self: *GroupData, entity: Entity) void { + pub fn maybeValidIf(self: *GroupData, entity: Entity) void { const isValid: bool = blk: { for (self.owned) |tid| { const ptr = self.registry.components.getValue(tid).?; @@ -120,7 +120,7 @@ pub const Registry = struct { } } - fn discardIf(self: *GroupData, entity: Entity) void { + pub fn discardIf(self: *GroupData, entity: Entity) void { if (self.owned.len == 0) { if (self.entity_set.contains(entity)) self.entity_set.remove(entity); @@ -608,7 +608,7 @@ pub const Registry = struct { inline fn concatTypes(comptime types: var) []const u8 { comptime { const impl = struct { - fn asc(lhs: []const u8, rhs: []const u8) bool { + fn asc(context: void, lhs: []const u8, rhs: []const u8) bool { return std.mem.lessThan(u8, lhs, rhs); } }; @@ -618,7 +618,7 @@ pub const Registry = struct { name.* = @typeName(types[i]); } - std.sort.sort([]const u8, &names, impl.asc); + std.sort.sort([]const u8, &names, {}, impl.asc); comptime var res: []const u8 = ""; inline for (names) |name| res = res ++ name; diff --git a/zig-ecs/src/ecs/sparse_set.zig b/zig-ecs/src/ecs/sparse_set.zig index e39571c..9848973 100644 --- a/zig-ecs/src/ecs/sparse_set.zig +++ b/zig-ecs/src/ecs/sparse_set.zig @@ -1,5 +1,6 @@ const std = @import("std"); const warn = std.debug.warn; +const utils = @import("utils.zig"); const ReverseSliceIterator = @import("utils.zig").ReverseSliceIterator; // TODO: fix entity_mask. it should come from EntityTraitsDefinition. @@ -38,7 +39,7 @@ pub fn SparseSet(comptime SparseT: type) type { allocator.destroy(self); } - fn page(self: Self, sparse: SparseT) usize { + pub fn page(self: Self, sparse: SparseT) usize { // TODO: support paging // return (sparse & EntityTraits.entity_mask) / sparse_per_page; return sparse & self.entity_mask; @@ -141,10 +142,19 @@ pub fn SparseSet(comptime SparseT: type) type { } /// Sort elements according to the given comparison function - pub fn sort(self: *Self, sortFn: fn (SparseT, SparseT) bool) void { - std.sort.insertionSort(SparseT, self.dense.items, sortFn); + pub fn sort(self: *Self, comptime sortFn: fn (void, SparseT, SparseT) bool) void { + std.sort.insertionSort(SparseT, self.dense.items, {}, sortFn); + + for (self.dense.items) |sparse| { + // self.assure(self.page(sparse))[self.offset(sparse)] = @intCast(SparseT, sparse); + self.sparse.items[self.page(sparse)] = @intCast(SparseT, sparse); + } + } + + /// 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); - var i = @as(usize, 0); for (self.dense.items) |sparse| { // self.assure(self.page(sparse))[self.offset(sparse)] = @intCast(SparseT, sparse); self.sparse.items[self.page(sparse)] = @intCast(SparseT, sparse); @@ -259,7 +269,7 @@ test "iterate" { set.add(2); set.add(3); - var i: u32 = @intCast(u32, set.len()) - 1; + var i: u32 = @intCast(u32, set.len()) - 1; var iter = set.reverseIterator(); while (iter.next()) |entity| { std.testing.expectEqual(i, entity); @@ -290,6 +300,8 @@ test "respect 1" { std.testing.expectEqual(set1.dense.items[1], set2.dense.items[2]); } +const desc_u32 = std.sort.desc(u32); + test "respect 2" { var set = SparseSet(u32).initPtr(std.testing.allocator); defer set.deinit(); @@ -300,11 +312,11 @@ test "respect 2" { set.add(1); set.add(3); - set.sort(std.sort.desc(u32)); + set.sort(desc_u32); for (set.dense.items) |item, i| { if (i < set.dense.items.len - 1) { std.debug.assert(item > set.dense.items[i + 1]); } } -} \ No newline at end of file +} diff --git a/zig-ecs/src/ecs/utils.zig b/zig-ecs/src/ecs/utils.zig index 0f45efc..770ade1 100644 --- a/zig-ecs/src/ecs/utils.zig +++ b/zig-ecs/src/ecs/utils.zig @@ -49,13 +49,13 @@ pub fn ReverseSliceIterator(comptime T: type) type { } /// 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 { +pub fn sortSub(comptime T1: type, comptime T2: type, items: []T1, sub_items: []T2, lessThan: fn (void, 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) { + while (j > 0 and lessThan({}, x, items[j - 1])) : (j -= 1) { items[j] = items[j - 1]; sub_items[j] = sub_items[j - 1]; } diff --git a/zig-ecs/tests/groups_test.zig b/zig-ecs/tests/groups_test.zig index f8c5d55..7a3407a 100644 --- a/zig-ecs/tests/groups_test.zig +++ b/zig-ecs/tests/groups_test.zig @@ -14,12 +14,41 @@ 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("{:3.0}", .{e}); - warn(" ({d:3.0})", .{store.instances.items[i]}); + warn("[{}] {}", .{e, store.set.sparse.items[store.set.page(store.set.dense.items[i])]}); + warn(" ({d:.2}) ", .{store.instances.items[i]}); } warn("\n", .{}); } +const asc_u32 = std.sort.asc(u32); +const desc_u32 = std.sort.desc(f32); + +test "sort component" { + std.debug.warn("\n", .{}); + + var store = ecs.ComponentStorage(f32, u32).initPtr(std.testing.allocator); + defer store.deinit(); + + store.add(1, @as(f32, 1.1)); + store.add(2, @as(f32, 2.2)); + store.add(0, @as(f32, 0.0)); + + printStore(store, "Fucker"); + + store.sort(u32, asc_u32); + for (store.raw()) |e, i| { + // std.debug.warn("{}: {}\n", .{i, e}); + // std.testing.expectEqual(@intCast(u32, i), e); + } + + printStore(store, "Fucker"); + + store.sort(f32, desc_u32); + for (store.raw()) |e, i| { + // std.testing.expectEqual(counter, e); + } +} + test "nested OwningGroups add/remove components" { var reg = Registry.init(std.testing.allocator); defer reg.deinit();