211 lines
7.3 KiB
Zig
211 lines
7.3 KiB
Zig
|
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 {
|
||
|
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 {
|
||
|
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.
|
||
|
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);
|
||
|
}
|