commit 6cabc41276cd4cb9c6105cd1cc423c67cd38e300 Author: abux Date: Sat Aug 1 21:34:45 2026 +0100 Hello World! diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5858b8d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.zig-cache/ +zig-pkg/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..343c1a3 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# NYXWorld + +Low cortisol simple + **Archetype** + **ECS** + +# Examples + +**Basic** +```zig +const Exclude = struct {}; +const Zombie = struct {}; +const Player = struct { name: []const u8 }; +const Vehicle = struct {}; +const Transform = struct { pos: @Vector(2, f32) = .{ 0, 0 }, scale: @Vector(2, f32) = .{ 32, 32 } }; +const Velocity = struct { value: @Vector(2, f32) = .{ 0, 0 } }; + +test "basic" { + // --- WORLD --- + var world: Self = .init(std.testing.allocator); + defer world.deinit(); + + // --- PLAYER --- + try world.spawn(.{ + Player{ .name = "abux" }, + Transform{}, + Velocity{}, + }); + + // --- QUERY --- + var it = world.query(struct { + player: ?*const Player, + zombie: ?*const Zombie, + vehicle: ?*const Velocity, + + transform: *Transform, + velocity: *Velocity, + }, .{ + .without = &.{ + Exclude, + }, + }); + + while (it.next()) |e| { + // --- MOVE --- + e.velocity.value[0] += 2; + e.velocity.value[1] += 4; + + // --- APPLY VELOCITY --- + e.transform.pos[0] += e.velocity.value[0]; + e.transform.pos[1] += e.velocity.value[1]; + + // --- DEBUG PLAYER --- + if (e.player) |player| { + std.debug.print( + \\({f})PLAYER + \\| Name: {s} + \\| Position: {} + \\| Scale: {} + \\ + , .{ + it.entity(), + player.name, + e.transform.pos, + e.transform.scale, + }); + } + } +} +``` diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..9280b62 --- /dev/null +++ b/build.zig @@ -0,0 +1,24 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + // --- TARGET --- + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // --- MOD --- + const mod = b.addModule("NYXWorld", .{ + .root_source_file = b.path("src/world.zig"), + + .target = target, + .optimize = optimize, + }); + + const mod_tests = b.addTest(.{ + .root_module = mod, + }); + + const run_mod_tests = b.addRunArtifact(mod_tests); + + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&run_mod_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..122d4f1 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,17 @@ +.{ + .name = .NYXWorld, + + .version = "0.0.0", + + .fingerprint = 0x3231e01252fb838c, + + .minimum_zig_version = "0.16.0", + + .dependencies = .{}, + + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/src/archetype.zig b/src/archetype.zig new file mode 100644 index 0000000..afde1d2 --- /dev/null +++ b/src/archetype.zig @@ -0,0 +1,144 @@ +// --- LIBS --- +const std = @import("std"); +const Signature = @import("signature.zig"); +const Column = @import("column.zig"); +const Entity = @import("entity.zig"); +const TypeID = @import("type_id.zig").of; +const Self = @This(); + +// +// FIELDS +// + +signature: Signature, +type_map: std.AutoHashMap(u64, usize), +entities: std.ArrayList(Entity), +columns: std.ArrayList(Column), +alloc: std.mem.Allocator, + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn init( + signature: Signature, + alloc: std.mem.Allocator, +) Self { + return .{ + .signature = signature, + .type_map = .init(alloc), + .entities = .empty, + .columns = .empty, + .alloc = alloc, + }; +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn deinit(self: *Self) void { + for (self.columns.items) |*col| { + col.deinit(); + } + + self.columns.deinit(self.alloc); + self.entities.deinit(self.alloc); + self.type_map.deinit(); + + self.alloc.free(self.signature.ids); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn addColumn( + self: *Self, + comptime T: type, +) !void { + const idx = self.columns.items.len; + + try self.columns.append( + self.alloc, + try .init(T, self.alloc), + ); + + try self.type_map.put( + TypeID(T), + idx, + ); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn addColumns( + self: *Self, + components: anytype, +) !void { + inline for (components) |T| { + try self.addColumn(T); + } +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn add( + self: *Self, + entity: Entity, + components: anytype, +) !void { + const fields = std.meta.fields(@TypeOf(components)); + + if (fields.len != self.columns.items.len) { + return error.MissingComponent; + } + + // const row = self.entities.items.len; + try self.entities.append(self.alloc, entity); + + inline for (fields) |field| { + const component = @field(components, field.name); + + const T = @TypeOf(component); + + const idx = self.type_map.get(TypeID(T)) orelse { + return error.ComponentNotFound; + }; + + try self.columns.items[idx].append(&component); + } + + // for (self.columns.items) |*col| { + // if (col. != row + 1) { + // return error.RowDesync; + // } + // } +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn get( + self: *Self, + comptime T: type, + row: usize, +) ?*T { + const idx = self.type_map.get(TypeID(T)) orelse return null; + const ptr = self.columns.items[idx].getPtr(row) orelse return null; + return @ptrCast(@alignCast(ptr)); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn remove( + self: *Self, + row: usize, +) ?Entity { + const last_entity = self.entities.items[self.entities.items.len - 1]; + + _ = self.entities.swapRemove(row); + + for (self.columns.items) |*col| { + col.swapRemove(row); + } + + if (row < self.entities.items.len) { + return last_entity; + } + + return null; +} diff --git a/src/column.zig b/src/column.zig new file mode 100644 index 0000000..e24dd5b --- /dev/null +++ b/src/column.zig @@ -0,0 +1,182 @@ +const std = @import("std"); +const TypeID = @import("type_id.zig").of; +const TypedColumn = @import("column_typed.zig").TypedColumn; +const Self = @This(); + +// +// FIELDS +// + +storage: *anyopaque, + +type_id: u64, + +elem_size: usize, +alignment: usize, + +alloc: std.mem.Allocator, + +append_fn: *const fn ( + storage: *anyopaque, + value: *const anyopaque, + alloc: std.mem.Allocator, +) anyerror!void, + +swap_remove_fn: *const fn ( + storage: *anyopaque, + idx: usize, +) void, + +get_ptr_fn: *const fn ( + storage: *anyopaque, + idx: usize, +) ?*anyopaque, + +deinit_fn: *const fn ( + storage: *anyopaque, + alloc: std.mem.Allocator, +) void, + +init_fn: *const fn ( + alloc: std.mem.Allocator, +) anyerror!Self, + +/// ------------------------------------------ +/// ------------------------------------------ +pub fn init( + comptime T: type, + alloc: std.mem.Allocator, +) !Self { + const typed = try alloc.create(TypedColumn(T)); + + typed.* = TypedColumn(T).init(); + + return .{ + .storage = typed, + + .type_id = TypeID(T), + + .elem_size = @sizeOf(T), + .alignment = @alignOf(T), + + .alloc = alloc, + + .append_fn = struct { + fn f( + storage: *anyopaque, + value: *const anyopaque, + allocator: std.mem.Allocator, + ) !void { + const col: *TypedColumn(T) = @ptrCast(@alignCast(storage)); + try col.append(value, allocator); + } + }.f, + + .swap_remove_fn = struct { + fn f( + storage: *anyopaque, + idx: usize, + ) void { + const col: *TypedColumn(T) = @ptrCast(@alignCast(storage)); + col.swapRemove(idx); + } + }.f, + + .get_ptr_fn = struct { + fn f( + storage: *anyopaque, + idx: usize, + ) ?*anyopaque { + const col: *TypedColumn(T) = @ptrCast(@alignCast(storage)); + if (idx >= col.items.items.len) { + return null; + } + return &col.items.items[idx]; + } + }.f, + + .deinit_fn = struct { + fn f( + storage: *anyopaque, + allocator: std.mem.Allocator, + ) void { + const col: *TypedColumn(T) = @ptrCast(@alignCast(storage)); + col.deinit(allocator); + allocator.destroy(col); + } + }.f, + + .init_fn = struct { + fn f(a: std.mem.Allocator) !Self { + return init(T, a); + } + }.f, + }; +} + +/// ------------------------------------------ +/// ------------------------------------------ +pub fn deinit(self: *Self) void { + self.deinit_fn( + self.storage, + self.alloc, + ); +} + +/// ------------------------------------------ +/// Create a new empty Column of the same type +/// ------------------------------------------ +pub fn cloneEmpty(self: *Self) !Self { + return self.init_fn(self.alloc); +} + +/// ------------------------------------------ +/// Copy a single element from another column +/// Both columns must store the same concrete type +/// ------------------------------------------ +pub fn copyElement( + self: *Self, + other: *Self, + idx: usize, +) !void { + if (other.getPtr(idx)) |ptr| { + try self.append(ptr); + } +} + +/// ------------------------------------------ +/// ------------------------------------------ +pub fn append( + self: *Self, + value: *const anyopaque, +) !void { + try self.append_fn( + self.storage, + value, + self.alloc, + ); +} + +/// ------------------------------------------ +/// ------------------------------------------ +pub fn swapRemove( + self: *Self, + idx: usize, +) void { + self.swap_remove_fn( + self.storage, + idx, + ); +} + +/// ------------------------------------------ +/// ------------------------------------------ +pub fn getPtr( + self: *Self, + idx: usize, +) ?*anyopaque { + return self.get_ptr_fn( + self.storage, + idx, + ); +} diff --git a/src/column_typed.zig b/src/column_typed.zig new file mode 100644 index 0000000..844453a --- /dev/null +++ b/src/column_typed.zig @@ -0,0 +1,61 @@ +const std = @import("std"); + +/// ------------------------------------------ +/// ------------------------------------------ +pub fn TypedColumn(comptime T: type) type { + return struct { + const Self = @This(); + + // + // FIELDS + // + + items: std.ArrayList(T), + + /// ------------------------------------------ + /// ------------------------------------------ + pub fn init() @This() { + return .{ + .items = .empty, + }; + } + + /// ------------------------------------------ + /// ------------------------------------------ + pub fn deinit( + self: *Self, + alloc: std.mem.Allocator, + ) void { + self.items.deinit(alloc); + } + + /// ------------------------------------------ + /// ------------------------------------------ + pub fn append( + self: *Self, + value: *const anyopaque, + alloc: std.mem.Allocator, + ) !void { + const typed: *const T = @ptrCast(@alignCast(value)); + try self.items.append(alloc, typed.*); + } + + /// ------------------------------------------ + /// ------------------------------------------ + pub fn swapRemove( + self: *Self, + idx: usize, + ) void { + _ = self.items.swapRemove(idx); + } + + /// ------------------------------------------ + /// ------------------------------------------ + pub fn getPtr( + self: *Self, + idx: usize, + ) *anyopaque { + return &self.items.items[idx]; + } + }; +} diff --git a/src/entity.zig b/src/entity.zig new file mode 100644 index 0000000..a5696f1 --- /dev/null +++ b/src/entity.zig @@ -0,0 +1,12 @@ +const std = @import("std"); + +pub const IDType = u64; + +id: IDType, + +pub fn format( + self: @This(), + writer: *std.Io.Writer, +) !void { + try writer.print("{d}", .{self.id}); +} diff --git a/src/entity_record.zig b/src/entity_record.zig new file mode 100644 index 0000000..b2ef352 --- /dev/null +++ b/src/entity_record.zig @@ -0,0 +1,2 @@ +archetype_idx: usize, +row: usize, diff --git a/src/query.zig b/src/query.zig new file mode 100644 index 0000000..8eb38f5 --- /dev/null +++ b/src/query.zig @@ -0,0 +1,141 @@ +const std = @import("std"); +const World = @import("world.zig"); +const Archetype = @import("archetype.zig"); +const Entity = @import("entity.zig"); +const QueryFilters = @import("query_filters.zig"); +const TypeID = @import("type_id.zig").of; + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn Query( + comptime Result: type, + comptime Filters: QueryFilters, +) type { + return struct { + const Self = @This(); + + const result_fields = std.meta.fields(Result); + + // + // FIELDS + // + + world: *World, + arch_cursor: usize, + row_cursor: usize, + + current_arch: ?*Archetype, + current_row: usize, + + /// ---------------------------------------------------- + /// ---------------------------------------------------- + pub fn init(world: *World) Self { + return .{ + .world = world, + .arch_cursor = 0, + .row_cursor = 0, + + .current_arch = null, + .current_row = 0, + }; + } + + /// ---------------------------------------------------- + /// ---------------------------------------------------- + fn matches(arch: *Archetype) bool { + // --- REQUIRED COMPONENTS --- + inline for (result_fields) |field| { + // TODO: Clean this shit up + const FieldT = field.type; + const is_optional = @typeInfo(FieldT) == .optional; + + const PtrT = if (is_optional) @typeInfo(FieldT).optional.child else FieldT; + const T = std.meta.Child(PtrT); + + if (!arch.type_map.contains(TypeID(T))) { + // --- SKIP OPTIONALS --- + if (!is_optional) { + return false; + } + } + } + + // --- WITH --- + inline for (Filters.with) |T| { + if (!arch.type_map.contains(TypeID(T))) { + return false; + } + } + + // --- WITHOUT --- + inline for (Filters.without) |T| { + if (arch.type_map.contains(TypeID(T))) { + return false; + } + } + + return true; + } + + /// ---------------------------------------------------- + /// ---------------------------------------------------- + pub fn next(self: *Self) ?Result { + while (self.arch_cursor < self.world.archetypes.items.len) { + var arch = &self.world.archetypes.items[self.arch_cursor]; + + // --- SKIP ARCHETYPES --- + if (!matches(arch)) { + self.arch_cursor += 1; + self.row_cursor = 0; + continue; + } + + // --- END ROWS --- + if (self.row_cursor >= arch.entities.items.len) { + self.arch_cursor += 1; + self.row_cursor = 0; + continue; + } + + const row = self.row_cursor; + + self.row_cursor += 1; + self.current_arch = arch; + self.current_row = row; + + var result: Result = undefined; + + inline for (result_fields) |field| { + // TODO: Clean this shit up + const FieldT = field.type; + + const is_optional = @typeInfo(FieldT) == .optional; + + const PtrT = if (is_optional) @typeInfo(FieldT).optional.child else FieldT; + + const T = std.meta.Child(PtrT); + + const ptr = arch.get(T, row); + + if (is_optional) { + @field(result, field.name) = ptr; + } else { + @field(result, field.name) = ptr orelse return null; + } + } + + return result; + } + + return null; + } + + /// ---------------------------------------------------- + /// ---------------------------------------------------- + pub fn entity(self: *const Self) Entity { + return self.current_arch.?.entities.items[ + self.current_row + ]; + } + }; +} diff --git a/src/query_filters.zig b/src/query_filters.zig new file mode 100644 index 0000000..4f28ecd --- /dev/null +++ b/src/query_filters.zig @@ -0,0 +1,2 @@ +with: []const type = &.{}, +without: []const type = &.{}, diff --git a/src/root.zig b/src/root.zig new file mode 100644 index 0000000..41dc228 --- /dev/null +++ b/src/root.zig @@ -0,0 +1,3 @@ +pub const World = @import("world.zig"); +pub const Entity = @import("entity.zig"); +pub const Archetype = @import("archetype.zig"); diff --git a/src/signature.zig b/src/signature.zig new file mode 100644 index 0000000..8167e45 --- /dev/null +++ b/src/signature.zig @@ -0,0 +1,25 @@ +// --- LIBS --- +const std = @import("std"); +const TypeID = @import("type_id.zig").of; +const Self = @This(); + +// +// FIELDS +// + +ids: []u64, + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn eql(a: Self, b: Self) bool { + return std.mem.eql(u64, a.ids, b.ids); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn hash(self: Self) u64 { + return std.hash.Wyhash.hash( + 25821, + std.mem.sliceAsBytes(self.ids), + ); +} diff --git a/src/type_id.zig b/src/type_id.zig new file mode 100644 index 0000000..68c62f3 --- /dev/null +++ b/src/type_id.zig @@ -0,0 +1,5 @@ +const std = @import("std"); + +pub fn of(comptime T: type) u64 { + return std.hash.Wyhash.hash(694721, @typeName(T)); +} diff --git a/src/world.zig b/src/world.zig new file mode 100644 index 0000000..faa3df4 --- /dev/null +++ b/src/world.zig @@ -0,0 +1,506 @@ +//! ---------------------------------------------------- +//! ---------------------------------------------------- + +// +// PRIVATE +// + +const std = @import("std"); +const Archetype = @import("archetype.zig"); +const Signature = @import("signature.zig"); +const EntityRecord = @import("entity_record.zig"); +const QueryFilters = @import("query_filters.zig"); +const Query = @import("query.zig").Query; +const TypeID = @import("type_id.zig").of; +const Self = @This(); + +// +// PUBLIC +// + +pub const Entity = @import("entity.zig"); + +// +// FIELDS +// + +archetypes: std.ArrayList(Archetype), +entity_map: std.AutoHashMap(Entity, EntityRecord), +signature_map: std.AutoHashMap(u64, usize), +next_entity: Entity.IDType, +alloc: std.mem.Allocator, + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn init(alloc: std.mem.Allocator) Self { + return .{ + .archetypes = .empty, + .entity_map = .init(alloc), + .signature_map = .init(alloc), + .next_entity = 1, + .alloc = alloc, + }; +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn deinit(self: *Self) void { + // --- ARCHETYPES --- + for (self.archetypes.items) |*arch| { + arch.deinit(); + } + self.entity_map.deinit(); + self.archetypes.deinit(self.alloc); + self.signature_map.deinit(); +} + +/// ---------------------------------------------------- +/// # Example +/// +/// ```zig +/// try world.spawn(.{ +/// Player{}, +/// Transform{}, +/// Velocity{}, +/// }); +/// ``` +/// ---------------------------------------------------- +pub fn spawn( + self: *Self, + components: anytype, +) !void { + _ = try self.spawnEntity(components); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn addComponents( + self: *Self, + entity: Entity, + components: anytype, +) !void { + const record = self.entity_map.get(entity) orelse return; + const new_fields = std.meta.fields(@TypeOf(components)); + const old_count = self.archetypes.items[record.archetype_idx].signature.ids.len; + + // --- BUILD COMBINED SIGNATURE (dedup against existing) --- + var ids = try self.alloc.alloc(u64, old_count + new_fields.len); + defer self.alloc.free(ids); + + for (self.archetypes.items[record.archetype_idx].signature.ids, 0..) |id, i| { + ids[i] = id; + } + + var total: usize = old_count; + inline for (new_fields) |field| { + const component = @field(components, field.name); + const id = TypeID(@TypeOf(component)); + var dominated = false; + + for (ids[0..total]) |existing| { + if (existing == id) { + dominated = true; + break; + } + } + + if (!dominated) { + ids[total] = id; + total += 1; + } + } + + if (total == old_count) return; + + std.mem.sort(u64, ids[0..total], {}, std.sort.asc(u64)); + const signature_hash = std.hash.Wyhash.hash(0, std.mem.sliceAsBytes(ids[0..total])); + + // --- FIND OR CREATE NEW ARCHETYPE --- + const new_arch_idx = + self.signature_map.get(signature_hash) orelse blk: { + const old = &self.archetypes.items[record.archetype_idx]; + const sig_ids = try self.alloc.dupe(u64, ids[0..total]); + var arch: Archetype = .init(.{ .ids = sig_ids }, self.alloc); + + // --- CLONE OLD COLUMNS --- + for (old.columns.items) |*col| { + try arch.columns.append(self.alloc, try col.cloneEmpty()); + } + + // --- COPY OLD TYPE MAPPINGS --- + var old_iter = old.type_map.iterator(); + while (old_iter.next()) |entry| { + try arch.type_map.put(entry.key_ptr.*, entry.value_ptr.*); + } + + // --- ADD NEW COLUMNS --- + inline for (new_fields) |field| { + const component = @field(components, field.name); + const T = @TypeOf(component); + var already = false; + for (old.signature.ids) |old_id| { + if (old_id == TypeID(T)) { + already = true; + break; + } + } + if (!already) try arch.addColumn(T); + } + + try self.archetypes.append(self.alloc, arch); + const idx = self.archetypes.items.len - 1; + try self.signature_map.put(signature_hash, idx); + break :blk idx; + }; + + // --- RE-FETCH --- + const old_arch = &self.archetypes.items[record.archetype_idx]; + const new_arch = &self.archetypes.items[new_arch_idx]; + + // --- COPY OLD COMPONENT VALUES --- + try new_arch.entities.append( + self.alloc, + entity, + ); + + for (old_arch.columns.items, 0..) |*old_col, i| { + if (old_col.getPtr(record.row)) |ptr| { + try new_arch.columns.items[i].append(ptr); + } + } + + // --- ADD NEW COMPONENT VALUES --- + inline for (new_fields) |field| { + const component = @field(components, field.name); + const T = @TypeOf(component); + + var already = false; + for (old_arch.signature.ids) |old_id| { + if (old_id == TypeID(T)) { + already = true; + break; + } + } + if (!already) { + const idx = new_arch.type_map.get(TypeID(T)) orelse return error.ComponentNotFound; + try new_arch.columns.items[idx].append(&component); + } + } + + // --- REMOVE FROM OLD ARCHETYPE --- + if (old_arch.remove(record.row)) |moved| { + self.entity_map.getPtr(moved).?.row = record.row; + } + + // --- UPDATE ENTITY MAP --- + self.entity_map.getPtr(entity).?.archetype_idx = new_arch_idx; + self.entity_map.getPtr(entity).?.row = new_arch.entities.items.len - 1; +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn getComponent( + self: *Self, + comptime T: type, + entity: Entity, +) ?*T { + const record = self.entity_map.get(entity) orelse return null; + const arch = &self.archetypes.items[record.archetype_idx]; + return arch.get(T, record.row); +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn getComponents( + self: *Self, + comptime Result: type, + entity: Entity, +) error{ComponentNotRegistered}!Result { + // --- RESULT --- + var result: Result = undefined; + + // --- GET FIELDS --- + inline for (std.meta.fields(Result)) |field| { + // --- FIELD INFO --- + const FieldType = field.type; + + switch (@typeInfo(FieldType)) { + .optional => |opt| { + const ptr_info = @typeInfo(opt.child).pointer; + @field(result, field.name) = self.getComponent(ptr_info.child, entity); + }, + + .pointer => |ptr| { + @field(result, field.name) = + self.getComponent(ptr.child, entity) orelse + return error.ComponentNotRegistered; + }, + + else => @compileError( + "getComponents fields must be *T or ?*T; field '" ++ field.name ++ "' has type '" ++ + @typeName(FieldType) ++ "'", + ), + } + } + + return result; +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +pub fn hasComponent( + self: *Self, + comptime T: type, + entity: Entity, +) bool { + return self.getComponent(T, entity) != null; +} + +/// ---------------------------------------------------- +/// # Example +/// +/// ```zig +/// const player = try world.spawnEntity(.{ +/// Player{}, +/// Transform{}, +/// Velocity{}, +/// }); +/// +/// world.despawn(player); +/// ``` +/// ---------------------------------------------------- +pub fn despawn( + self: *Self, + entity: Entity, +) void { + const record = self.entity_map.get(entity) orelse return; + if (self.archetypes.items[record.archetype_idx].remove(record.row)) |moved| { + self.entity_map.getPtr(moved).?.row = record.row; + } + _ = self.entity_map.remove(entity); +} + +/// ---------------------------------------------------- +/// # Example +/// +/// ```zig +/// const player = try world.spawnEntity(.{ +/// Player{}, +/// Transform{}, +/// Velocity{}, +/// }); +/// ``` +/// ---------------------------------------------------- +pub fn spawnEntity( + self: *Self, + components: anytype, +) !Entity { + const Components = @TypeOf(components); + + const fields = std.meta.fields(Components); + + // --- SIGNATURE --- + var ids: [fields.len]u64 = undefined; + inline for (fields, 0..) |field, i| { + const component = @field( + components, + field.name, + ); + ids[i] = TypeID(@TypeOf(component)); + } + + // --- SORT TYPE ID'S --- + std.mem.sort( + u64, + &ids, + {}, + std.sort.asc(u64), + ); + + const signature_hash = std.hash.Wyhash.hash(0, std.mem.sliceAsBytes(&ids)); + + // --- FIND OR CREATE ARCHETYPE --- + const archetype_idx = + self.signature_map.get(signature_hash) orelse blk: { + const sig_ids = try self.alloc.dupe(u64, &ids); + + var arch: Archetype = .init(.{ .ids = sig_ids }, self.alloc); + + // --- NEW COLUMNS --- + inline for (fields) |field| { + const component = @field(components, field.name); + try arch.addColumn(@TypeOf(component)); + } + + // --- ADD ARCHETYPE --- + try self.archetypes.append( + self.alloc, + arch, + ); + + // --- ADD SIGNATURE --- + const idx = self.archetypes.items.len - 1; + try self.signature_map.put( + signature_hash, + idx, + ); + + break :blk idx; + }; + + // --- NEW ENTITY --- + const entity = self.createEntity(); + + // --- ADD ROW --- + var arch = &self.archetypes.items[archetype_idx]; + + const row = arch.entities.items.len; + + try arch.add( + entity, + components, + ); + + // --- STORE ENTITY LOCATION --- + try self.entity_map.put(entity, .{ + .archetype_idx = archetype_idx, + .row = row, + }); + + return entity; +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +fn createEntity(self: *Self) Entity { + const entity = Entity{ .id = self.next_entity }; + self.next_entity += 1; + return entity; +} + +/// ---------------------------------------------------- +/// ---------------------------------------------------- +fn createArchetype( + self: *Self, + comptime Components: type, +) !usize { + const fields = + std.meta.fields(Components); + + var ids = try self.alloc.alloc( + u64, + fields.len, + ); + + inline for (fields, 0..) |field, i| { + ids[i] = TypeID(field.type); + } + + const sig: Signature = .{ + .ids = ids, + }; + + var arch: Archetype = .init( + sig, + self.alloc, + ); + + inline for (fields) |field| { + try arch.addColumn( + field.type, + ); + } + + try self.archetypes.append( + self.alloc, + arch, + ); + + return self.archetypes.items.len - 1; +} + +/// ---------------------------------------------------- +/// # Example +/// +/// ```zig +/// var query = world.query(struct { +/// player: ?*Player, +/// pos: *Position, +/// vel: *Velocity, +/// }, .{}); +/// while (query.next()) |entity| { +/// if (entity.player) |player| {} +/// } +/// ``` +/// ---------------------------------------------------- +pub fn query( + self: *Self, + comptime Result: type, + comptime Filters: QueryFilters, +) Query(Result, Filters) { + return .init(self); +} + +// +// TESTING +// + +const Exclude = struct {}; +const Zombie = struct {}; +const Player = struct { name: []const u8 }; +const Vehicle = struct {}; +const Transform = struct { pos: @Vector(2, f32) = .{ 0, 0 }, scale: @Vector(2, f32) = .{ 32, 32 } }; +const Velocity = struct { value: @Vector(2, f32) = .{ 0, 0 } }; + +test "basic" { + // --- WORLD --- + var world: Self = .init(std.testing.allocator); + defer world.deinit(); + + // --- PLAYER --- + try world.spawn(.{ + Player{ .name = "abux" }, + Transform{}, + Velocity{}, + }); + + // --- QUERY --- + var it = world.query(struct { + player: ?*const Player, + zombie: ?*const Zombie, + vehicle: ?*const Velocity, + + transform: *Transform, + velocity: *Velocity, + }, .{ + .without = &.{ + Exclude, + }, + }); + + while (it.next()) |e| { + // --- MOVE --- + e.velocity.value[0] += 2; + e.velocity.value[1] += 4; + + // --- APPLY VELOCITY --- + e.transform.pos[0] += e.velocity.value[0]; + e.transform.pos[1] += e.velocity.value[1]; + + // --- DEBUG PLAYER --- + if (e.player) |player| { + std.debug.print( + \\({f}) PLAYER + \\| Name: {s} + \\| Position: {} + \\| Scale: {} + \\ + , .{ + it.entity(), + player.name, + e.transform.pos, + e.transform.scale, + }); + } + } +}