Hello World!
This commit is contained in:
commit
6cabc41276
15 changed files with 1196 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
.zig-cache/
|
||||
zig-pkg/
|
||||
70
README.md
Normal file
70
README.md
Normal file
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
24
build.zig
Normal file
24
build.zig
Normal file
|
|
@ -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);
|
||||
}
|
||||
17
build.zig.zon
Normal file
17
build.zig.zon
Normal file
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
144
src/archetype.zig
Normal file
144
src/archetype.zig
Normal file
|
|
@ -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;
|
||||
}
|
||||
182
src/column.zig
Normal file
182
src/column.zig
Normal file
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
61
src/column_typed.zig
Normal file
61
src/column_typed.zig
Normal file
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
12
src/entity.zig
Normal file
12
src/entity.zig
Normal file
|
|
@ -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});
|
||||
}
|
||||
2
src/entity_record.zig
Normal file
2
src/entity_record.zig
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
archetype_idx: usize,
|
||||
row: usize,
|
||||
141
src/query.zig
Normal file
141
src/query.zig
Normal file
|
|
@ -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
|
||||
];
|
||||
}
|
||||
};
|
||||
}
|
||||
2
src/query_filters.zig
Normal file
2
src/query_filters.zig
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
with: []const type = &.{},
|
||||
without: []const type = &.{},
|
||||
3
src/root.zig
Normal file
3
src/root.zig
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub const World = @import("world.zig");
|
||||
pub const Entity = @import("entity.zig");
|
||||
pub const Archetype = @import("archetype.zig");
|
||||
25
src/signature.zig
Normal file
25
src/signature.zig
Normal file
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
5
src/type_id.zig
Normal file
5
src/type_id.zig
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub fn of(comptime T: type) u64 {
|
||||
return std.hash.Wyhash.hash(694721, @typeName(T));
|
||||
}
|
||||
506
src/world.zig
Normal file
506
src/world.zig
Normal file
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue