Compare commits

...

180 Commits

Author SHA1 Message Date
RedStealthDev 4451fe4224 fixed patch
10 months ago
RedStealthDev 30129f259e Merge remote-tracking branch 'zig-ecs/Patch'
10 months ago
RedStealthDev 630577f550 fix build system
10 months ago
prime31 b863956c67 Merge pull request #36 from menduz/master
1 year ago
menduz 531ae84819 fix tests
1 year ago
prime31 0c73e99107 Merge pull request #34 from menduz/master
1 year ago
menduz 55f7813640 consistent naming for entityIterator among view implementations
1 year ago
menduz 945dd3ec65 Merge branch 'prime31:master' into master
1 year ago
menduz 1a798a9c5d expose utils and EntityHandles for iterator typing
1 year ago
prime31 e6235c93f9 Merge pull request #33 from menduz/master
2 years ago
menduz bdcd98a989 fix: signals for addOrReplace
2 years ago
menduz 13852c2e08 zig fmt + expose SparseSet
2 years ago
prime31 da441c3363 Delete .github/workflows directory
2 years ago
prime31 d49c58c314 Merge pull request #32 from menduz/patch-1
2 years ago
menduz a4d2e17f60 fix: add missing comptime
2 years ago
prime31 7d2478c901 Merge pull request #31 from Tekkunsan/zig-0.11.0-dev.3747-changes
2 years ago
Aron Gabriel c7a1353847 use `std.sort.pdq()` instead of `std.sort.block()`
2 years ago
Aron Gabriel 13ea2be1c1 Fix `@call` usage errors and duplicate test names
2 years ago
Aron Gabriel 5045a4fa33 use `@max` instead of `std.math.max`
2 years ago
Aron Gabriel e67ec88690 change @srcToDest to @destFromSrc
2 years ago
prime31 f714405297 Merge pull request #30 from Tekkunsan/module-visibility
2 years ago
Aron Gabriel 1e917df772 use `addModule` instead of `createModule`
2 years ago
prime31 bb433389b7 Merge pull request #29 from overlandandseas/master
2 years ago
Lucas 217b1a9e51 Update to latest zig release
2 years ago
prime31 69afaeab3f Merge pull request #28 from stefanpartheym/master
2 years ago
Stefan Partheymüller 94f38a3d89 make source files and build file compatible with current zig compiler 0.11.0-dev.1580+a5b34a61a
2 years ago
Stefan Partheymüller e0eb68dd5b change Iterator implementation in MultiView struct to use already existing ReverseSliceIterator
2 years ago
Stefan Partheymüller 701fbfb4db invoke singletons() function to satisfy compiler
2 years ago
Stefan Partheymüller b1b8321fa4 change comptime access to typeInfo of load function to satisfy compiler / use function pointer in order to make @field() work
2 years ago
Stefan Partheymüller 386113ac5f copy and fix function pointer syntax in function std.sort.insertionSort to avoid compliation error
2 years ago
Stefan Partheymüller 42b2a57d1c fix invokation of sort function
2 years ago
Stefan Partheymüller 9bbff920bb exclude generated docs directory from repo
2 years ago
Stefan Partheymüller 6f0eacc8ca add name for test case
2 years ago
Stefan Partheymüller 4196c9186c fix some compilation errors caused by language changes in zig v0.10.0
2 years ago
prime31 c047390426 Merge pull request #26 from JustinRyanH/master
3 years ago
Justin Ryan Hurst e3e039ba82 fix: Fix add package for zig 0.9.0 and master 0b22
3 years ago
prime31 d3aa1efe38 Merge pull request #25 from JacobHin2/patch-1
3 years ago
Jacob b630099aca Update build.zig
3 years ago
Jacob Hinchliffe 1836e941fd Generate documentation from doc comments.
3 years ago
prime31 9bf6b4af03 Merge pull request #24 from vvcarvalho/rename-singleton-field
3 years ago
Victor carvalho a294babecc mod(registry.zig): rename singletons field, return pointer *TypeStore.
3 years ago
prime31 6852f38a83 Update README.md
3 years ago
Mike e589bae3b0 latest zig support
3 years ago
prime31 a7d224247b Merge pull request #23 from rudedogg/master
3 years ago
Austin Rude 253c2ca87a Use allocator.dupe() instead of std.mem.dupe()
3 years ago
Austin Rude 0fe6d1646f Use std.debug.print instead of std.debug.warn
3 years ago
Austin Rude a95c85f5c7 Update for allocgate
3 years ago
Austin Rude 157a554e6d Update build.zig to use the std.target.Arch enum
3 years ago
prime31 a14bfe64c3 Merge pull request #22 from Sashiri/builtin_migration
3 years ago
Sashiri 5ba2773f7a Migrate from std.builtin to @import("builtin")
3 years ago
prime31 96ffd50ea6 Merge pull request #21 from Sashiri/entity_traits_definition
3 years ago
prime31 c93429bf3d Merge pull request #20 from Sashiri/undefined_type_fix
3 years ago
Sashiri aa0dd139b2 Update EntityTraitsDefinition
3 years ago
Sashiri e4dc1dc9ca Fix process pause signature
3 years ago
prime31 2bc293066a Merge pull request #19 from locriacyber/master
3 years ago
Locria Cyber 605693b8c2 Fix syntax errors
3 years ago
prime31 434aa523a3 Create LICENSE
3 years ago
prime31 e079ac5808 Merge pull request #17 from RUSshy/fix_memleak_2
3 years ago
RUSshy 2161bf61b4 Added missing entity traits changes
3 years ago
prime31 0d89c0e255 Merge pull request #16 from RUSshy/master
3 years ago
RUSshy 4a480f84d2 Merge pull request #1 from RUSshy/fix_memleak
3 years ago
RUSshy 15d0e6d00d Update registry.zig
3 years ago
RUSshy 22b4fe2e02 Update handles.zig
3 years ago
RUSshy e488cc384f Update sparse_set.zig
3 years ago
RUSshy b900dfde2d Update handles.zig
3 years ago
prime31 7fd238b638 Merge pull request #10 from leroycep/update-to-0.8.0
4 years ago
LeRoyce Pearson 33a5344b10 Remove unused variables and redudant `comptime`s
4 years ago
LeRoyce Pearson 44192a1398 Update tests to return errors
4 years ago
prime31 9b467c6ae8 Merge pull request #12 from Nomy1/fix-util-error
4 years ago
Jonathan Bro 39af857bdc Resolve use of undeclared identifier 'slice' error
4 years ago
prime31 39219bafd9 Merge pull request #11 from nektro/patch-1
4 years ago
Meghan 04fae5d871 Create zig.mod
4 years ago
LeRoyce Pearson 514a893918 Update Registry.removeAll to 0.8.0 HashMap API
4 years ago
LeRoyce Pearson 6df575ee04 Update some uses of HashMap to match 0.8.0 changes
4 years ago
prime31 9b8e051017 Merge pull request #9 from leroycep/master
4 years ago
LeRoyce Pearson efb8127b78 Update to zig 0.8.0-dev.1721
4 years ago
Mike 6233d751cf set output dir for all exes
4 years ago
Mike e3291e3012 dont foget windows
4 years ago
Mike b7c71e51e7 fix commas
4 years ago
Mike b8e6f16d6f cleaned
4 years ago
Mike 5840b0709f Merge branch 'master' of github.com:prime31/zig-ecs
4 years ago
Mike 3b279cec59 better build
4 years ago
Mike Desaro b398969be6 fix cache dir
4 years ago
Mike Desaro a9b1f60f5a Merge branch 'master' of github.com:prime31/zig-ecs
4 years ago
Mike Desaro 41a5180ece ignore mac files
4 years ago
Mike Desaro 3591aac019 snake case
4 years ago
Mike Desaro b8117306fa zig 0.7 fixes
4 years ago
prime31 dec796d256 Merge pull request #8 from foxnne/pkg
4 years ago
foxnne 737e5a9b39 fix error in linkArtifact
4 years ago
foxnne 3c540293fd add getPackage to build.zig
4 years ago
Mike Desaro ae22b69026 arm64 support
4 years ago
Mike Desaro 695e59bbf5 hack to validate unsigned. closes #7
4 years ago
prime31 069d86b1c9 Merge pull request #6 from mbcrocci/fix-removeAll
4 years ago
Maurizio Crocci 6e412489a2 Fixes Registry.removeAll iteration
4 years ago
prime31 6477d18a1a Merge pull request #5 from codehz/fix
4 years ago
CodeHz f1171b78c8 fix for ziglang pull 6246
4 years ago
CodeHz 6b4ed304a6 fix iterate hashmap
4 years ago
prime31 5360be5b0e Update README.md
4 years ago
prime31 427c925bc5 Update ci.yml
4 years ago
prime31 19c25c9ca4 Update ci.yml
4 years ago
prime31 25a051981c Update ci.yml
4 years ago
prime31 d401f239b6 Update ci.yml
4 years ago
prime31 94c451990e Create ci.yml
4 years ago
prime31 d5bfda0a64 Merge pull request #3 from joachimschmidt557/hashmap-api
4 years ago
joachimschmidt557 aa9c20d4b5 Update to latest HashMap API
4 years ago
prime31 6a19b1cd93 Merge pull request #1 from joachimschmidt557/update-latest-zig
4 years ago
joachimschmidt557 c0a10ee49d Update to latest zig
4 years ago
Mike Desaro a18a627f98 bump for new zig version
4 years ago
prime31 d6cfdbe671 Update README.md
4 years ago
Mike 9ca3e90b7b update to latest zig version
4 years ago
Mike 76e322a49c sorting test
5 years ago
Mike 46c2af939e bug fixes and speedup
5 years ago
Mike cc92e4a225 direct bench
5 years ago
Mike 7874ddf6b6 valgrinded
5 years ago
Mike 69ff91ad7b Merge branch 'master' of github.com-p31:prime31/zig-ecs
5 years ago
Mike c61f6e998d valgrinding
5 years ago
Mike 57a27bd57d dont use testing allocator
5 years ago
Mike df926f965c fix group bugs
5 years ago
Mike e76971101b fixes and tests
5 years ago
Mike 73080c2cfc add view vs group
5 years ago
Mike 2beaeedb6f scheduler done
5 years ago
Mike dd52ed2f46 comments
5 years ago
Mike cdef61d6dc Schedular working
5 years ago
Mike a352f8350c start of scheduler
5 years ago
Mike 9dd7ca445a cleanup sort
5 years ago
Mike 96385b6c61 cleanup sort
5 years ago
Mike 6b34565a2a entity iterator
5 years ago
Mike 7c7b50aefa cleaning
5 years ago
Mike dd5285875e clear clears
5 years ago
Mike 230afa8e4c the monster update to paged sparse sets
5 years ago
Mike 735830994f add len to all sorts for owned groups
5 years ago
Mike 34b2702333 progress on sort
5 years ago
Mike e098c99632 basic group sorting
5 years ago
Mike c8c58255f6 cleanup type names
5 years ago
Mike 2f90bc20f4 comment
5 years ago
Mike 6aab3abe44 revert
5 years ago
Mike 491adbb8c5 fake commit to hold the sort context code
5 years ago
Mike 52aef18b35 sorting a Storage works now
5 years ago
Mike 8235f3dc7f wip
5 years ago
Mike ac17a79631 more sorting...
5 years ago
Mike bdb4b0537d udpate to Zig master
5 years ago
Mike f2a302b7a4 cleaning
5 years ago
Mike f0c711123e debug only
5 years ago
Mike 896095fc15 everything iterates backwards
5 years ago
Mike e32e9069d8 validation
5 years ago
Mike 1d283cfddd include both!
5 years ago
Mike c9d228bf19 cleaner
5 years ago
Mike 3c57820e02 implemented each for owned groups
5 years ago
Mike 2561b78a5f owning group iterator monster
5 years ago
Mike 81ccd19b65 ptr and slice access to entities
5 years ago
Mike 7dd8a28dbe massive group simplification
5 years ago
Mike 21a51a9298 order doesnt matter anymore for types
5 years ago
Mike 088a11b840 bit of tests for nested groups
5 years ago
Mike 8f401ec491 big work on owned groups
5 years ago
Mike 917ca5ffdc allow inserts
5 years ago
Mike 1234b801fd cleanup
5 years ago
Mike 37af42daaa singletons
5 years ago
Mike 07905d0b09 resources pretty done
5 years ago
Mike a547add727 to utils
5 years ago
Mike 3fa455dc48 asset loader
5 years ago
Mike 96cbb37476 better string hash
5 years ago
Mike bb688e52f4 crap
5 years ago
Mike 5b62f55123 tests
5 years ago
Mike d6356a6d6b new cache
5 years ago
Mike faeedaf43d simplify sparse set
5 years ago
Mike b32e907fd4 TypeMap gone
5 years ago
Mike df29819b11 start of owning groups
5 years ago
Mike 1d1c761c15 sort and respect
5 years ago
Mike 4cd3d37657 basic group ready kind of
5 years ago
Mike 8fd20e3b42 tests
5 years ago
Mike 3dc2e33461 BasicGroup mostly working
5 years ago
Mike db332e82d9 cleanup
5 years ago
Mike 2ed3be236b start of groups
5 years ago
Mike b280002223 bug fixes and moving stuff around
5 years ago
Mike 61d882d1a8 add excludes to Views
5 years ago
Mike 0b2542d292 add signals
5 years ago
Mike 5bf1754dfb removed component contexts
5 years ago
Mike ed23befd26 formatting
5 years ago
Mike f7e2c263ba debugging and vscode stuffs
5 years ago
Mike 5589130bfd initial commit
5 years ago

4
.gitmodules vendored

@ -1,7 +1,3 @@
[submodule "raylib-zig"]
path = raylib-zig
url = https://github.com/Not-Nik/raylib-zig.git
[submodule "zig-ecs"]
path = zig-ecs
url = https://github.com/prime31/zig-ecs.git
branch = Patch

@ -1 +0,0 @@
Subproject commit abf785549393a14fd5d0350899e0786fa32d88c5

@ -0,0 +1,6 @@
zig-cache
zig-arm-cache
/docs/
.DS_Store

@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "lldb Debug ecs binary",
"type": "lldb",
"request": "launch",
"program": "${workspaceFolder}/zig-cache/bin/ecs",
"args": [],
"preLaunchTask": "Build Project",
}
]
}

@ -0,0 +1,122 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"options": {
"env": {
"ZIG_SYSTEM_LINKER_HACK": "1"
}
},
"tasks": [
{
"label": "Build Project",
"type": "shell",
"command": "zig build",
"problemMatcher": [
"$gcc"
]
},
{
"label": "Build and Run Project",
"type": "shell",
"command": "zig build run",
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"clear": true
}
},
{
"label": "Build and Run Project (x64 on arm)",
"type": "shell",
"command": "~/zig/zig-x64/zig build run",
"problemMatcher": [
"$gcc"
],
"group": "build",
"presentation": {
"clear": true
}
},
{
"label": "Build and Run Project (release-fast)",
"type": "shell",
"command": "zig build run -Drelease-fast",
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"clear": true
}
},
{
"label": "Build and Run Project (release-small)",
"type": "shell",
"command": "zig build run -Drelease-small",
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"clear": true
}
},
{
"label": "Test Project",
"type": "shell",
"command": "zig build test",
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"clear": true
}
},
{
"label": "Build and Run Current File",
"type": "shell",
"command": "zig run ${file}",
"problemMatcher": [
"$gcc"
],
"presentation": {
"clear": true
},
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "Build and Run Tests in Current File",
"type": "shell",
"command": "zig test ${file}",
"problemMatcher": [
"$gcc"
],
"presentation": {
"clear": true
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 prime31
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,46 @@
# Zig ECS
Zig ECS is a zig port of the fantasic [Entt](https://github.com/skypjack/entt). Entt is _highly_ templated C++ code which depending on your opinion is either a good thing or satan itself in code form. Zig doesn't have the same concept as C++ templates (thank goodness!) so the templated code was changed over to use Zig's generics and compile time metaprogramming.
## What does a zigified Entt look like?
Below are examples of a View and a Group, the two main ways to work with entities in the ecs along with the scaffolding code.
Declare some structs to work with:
```zig
pub const Velocity = struct { x: f32, y: f32 };
pub const Position = struct { x: f32, y: f32 };
```
Setup the Registry, which holds the entity data and is where we run our queries:
```zig
var reg = ecs.Registry.init(std.testing.allocator);
```
Create a couple entities and add some components to them
```zig
var entity = reg.create();
reg.add(entity, Position{ .x = 0, .y = 0 });
reg.add(entity, Velocity{ .x = 5, .y = 7 });
...
```
Create and iterate a View that matches all entities with a `Velocity` and `Position` component:
```zig
var view = reg.view(.{ Velocity, Position }, .{});
var iter = view.iterator();
while (iter.next()) |entity| {
const pos = view.getConst(Position, entity); // readonly copy
var vel = view.get(Velocity, entity); // mutable
}
```
The same example using an owning Group:
```zig
var group = reg.group(.{ Velocity, Position }, .{}, .{});
group.each(each);
fn each(e: struct { vel: *Velocity, pos: *Position }) void {
e.pos.*.x += e.vel.x;
e.pos.*.y += e.vel.y;
}
```

@ -0,0 +1,104 @@
const std = @import("std");
const Builder = std.build.Builder;
const builtin = @import("builtin");
pub fn build(b: *Builder) void {
const optimize = b.standardOptimizeOption(.{});
const ecs_module = b.addModule("zig-ecs", .{
.source_file = std.build.FileSource{ .path = "src/ecs.zig" },
});
// use a different cache folder for macos arm builds
b.cache_root = .{
.handle = std.fs.cwd(),
.path = if (builtin.os.tag == .macos and builtin.target.cpu.arch == .aarch64) "zig-arm-cache" else "zig-cache",
};
const examples = [_][2][]const u8{
[_][]const u8{ "view_vs_group", "examples/view_vs_group.zig" },
[_][]const u8{ "group_sort", "examples/group_sort.zig" },
[_][]const u8{ "simple", "examples/simple.zig" },
};
for (examples, 0..) |example, i| {
const name = if (i == 0) "ecs" else example[0];
const source = example[1];
var exe = b.addExecutable(.{
.name = name,
.root_source_file = std.build.FileSource{ .path = source },
.optimize = optimize,
});
// exe.setOutputDir(std.fs.path.join(b.allocator, &[_][]const u8{ b.cache_root, "bin" }) catch unreachable);
// exe.output_dirname_source = .{ .path = std.fs.path.join(b.allocator, &[_][]const u8{ b.cache_root.path.?, "bin" }) catch unreachable, .step = &exe.step };
exe.addModule("ecs", ecs_module);
exe.linkLibC();
const docs = exe;
// docs.emit_docs = .emit;
const doc = b.step(b.fmt("{s}-docs", .{name}), "Generate documentation");
doc.dependOn(&docs.step);
const run_cmd = b.addRunArtifact(exe);
const exe_step = b.step(name, b.fmt("run {s}.zig", .{name}));
exe_step.dependOn(&run_cmd.step);
// first element in the list is added as "run" so "zig build run" works
if (i == 0) {
const run_exe_step = b.step("run", b.fmt("run {s}.zig", .{name}));
run_exe_step.dependOn(&run_cmd.step);
}
}
// internal tests
const internal_test_step = b.addTest(.{
.root_source_file = std.build.FileSource{ .path = "src/tests.zig" },
.optimize = optimize,
});
// public api tests
const test_step = b.addTest(.{
.root_source_file = std.build.FileSource{ .path = "tests/tests.zig" },
.optimize = optimize,
});
test_step.addModule("ecs", ecs_module);
const test_cmd = b.step("test", "Run the tests");
test_cmd.dependOn(&internal_test_step.step);
test_cmd.dependOn(&test_step.step);
}
pub const LibType = enum(i32) {
static,
dynamic, // requires DYLD_LIBRARY_PATH to point to the dylib path
exe_compiled,
};
pub fn getModule(b: *std.Build, comptime prefix_path: []const u8) *std.build.Module {
return b.addModule("zig-ecs", .{
.source_file = .{ .path = prefix_path ++ "/src/ecs.zig" },
});
}
/// prefix_path is used to add package paths. It should be the the same path used to include this build file
pub fn linkArtifact(b: *Builder, artifact: *std.build.LibExeObjStep, _: std.build.Target, lib_type: LibType, comptime prefix_path: []const u8) void {
const optimize = b.standardOptimizeOption(.{});
switch (lib_type) {
.static => {
const lib = b.addStaticLibrary(.{ .name = "ecs", .root_source_file = "ecs.zig", .optimize = optimize });
lib.install();
artifact.linkLibrary(lib);
},
.dynamic => {
const lib = b.addSharedLibrary(.{ .name = "ecs", .root_source_file = "ecs.zig", .optimize = optimize });
lib.install();
artifact.linkLibrary(lib);
},
else => {},
}
artifact.addModule(getModule(prefix_path));
}

@ -0,0 +1,64 @@
const std = @import("std");
const ecs = @import("ecs");
// override the EntityTraits used by ecs
pub const EntityTraits = ecs.EntityTraitsType(.medium);
pub const Velocity = struct { x: f32, y: f32 };
pub const Position = struct { x: f32, y: f32 };
const total_entities: usize = 10000;
/// logs the timing for views vs non-owning groups vs owning groups with 1,000,000 entities
pub fn main() !void {
var reg = ecs.Registry.init(std.heap.c_allocator);
defer reg.deinit();
createEntities(&reg);
owningGroup(&reg);
}
fn createEntities(reg: *ecs.Registry) void {
var r = std.rand.DefaultPrng.init(666);
var timer = std.time.Timer.start() catch unreachable;
var i: usize = 0;
while (i < total_entities) : (i += 1) {
var e1 = reg.create();
reg.add(e1, Position{ .x = 1, .y = r.random().float(f32) * 100 });
reg.add(e1, Velocity{ .x = 1, .y = r.random().float(f32) * 100 });
}
var end = timer.lap();
std.debug.print("create {d} entities: {d}\n", .{ total_entities, @as(f64, @floatFromInt(end)) / 1000000000 });
}
fn owningGroup(reg: *ecs.Registry) void {
var group = reg.group(.{ Velocity, Position }, .{}, .{});
// var group_iter = group.iterator(struct { vel: *Velocity, pos: *Position });
// while (group_iter.next()) |e| {
// std.debug.print("pos.y {d:.3}, ent: {}\n", .{e.pos.y, group_iter.entity()});
// }
const SortContext = struct {
fn sort(_: void, a: Position, b: Position) bool {
return a.y < b.y;
}
};
var timer = std.time.Timer.start() catch unreachable;
group.sort(Position, {}, SortContext.sort);
var end = timer.lap();
std.debug.print("group (sort): {d}\n", .{@as(f64, @floatFromInt(end)) / 1000000000});
timer.reset();
group.sort(Position, {}, SortContext.sort);
end = timer.lap();
std.debug.print("group (sort 2): {d}\n", .{@as(f64, @floatFromInt(end)) / 1000000000});
// var group_iter2 = group.iterator(struct { vel: *Velocity, pos: *Position });
// while (group_iter2.next()) |e| {
// std.debug.print("pos.y {d:.3}, ent: {}\n", .{e.pos.y, group_iter2.entity()});
// }
}

@ -0,0 +1,47 @@
const std = @import("std");
const ecs = @import("ecs");
// override the EntityTraits used by ecs
pub const EntityTraits = ecs.EntityTraitsType(.small);
pub const Velocity = struct { x: f32, y: f32 };
pub const Position = struct { x: f32, y: f32 };
pub fn main() !void {
var reg = ecs.Registry.init(std.heap.c_allocator);
defer reg.deinit();
var e1 = reg.create();
reg.add(e1, Position{ .x = 0, .y = 0 });
reg.add(e1, Velocity{ .x = 5, .y = 7 });
var e2 = reg.create();
reg.add(e2, Position{ .x = 10, .y = 10 });
reg.add(e2, Velocity{ .x = 15, .y = 17 });
var view = reg.view(.{ Velocity, Position }, .{});
var iter = view.iterator();
while (iter.next()) |entity| {
var pos = view.get(Position, entity);
const vel = view.getConst(Velocity, entity);
std.debug.print(
"entity: {}, pos: (x = {d}, y = {d}), vel: (x = {d}, y = {d})\n",
.{ entity, pos.x, pos.y, vel.x, vel.y },
);
pos.*.x += vel.x;
pos.*.y += vel.y;
}
std.debug.print("---- resetting iter\n", .{});
iter.reset();
while (iter.next()) |entity| {
const pos = view.getConst(Position, entity);
const vel = view.getConst(Velocity, entity);
std.debug.print(
"entity: {}, pos: (x = {d}, y = {d}), vel: (x = {d}, y = {d})\n",
.{ entity, pos.x, pos.y, vel.x, vel.y },
);
}
}

@ -0,0 +1,119 @@
const std = @import("std");
const ecs = @import("ecs");
// override the EntityTraits used by ecs
pub const EntityTraits = ecs.EntityTraitsType(.medium);
pub const Velocity = struct { x: f32, y: f32 };
pub const Position = struct { x: f32, y: f32 };
/// logs the timing for views vs non-owning groups vs owning groups with 1,000,000 entities
pub fn main() !void {
var reg = ecs.Registry.init(std.heap.c_allocator);
defer reg.deinit();
// var timer = try std.time.Timer.start();
createEntities(&reg);
iterateView(&reg);
nonOwningGroup(&reg);
owningGroup(&reg);
}
fn createEntities(reg: *ecs.Registry) void {
var timer = std.time.Timer.start() catch unreachable;
var i: usize = 0;
while (i < 1000000) : (i += 1) {
var e1 = reg.create();
reg.add(e1, Position{ .x = 1, .y = 1 });
reg.add(e1, Velocity{ .x = 1, .y = 1 });
}
var end = timer.lap();
std.debug.print("create entities: \t{d}\n", .{@as(f64, @floatFromInt(end)) / 1000000000});
}
fn iterateView(reg: *ecs.Registry) void {
std.debug.print("--- multi-view ---\n", .{});
var view = reg.view(.{ Velocity, Position }, .{});
var timer = std.time.Timer.start() catch unreachable;
var iter = view.entityIterator();
while (iter.next()) |entity| {
var pos = view.get(Position, entity);
const vel = view.getConst(Velocity, entity);
pos.*.x += vel.x;
pos.*.y += vel.y;
}
var end = timer.lap();
std.debug.print("view (iter): \t{d}\n", .{@as(f64, @floatFromInt(end)) / 1000000000});
}
fn nonOwningGroup(reg: *ecs.Registry) void {
std.debug.print("--- non-owning ---\n", .{});
var timer = std.time.Timer.start() catch unreachable;
var group = reg.group(.{}, .{ Velocity, Position }, .{});
var end = timer.lap();
std.debug.print("group (create): {d}\n", .{@as(f64, @floatFromInt(end)) / 1000000000});
timer.reset();
var group_iter = group.iterator();
while (group_iter.next()) |entity| {
var pos = group.get(Position, entity);
const vel = group.getConst(Velocity, entity);
pos.*.x += vel.x;
pos.*.y += vel.y;
}
end = timer.lap();
std.debug.print("group (iter): \t{d}\n", .{@as(f64, @floatFromInt(end)) / 1000000000});
}
fn owningGroup(reg: *ecs.Registry) void {
std.debug.print("--- owning ---\n", .{});
var timer = std.time.Timer.start() catch unreachable;
var group = reg.group(.{ Velocity, Position }, .{}, .{});
var end = timer.lap();
std.debug.print("group (create): {d}\n", .{@as(f64, @floatFromInt(end)) / 1000000000});
timer.reset();
var group_iter = group.iterator(struct { vel: *Velocity, pos: *Position });
while (group_iter.next()) |e| {
e.pos.*.x += e.vel.x;
e.pos.*.y += e.vel.y;
}
end = timer.lap();
std.debug.print("group (iter): \t{d}\n", .{@as(f64, @floatFromInt(end)) / 1000000000});
timer.reset();
group.each(each);
end = timer.lap();
std.debug.print("group (each): \t{d}\n", .{@as(f64, @floatFromInt(end)) / 1000000000});
timer.reset();
// var storage = reg.assure(Velocity);
// var vel = storage.instances.items;
var pos = reg.assure(Position).instances.items;
var index: usize = group.group_data.current;
while (true) {
if (index == 0) break;
index -= 1;
pos[index].x += pos[index].x;
pos[index].y += pos[index].y;
}
end = timer.lap();
std.debug.print("group (direct): {d}\n", .{@as(f64, @floatFromInt(end)) / 1000000000});
}
fn each(e: struct { vel: *Velocity, pos: *Position }) void {
e.pos.*.x += e.vel.x;
e.pos.*.y += e.vel.y;
}

@ -0,0 +1,19 @@
// ecs
pub const EntityTraitsType = @import("ecs/entity.zig").EntityTraitsType;
// TODO: remove me. this is just for testing
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 EntityHandles = @import("ecs/registry.zig").EntityHandles;
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;
pub const SparseSet = @import("ecs/sparse_set.zig").SparseSet;
pub const utils = @import("ecs/utils.zig");
// signals
pub const Signal = @import("signals/signal.zig").Signal;
pub const Dispatcher = @import("signals/dispatcher.zig").Dispatcher;

@ -0,0 +1,88 @@
const std = @import("std");
const Registry = @import("registry.zig").Registry;
const Entity = @import("registry.zig").Entity;
pub const Actor = struct {
registry: *Registry,
entity: Entity = undefined,
pub fn init(registry: *Registry) Actor {
var reg = registry;
return .{
.registry = registry,
.entity = reg.create(),
};
}
pub fn deinit(self: *Actor) void {
self.registry.destroy(self.entity);
}
pub fn add(self: *Actor, value: anytype) void {
self.registry.add(self.entity, value);
}
pub fn addTyped(self: *Actor, comptime T: type, value: T) void {
self.registry.addTyped(T, self.entity, value);
}
pub fn remove(self: *Actor, comptime T: type) void {
self.registry.remove(T, self.entity);
}
pub fn has(self: *Actor, comptime T: type) bool {
return self.registry.has(T, self.entity);
}
pub fn get(self: *Actor, comptime T: type) *T {
return self.registry.get(T, self.entity);
}
pub fn tryGet(self: *Actor, comptime T: type) ?*T {
return self.registry.tryGet(T, self.entity);
}
};
test "actor" {
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var actor = Actor.init(&reg);
defer actor.deinit();
std.debug.assert(!actor.has(f32));
actor.addTyped(f32, 67.45);
if (actor.tryGet(f32)) |val| {
try std.testing.expectEqual(val.*, 67.45);
}
actor.addTyped(u64, 8888);
try std.testing.expectEqual(actor.get(u64).*, 8888);
std.debug.assert(actor.has(u64));
actor.remove(u64);
std.debug.assert(!actor.has(u64));
}
test "actor structs" {
const Velocity = struct { x: f32, y: f32 };
const Position = struct { x: f32 = 0, y: f32 = 0 };
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var actor = Actor.init(&reg);
defer actor.deinit();
actor.add(Velocity{ .x = 5, .y = 10 });
actor.add(Position{});
var vel = actor.get(Velocity);
var pos = actor.get(Position);
pos.*.x += vel.x;
pos.*.y += vel.y;
try std.testing.expectEqual(actor.get(Position).*.x, 5);
try std.testing.expectEqual(actor.get(Position).*.y, 10);
}

@ -0,0 +1,451 @@
const std = @import("std");
const utils = @import("utils.zig");
const SparseSet = @import("sparse_set.zig").SparseSet;
const Signal = @import("../signals/signal.zig").Signal;
const Sink = @import("../signals/sink.zig").Sink;
/// Stores an ArrayList of components along with a SparseSet of entities
pub fn ComponentStorage(comptime Component: type, comptime Entity: type) type {
std.debug.assert(!utils.isComptime(Component));
// empty (zero-sized) structs will not have an array created
const is_empty_struct = @sizeOf(Component) == 0;
// HACK: due to this being stored as untyped ptrs, when deinit is called we are casted to a Component 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 ComponentOrDummy = if (is_empty_struct) struct { dummy: u1 } else Component;
return struct {
const Self = @This();
set: *SparseSet(Entity),
instances: std.ArrayList(ComponentOrDummy),
allocator: ?std.mem.Allocator,
/// doesnt really belong here...used to denote group ownership
super: usize = 0,
safeDeinit: *const fn (*Self) void,
safeSwap: *const fn (*Self, Entity, Entity, bool) void,
safeRemoveIfContains: *const fn (*Self, Entity) void,
construction: Signal(Entity),
update: Signal(Entity),
destruction: Signal(Entity),
pub fn init(allocator: std.mem.Allocator) Self {
var store = Self{
.set = SparseSet(Entity).initPtr(allocator),
.instances = undefined,
.safeDeinit = struct {
fn deinit(self: *Self) void {
if (!is_empty_struct) {
self.instances.deinit();
}
}
}.deinit,
.safeSwap = struct {
fn swap(self: *Self, lhs: Entity, rhs: Entity, instances_only: bool) void {
if (!is_empty_struct) {
std.mem.swap(Component, &self.instances.items[self.set.index(lhs)], &self.instances.items[self.set.index(rhs)]);
}
if (!instances_only) self.set.swap(lhs, rhs);
}
}.swap,
.safeRemoveIfContains = struct {
fn removeIfContains(self: *Self, entity: Entity) void {
if (self.contains(entity)) {
self.remove(entity);
}
}
}.removeIfContains,
.allocator = null,
.construction = Signal(Entity).init(allocator),
.update = Signal(Entity).init(allocator),
.destruction = Signal(Entity).init(allocator),
};
if (!is_empty_struct) {
store.instances = std.ArrayList(ComponentOrDummy).init(allocator);
}
return store;
}
pub fn initPtr(allocator: std.mem.Allocator) *Self {
var store = allocator.create(Self) catch unreachable;
store.set = SparseSet(Entity).initPtr(allocator);
if (!is_empty_struct) {
store.instances = std.ArrayList(ComponentOrDummy).init(allocator);
}
store.allocator = allocator;
store.super = 0;
store.construction = Signal(Entity).init(allocator);
store.update = Signal(Entity).init(allocator);
store.destruction = Signal(Entity).init(allocator);
// since we are stored as a pointer, we need to catpure this
store.safeDeinit = struct {
fn deinit(self: *Self) void {
if (!is_empty_struct) {
self.instances.deinit();
}
}
}.deinit;
store.safeSwap = struct {
fn swap(self: *Self, lhs: Entity, rhs: Entity, instances_only: bool) void {
if (!is_empty_struct) {
std.mem.swap(Component, &self.instances.items[self.set.index(lhs)], &self.instances.items[self.set.index(rhs)]);
}
if (!instances_only) self.set.swap(lhs, rhs);
}
}.swap;
store.safeRemoveIfContains = struct {
fn removeIfContains(self: *Self, entity: Entity) void {
if (self.contains(entity)) {
self.remove(entity);
}
}
}.removeIfContains;
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.safeDeinit(self);
self.set.deinit();
self.construction.deinit();
self.update.deinit();
self.destruction.deinit();
if (self.allocator) |allocator| {
allocator.destroy(self);
}
}
pub fn onConstruct(self: *Self) Sink(Entity) {
return self.construction.sink();
}
pub fn onUpdate(self: *Self) Sink(Entity) {
return self.update.sink();
}
pub fn onDestruct(self: *Self) Sink(Entity) {
return self.destruction.sink();
}
/// 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 assigns its object
pub fn add(self: *Self, entity: Entity, value: Component) void {
if (!is_empty_struct) {
_ = self.instances.append(value) catch unreachable;
}
self.set.add(entity);
self.construction.publish(entity);
}
/// Removes an entity from a storage
pub fn remove(self: *Self, entity: Entity) void {
self.destruction.publish(entity);
if (!is_empty_struct) {
_ = self.instances.swapRemove(self.set.index(entity));
}
self.set.remove(entity);
}
/// Checks if a view contains an entity
pub fn contains(self: Self, entity: Entity) bool {
return self.set.contains(entity);
}
pub fn removeIfContains(self: *Self, entity: Entity) void {
if (Component == u1) {
self.safeRemoveIfContains(self, entity);
} else if (self.contains(entity)) {
self.remove(entity);
}
}
pub fn len(self: Self) usize {
return self.set.len();
}
pub usingnamespace if (is_empty_struct)
struct {
/// 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: anytype, comptime lessThan: *const fn (@TypeOf(context), T, T) bool) void {
std.debug.assert(T == Entity);
self.set.sort(context, lessThan);
}
}
else
struct {
/// Direct access to the array of objects
pub fn raw(self: Self) []Component {
return self.instances.items;
}
/// Replaces the given component for an entity
pub fn replace(self: *Self, entity: Entity, value: Component) void {
self.get(entity).* = value;
self.update.publish(entity);
}
/// Returns the object associated with an entity
pub fn get(self: *Self, entity: Entity) *Component {
std.debug.assert(self.contains(entity));
return &self.instances.items[self.set.index(entity)];
}
pub fn getConst(self: *Self, entity: Entity) Component {
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: Entity) ?*Component {
return if (self.set.contains(entity)) &self.instances.items[self.set.index(entity)] else null;
}
pub fn tryGetConst(self: *Self, entity: Entity) ?Component {
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. Valid types for T are Entity or Component.
pub fn sort(self: *Self, comptime T: type, length: usize, context: anytype, comptime lessThan: *const fn (@TypeOf(context), T, T) bool) void {
std.debug.assert(T == Entity or T == Component);
// we have to perform a swap after the sort for all moved entities so we make a helper struct for that. In the
// case of a Component sort we also wrap that into the struct so we can get the Component data to pass to the
// lessThan method passed in.
if (T == Entity) {
const SortContext = struct {
storage: *Self,
pub fn swap(this: @This(), a: Entity, b: Entity) void {
this.storage.safeSwap(this.storage, a, b, true);
}
};
const swap_context = SortContext{ .storage = self };
self.set.arrange(length, context, lessThan, swap_context);
} else {
const SortContext = struct {
storage: *Self,
wrapped_context: @TypeOf(context),
lessThan: *const fn (@TypeOf(context), T, T) bool,
fn sort(this: @This(), a: Entity, b: Entity) bool {
const real_a = this.storage.getConst(a);
const real_b = this.storage.getConst(b);
return this.lessThan(this.wrapped_context, real_a, real_b);
}
pub fn swap(this: @This(), a: Entity, b: Entity) void {
this.storage.safeSwap(this.storage, a, b, true);
}
};
const swap_context = SortContext{ .storage = self, .wrapped_context = context, .lessThan = lessThan };
self.set.arrange(length, swap_context, SortContext.sort, swap_context);
}
}
};
/// Direct access to the array of entities
pub fn data(self: Self) []const Entity {
return self.set.data();
}
/// Direct access to the array of entities
pub fn dataPtr(self: Self) *const []Entity {
return self.set.dataPtr();
}
/// Swaps entities and objects in the internal packed arrays
pub fn swap(self: *Self, lhs: Entity, rhs: Entity) void {
self.safeSwap(self, lhs, rhs, false);
}
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).init(std.testing.allocator);
defer store.deinit();
store.add(3, 66.45);
try std.testing.expectEqual(store.tryGetConst(3).?, 66.45);
if (store.tryGet(3)) |found| {
try std.testing.expectEqual(@as(f32, 66.45), found.*);
}
store.remove(3);
var val_null = store.tryGet(3);
try std.testing.expectEqual(val_null, null);
store.clear();
}
test "add/get/remove" {
var store = ComponentStorage(f32, u32).init(std.testing.allocator);
defer store.deinit();
store.add(3, 66.45);
if (store.tryGet(3)) |found| try std.testing.expectEqual(@as(f32, 66.45), found.*);
try std.testing.expectEqual(store.tryGetConst(3).?, 66.45);
store.remove(3);
try std.testing.expectEqual(store.tryGet(3), null);
}
test "iterate" {
var store = ComponentStorage(f32, u32).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(), 0..) |entity, i| {
if (i == 0) {
try std.testing.expectEqual(entity, 3);
}
if (i == 1) {
try std.testing.expectEqual(entity, 5);
}
if (i == 2) {
try std.testing.expectEqual(entity, 7);
}
}
}
test "empty component" {
const Empty = struct {};
var store = ComponentStorage(Empty, u32).initPtr(std.testing.allocator);
defer store.deinit();
store.add(3, Empty{});
store.remove(3);
}
fn construct(e: u32) void {
std.debug.assert(e == 3);
}
fn update(e: u32) void {
std.debug.assert(e == 3);
}
fn destruct(e: u32) void {
std.debug.assert(e == 3);
}
test "signals" {
var store = ComponentStorage(f32, u32).init(std.testing.allocator);
defer store.deinit();
store.onConstruct().connect(construct);
store.onUpdate().connect(update);
store.onDestruct().connect(destruct);
store.add(3, 66.45);
store.replace(3, 45.64);
store.remove(3);
store.onConstruct().disconnect(construct);
store.onUpdate().disconnect(update);
store.onDestruct().disconnect(destruct);
store.add(4, 66.45);
store.replace(4, 45.64);
store.remove(4);
}
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{});
const asc_u32 = comptime std.sort.asc(u32);
store.sort(u32, {}, asc_u32);
for (store.data(), 0..) |e, i| {
try std.testing.expectEqual(@as(u32, @intCast(i)), e);
}
const desc_u32 = comptime std.sort.desc(u32);
store.sort(u32, {}, desc_u32);
var counter: u32 = 2;
for (store.data()) |e| {
try std.testing.expectEqual(counter, e);
if (counter > 0) counter -= 1;
}
}
test "sort by entity" {
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));
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, store.len(), context, SortContext.sort);
var compare: f32 = 5;
for (store.raw()) |val| {
try std.testing.expect(compare > val);
compare = val;
}
}
test "sort by component" {
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));
const desc_f32 = comptime std.sort.desc(f32);
store.sort(f32, store.len(), {}, desc_f32);
var compare: f32 = 5;
for (store.raw()) |val| {
try std.testing.expect(compare > val);
compare = val;
}
}

@ -0,0 +1,56 @@
const std = @import("std");
/// default EntityTraitsDefinition with reasonable sizes suitable for most situations
pub const EntityTraits = EntityTraitsType(.medium);
pub const EntityTraitsSize = enum { small, medium, large };
pub fn EntityTraitsType(comptime size: EntityTraitsSize) type {
return switch (size) {
.small => EntityTraitsDefinition(u16, u12, u4),
.medium => EntityTraitsDefinition(u32, u20, u12),
.large => EntityTraitsDefinition(u64, u32, u32),
};
}
fn EntityTraitsDefinition(comptime EntityType: type, comptime IndexType: type, comptime VersionType: type) type {
std.debug.assert(std.meta.trait.isUnsignedInt(EntityType));
std.debug.assert(std.meta.trait.isUnsignedInt(IndexType));
std.debug.assert(std.meta.trait.isUnsignedInt(VersionType));
const sizeOfIndexType = @bitSizeOf(IndexType);
const sizeOfVersionType = @bitSizeOf(VersionType);
const entityShift = sizeOfIndexType;
if (sizeOfIndexType + sizeOfVersionType != @bitSizeOf(EntityType))
@compileError("IndexType and VersionType must sum to EntityType's bit count");
const entityMask = std.math.maxInt(IndexType);
const versionMask = std.math.maxInt(VersionType);
return struct {
entity_type: type = EntityType,
index_type: type = IndexType,
version_type: type = VersionType,
/// Mask to use to get the entity index number out of an identifier
entity_mask: EntityType = entityMask,
/// Mask to use to get the version out of an identifier
version_mask: EntityType = versionMask,
/// Bit size of entity in entity_type
entity_shift: EntityType = entityShift,
pub fn init() @This() {
return @This(){};
}
};
}
test "entity traits" {
const sm = EntityTraitsType(.small).init();
const m = EntityTraitsType(.medium).init();
const l = EntityTraitsType(.large).init();
try std.testing.expectEqual(sm.entity_mask, std.math.maxInt(sm.index_type));
try std.testing.expectEqual(m.entity_mask, std.math.maxInt(m.index_type));
try std.testing.expectEqual(l.entity_mask, std.math.maxInt(l.index_type));
}

@ -0,0 +1,469 @@
const std = @import("std");
const builtin = @import("builtin");
const utils = @import("utils.zig");
const Registry = @import("registry.zig").Registry;
const Storage = @import("registry.zig").Storage;
const SparseSet = @import("sparse_set.zig").SparseSet;
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 {
registry: *Registry,
group_data: *Registry.GroupData,
pub fn init(registry: *Registry, group_data: *Registry.GroupData) BasicGroup {
return .{
.registry = registry,
.group_data = group_data,
};
}
pub fn len(self: BasicGroup) usize {
return self.group_data.entity_set.len();
}
/// Direct access to the array of entities
pub fn data(self: BasicGroup) []const Entity {
return self.group_data.entity_set.data();
}
pub fn get(self: BasicGroup, comptime T: type, entity: Entity) *T {
return self.registry.assure(T).get(entity);
}
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: BasicGroup) utils.ReverseSliceIterator(Entity) {
return self.group_data.entity_set.reverseIterator();
}
pub fn sort(self: BasicGroup, comptime T: type, context: anytype, comptime lessThan: *const 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: *const 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 {
registry: *Registry,
group_data: *Registry.GroupData,
super: *usize,
/// iterator the provides the data from all the requested owned components in a single struct. Access to the current Entity
/// being iterated is available via the entity() method, useful for accessing non-owned component data. The get() method can
/// also be used to fetch non-owned component data for the currently iterated Entity.
/// TODO: support const types in the Components struct in addition to the current ptrs
fn Iterator(comptime Components: anytype) type {
return struct {
group: OwningGroup,
index: usize,
storage: *Storage(u1),
component_ptrs: [@typeInfo(Components).Struct.fields.len][*]u8,
pub fn init(group: OwningGroup) @This() {
const component_info = @typeInfo(Components).Struct;
var component_ptrs: [component_info.fields.len][*]u8 = undefined;
inline for (component_info.fields, 0..) |field, i| {
const storage = group.registry.assure(@typeInfo(field.type).Pointer.child);
component_ptrs[i] = @as([*]u8, @ptrCast(storage.instances.items.ptr));
}
return .{
.group = group,
.index = group.group_data.current,
.storage = group.firstOwnedStorage(),
.component_ptrs = component_ptrs,
};
}
pub fn next(it: *@This()) ?Components {
if (it.index == 0) return null;
it.index -= 1;
// fill and return the struct
var comps: Components = undefined;
inline for (@typeInfo(Components).Struct.fields, 0..) |field, i| {
const typed_ptr = @as([*]@typeInfo(field.type).Pointer.child, @ptrCast(@alignCast(it.component_ptrs[i])));
@field(comps, field.name) = &typed_ptr[it.index];
}
return comps;
}
pub fn entity(it: @This()) Entity {
std.debug.assert(it.index >= 0 and it.index < it.group.group_data.current);
return it.storage.set.dense.items[it.index];
}
pub fn get(it: @This(), comptime T: type) *T {
return it.group.registry.get(T, it.entity());
}
// Reset the iterator to the initial index
pub fn reset(it: *@This()) void {
it.index = it.group.group_data.current;
}
};
}
pub fn init(registry: *Registry, group_data: *Registry.GroupData, super: *usize) OwningGroup {
return .{
.registry = registry,
.group_data = group_data,
.super = super,
};
}
/// grabs an untyped (u1) reference to the first Storage(T) in the owned array
fn firstOwnedStorage(self: OwningGroup) *Storage(u1) {
const ptr = self.registry.components.get(self.group_data.owned[0]).?;
return @as(*Storage(u1), @ptrFromInt(ptr));
}
/// total number of entities in the group
pub fn len(self: OwningGroup) usize {
return self.group_data.current;
}
/// direct access to the array of entities of the first owning group
pub fn data(self: OwningGroup) []const Entity {
return self.firstOwnedStorage().data();
}
pub fn contains(self: OwningGroup, entity: Entity) bool {
var storage = self.firstOwnedStorage();
return storage.contains(entity) and storage.set.index(entity) < self.len();
}
fn validate(self: OwningGroup, comptime Components: anytype) void {
if (builtin.mode == .Debug and self.group_data.owned.len > 0) {
std.debug.assert(@typeInfo(Components) == .Struct);
inline for (@typeInfo(Components).Struct.fields) |field| {
std.debug.assert(@typeInfo(field.type) == .Pointer);
const found = std.mem.indexOfScalar(u32, self.group_data.owned, utils.typeId(std.meta.Child(field.type)));
std.debug.assert(found != null);
}
}
}
pub fn getOwned(self: OwningGroup, entity: Entity, comptime Components: anytype) Components {
self.validate(Components);
const component_info = @typeInfo(Components).Struct;
var component_ptrs: [component_info.fields.len][*]u8 = undefined;
inline for (component_info.fields, 0..) |field, i| {
const storage = self.registry.assure(std.meta.Child(field.type));
component_ptrs[i] = @as([*]u8, @ptrCast(storage.instances.items.ptr));
}
// fill the struct
const index = self.firstOwnedStorage().set.index(entity);
var comps: Components = undefined;
inline for (component_info.fields, 0..) |field, i| {
const typed_ptr = @as([*]std.meta.Child(field.type), @ptrCast(@alignCast(component_ptrs[i])));
@field(comps, field.name) = &typed_ptr[index];
}
return comps;
}
pub fn each(self: OwningGroup, comptime func: anytype) void {
const Components = switch (@typeInfo(@TypeOf(func))) {
.Fn => |func_info| func_info.params[0].type.?,
else => std.debug.assert("invalid func"),
};
self.validate(Components);
// optionally we could just use an Iterator here and pay for some slight indirection for code sharing
var iter = self.iterator(Components);
while (iter.next()) |comps| {
@call(.always_inline, func, .{comps});
}
}
/// returns the component storage for the given type for direct access
pub fn getStorage(self: OwningGroup, comptime T: type) *Storage(T) {
return self.registry.assure(T);
}
pub fn get(self: OwningGroup, comptime T: type, entity: Entity) *T {
return self.registry.assure(T).get(entity);
}
pub fn getConst(self: OwningGroup, comptime T: type, entity: Entity) T {
return self.registry.assure(T).getConst(entity);
}
pub fn sortable(self: OwningGroup) bool {
return self.group_data.super == self.group_data.size;
}
/// returns an iterator with optimized access to the owend Components. Note that Components should be a struct with
/// fields that are pointers to the component types that you want to fetch. Only types that are owned are valid! Non-owned
/// types should be fetched via Iterator.get.
pub fn iterator(self: OwningGroup, comptime Components: anytype) Iterator(Components) {
self.validate(Components);
return Iterator(Components).init(self);
}
pub fn entityIterator(self: OwningGroup) utils.ReverseSliceIterator(Entity) {
return utils.ReverseSliceIterator(Entity).init(self.firstOwnedStorage().set.dense.items[0..self.group_data.current]);
}
pub fn sort(self: OwningGroup, comptime T: type, context: anytype, comptime lessThan: *const fn (@TypeOf(context), T, T) bool) void {
var first_storage = self.firstOwnedStorage();
if (T == Entity) {
// only sort up to self.group_data.current
first_storage.sort(Entity, self.group_data.current, context, lessThan);
} else {
// TODO: in debug mode, validate that T is present in the group
const SortContext = struct {
group: OwningGroup,
wrapped_context: @TypeOf(context),
lessThan: *const 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);
}
};
const wrapper = SortContext{ .group = self, .wrapped_context = context, .lessThan = lessThan };
first_storage.sort(Entity, self.group_data.current, wrapper, SortContext.sort);
}
// sync up the rest of the owned components
var next: usize = self.group_data.current;
while (true) : (next -= 1) {
if (next == 0) break;
const pos = next - 1;
const entity = first_storage.data()[pos];
// skip the first one since its what we are using to sort with
for (self.group_data.owned[1..]) |type_id| {
var other_ptr = self.registry.components.get(type_id).?;
var storage = @as(*Storage(u1), @ptrFromInt(other_ptr));
storage.swap(storage.data()[pos], entity);
}
}
}
};
test "BasicGroup creation/iteration" {
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var group = reg.group(.{}, .{ i32, u32 }, .{});
try std.testing.expectEqual(group.len(), 0);
var e0 = reg.create();
reg.add(e0, @as(i32, 44));
reg.add(e0, @as(u32, 55));
std.debug.assert(group.len() == 1);
var iterated_entities: usize = 0;
var iter = group.iterator();
while (iter.next()) |_| {
iterated_entities += 1;
}
try std.testing.expectEqual(iterated_entities, 1);
iterated_entities = 0;
for (group.data()) |_| {
iterated_entities += 1;
}
try std.testing.expectEqual(iterated_entities, 1);
reg.remove(i32, e0);
std.debug.assert(group.len() == 0);
}
test "BasicGroup excludes" {
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var group = reg.group(.{}, .{i32}, .{u32});
try std.testing.expectEqual(group.len(), 0);
var e0 = reg.create();
reg.add(e0, @as(i32, 44));
std.debug.assert(group.len() == 1);
var iterated_entities: usize = 0;
var iter = group.iterator();
while (iter.next()) |_| {
iterated_entities += 1;
}
try std.testing.expectEqual(iterated_entities, 1);
reg.add(e0, @as(u32, 55));
std.debug.assert(group.len() == 0);
}
test "BasicGroup create late" {
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var e0 = reg.create();
reg.add(e0, @as(i32, 44));
reg.add(e0, @as(u32, 55));
var group = reg.group(.{}, .{ i32, u32 }, .{});
try std.testing.expectEqual(group.len(), 1);
}
test "OwningGroup" {
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var group = reg.group(.{ i32, u32 }, .{}, .{});
var e0 = reg.create();
reg.add(e0, @as(i32, 44));
reg.add(e0, @as(u32, 55));
try std.testing.expectEqual(group.len(), 1);
try std.testing.expect(group.contains(e0));
try std.testing.expectEqual(group.get(i32, e0).*, 44);
try std.testing.expectEqual(group.getConst(u32, e0), 55);
var vals = group.getOwned(e0, struct { int: *i32, uint: *u32 });
try std.testing.expectEqual(vals.int.*, 44);
try std.testing.expectEqual(vals.uint.*, 55);
vals.int.* = 666;
var vals2 = group.getOwned(e0, struct { int: *i32, uint: *u32 });
try std.testing.expectEqual(vals2.int.*, 666);
}
test "OwningGroup add/remove" {
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var group = reg.group(.{ i32, u32 }, .{}, .{});
var e0 = reg.create();
reg.add(e0, @as(i32, 44));
reg.add(e0, @as(u32, 55));
try std.testing.expectEqual(group.len(), 1);
reg.remove(u32, e0);
try std.testing.expectEqual(group.len(), 0);
}
test "OwningGroup iterate" {
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var e0 = reg.create();
reg.add(e0, @as(i32, 44));
reg.add(e0, @as(u32, 55));
reg.add(e0, @as(u8, 11));
var e1 = reg.create();
reg.add(e1, @as(i32, 666));
reg.add(e1, @as(u32, 999));
reg.add(e1, @as(f32, 55.5));
var group = reg.group(.{ i32, u32 }, .{}, .{});
var iter = group.iterator(struct { int: *i32, uint: *u32 });
while (iter.next()) |item| {
if (iter.entity() == e0) {
try std.testing.expectEqual(item.int.*, 44);
try std.testing.expectEqual(item.uint.*, 55);
try std.testing.expectEqual(iter.get(u8).*, 11);
} else {
try std.testing.expectEqual(item.int.*, 666);
try std.testing.expectEqual(item.uint.*, 999);
try std.testing.expectEqual(iter.get(f32).*, 55.5);
}
}
}
fn each(components: struct {
int: *i32,
uint: *u32,
}) void {
std.testing.expectEqual(components.int.*, 44) catch unreachable;
std.testing.expectEqual(components.uint.*, 55) catch unreachable;
}
test "OwningGroup each" {
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var e0 = reg.create();
reg.add(e0, @as(i32, 44));
reg.add(e0, @as(u32, 55));
const Thing = struct {
fn each(_: @This(), components: struct {
int: *i32,
uint: *u32,
}) void {
std.testing.expectEqual(components.int.*, 44) catch unreachable;
std.testing.expectEqual(components.uint.*, 55) catch unreachable;
}
};
var thing = Thing{};
var group = reg.group(.{ i32, u32 }, .{}, .{});
// group.each(thing.each); // zig v0.10.0: error: no field named 'each' in struct 'ecs.groups.test.OwningGroup each.Thing'
_ = thing;
// group.each(each); // zig v0.10.0: error: expected type 'ecs.groups.each__struct_6297', found 'ecs.groups.each__struct_3365'
_ = group;
}
test "multiple OwningGroups" {
const Sprite = struct { x: f32 };
const Transform = struct { x: f32 };
const Renderable = struct { x: f32 };
const Rotation = struct { x: f32 };
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
// var group1 = reg.group(.{u64, u32}, .{}, .{});
// var group2 = reg.group(.{u64, u32, u8}, .{}, .{});
_ = reg.group(.{ Sprite, Transform }, .{ Renderable, Rotation }, .{});
_ = reg.group(.{Sprite}, .{Renderable}, .{});
_ = reg.group(.{ Sprite, Transform }, .{Renderable}, .{});
// ensure groups are ordered correctly internally
var last_size: u8 = 0;
for (reg.groups.items) |grp| {
try std.testing.expect(last_size <= grp.size);
last_size = grp.size;
}
try std.testing.expect(!reg.sortable(Sprite));
// this will break the group
// var group6 = reg.group(.{Sprite, Rotation}, .{}, .{});
}

@ -0,0 +1,161 @@
const std = @import("std");
const registry = @import("registry.zig");
/// generates versioned "handles" (https://floooh.github.io/2018/06/17/handles-vs-pointers.html)
/// you choose the type of the handle (aka its size) and how much of that goes to the index and the version.
/// the bitsize of version + id must equal the handle size.
pub fn Handles(comptime HandleType: type, comptime IndexType: type, comptime VersionType: type) type {
std.debug.assert(@typeInfo(HandleType) == .Int and std.meta.Int(.unsigned, @bitSizeOf(HandleType)) == HandleType);
std.debug.assert(@typeInfo(IndexType) == .Int and std.meta.Int(.unsigned, @bitSizeOf(IndexType)) == IndexType);
std.debug.assert(@typeInfo(VersionType) == .Int and std.meta.Int(.unsigned, @bitSizeOf(VersionType)) == VersionType);
if (@bitSizeOf(IndexType) + @bitSizeOf(VersionType) != @bitSizeOf(HandleType))
@compileError("IndexType and VersionType must sum to HandleType's bit count");
return struct {
const Self = @This();
handles: []HandleType,
append_cursor: IndexType = 0,
last_destroyed: ?IndexType = null,
allocator: std.mem.Allocator,
const invalid_id = std.math.maxInt(IndexType);
pub const Iterator = struct {
hm: Self,
index: usize = 0,
pub fn init(hm: Self) @This() {
return .{ .hm = hm };
}
pub fn next(self: *@This()) ?HandleType {
if (self.index == self.hm.append_cursor) return null;
for (self.hm.handles[self.index..self.hm.append_cursor]) |h| {
self.index += 1;
if (self.hm.alive(h)) {
return h;
}
}
return null;
}
};
pub fn init(allocator: std.mem.Allocator) Self {
return initWithCapacity(allocator, 32);
}
pub fn initWithCapacity(allocator: std.mem.Allocator, capacity: usize) Self {
return Self{
.handles = allocator.alloc(HandleType, capacity) catch unreachable,
.allocator = allocator,
};
}
pub fn deinit(self: Self) void {
self.allocator.free(self.handles);
}
pub fn extractId(_: Self, handle: HandleType) IndexType {
return @as(IndexType, @truncate(handle & registry.entity_traits.entity_mask));
}
pub fn extractVersion(_: Self, handle: HandleType) VersionType {
return @as(VersionType, @truncate(handle >> registry.entity_traits.entity_shift));
}
fn forge(id: IndexType, version: VersionType) HandleType {
return id | @as(HandleType, version) << registry.entity_traits.entity_shift;
}
pub fn create(self: *Self) HandleType {
if (self.last_destroyed == null) {
// ensure capacity and grow if needed
if (self.handles.len - 1 == self.append_cursor) {
self.handles = self.allocator.realloc(self.handles, self.handles.len * 2) catch unreachable;
}
const id = self.append_cursor;
const handle = forge(self.append_cursor, 0);
self.handles[id] = handle;
self.append_cursor += 1;
return handle;
}
const version = self.extractVersion(self.handles[self.last_destroyed.?]);
const destroyed_id = self.extractId(self.handles[self.last_destroyed.?]);
const handle = forge(self.last_destroyed.?, version);
self.handles[self.last_destroyed.?] = handle;
self.last_destroyed = if (destroyed_id == invalid_id) null else destroyed_id;
return handle;
}
pub fn remove(self: *Self, handle: HandleType) !void {
const id = self.extractId(handle);
if (id > self.append_cursor or self.handles[id] != handle)
return error.RemovedInvalidHandle;
const next_id = self.last_destroyed orelse invalid_id;
if (next_id == id) return error.ExhaustedEntityRemoval;
const version = self.extractVersion(handle);
self.handles[id] = forge(next_id, version +% 1);
self.last_destroyed = id;
}
pub fn alive(self: Self, handle: HandleType) bool {
const id = self.extractId(handle);
return id < self.append_cursor and self.handles[id] == handle;
}
pub fn iterator(self: Self) Iterator {
return Iterator.init(self);
}
};
}
test "handles" {
var hm = Handles(u32, u20, u12).init(std.testing.allocator);
defer hm.deinit();
const e0 = hm.create();
const e1 = hm.create();
const e2 = hm.create();
std.debug.assert(hm.alive(e0));
std.debug.assert(hm.alive(e1));
std.debug.assert(hm.alive(e2));
hm.remove(e1) catch unreachable;
std.debug.assert(!hm.alive(e1));
try std.testing.expectError(error.RemovedInvalidHandle, hm.remove(e1));
var e_tmp = hm.create();
std.debug.assert(hm.alive(e_tmp));
hm.remove(e_tmp) catch unreachable;
std.debug.assert(!hm.alive(e_tmp));
hm.remove(e0) catch unreachable;
std.debug.assert(!hm.alive(e0));
hm.remove(e2) catch unreachable;
std.debug.assert(!hm.alive(e2));
e_tmp = hm.create();
std.debug.assert(hm.alive(e_tmp));
e_tmp = hm.create();
std.debug.assert(hm.alive(e_tmp));
e_tmp = hm.create();
std.debug.assert(hm.alive(e_tmp));
}

@ -0,0 +1,644 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const utils = @import("utils.zig");
const Handles = @import("handles.zig").Handles;
const SparseSet = @import("sparse_set.zig").SparseSet;
const ComponentStorage = @import("component_storage.zig").ComponentStorage;
const Sink = @import("../signals/sink.zig").Sink;
const TypeStore = @import("type_store.zig").TypeStore;
// allow overriding EntityTraits by setting in root via: EntityTraits = EntityTraitsType(.medium);
const root = @import("root");
pub const entity_traits = if (@hasDecl(root, "EntityTraits")) root.EntityTraits.init() else @import("entity.zig").EntityTraits.init();
// setup the Handles type based on the type set in EntityTraits
pub const EntityHandles = Handles(entity_traits.entity_type, entity_traits.index_type, entity_traits.version_type);
pub const Entity = entity_traits.entity_type;
const BasicView = @import("views.zig").BasicView;
const MultiView = @import("views.zig").MultiView;
const BasicGroup = @import("groups.zig").BasicGroup;
const OwningGroup = @import("groups.zig").OwningGroup;
/// Stores an ArrayList of components. The max amount that can be stored is based on the type below
pub fn Storage(comptime CompT: type) type {
return ComponentStorage(CompT, Entity);
}
/// the registry is the main gateway to all ecs functionality. It assumes all internal allocations will succeed and returns
/// no errors to keep the API clean and because if a component array cant be allocated you've got bigger problems.
pub const Registry = struct {
handles: EntityHandles,
components: std.AutoHashMap(u32, usize),
contexts: std.AutoHashMap(u32, usize),
groups: std.ArrayList(*GroupData),
type_store: TypeStore,
allocator: std.mem.Allocator,
/// internal, persistant data structure to manage the entities in a group
pub const GroupData = struct {
hash: u64,
size: u8,
/// optional. there will be an entity_set for non-owning groups and current for owning
entity_set: SparseSet(Entity) = undefined,
owned: []u32,
include: []u32,
exclude: []u32,
registry: *Registry,
current: usize,
pub fn initPtr(allocator: std.mem.Allocator, registry: *Registry, hash: u64, owned: []u32, include: []u32, exclude: []u32) *GroupData {
// std.debug.assert(std.mem.indexOfAny(u32, owned, include) == null);
// std.debug.assert(std.mem.indexOfAny(u32, owned, exclude) == null);
// std.debug.assert(std.mem.indexOfAny(u32, include, exclude) == null);
var group_data = allocator.create(GroupData) catch unreachable;
group_data.hash = hash;
group_data.size = @as(u8, @intCast(owned.len + include.len + exclude.len));
if (owned.len == 0) {
group_data.entity_set = SparseSet(Entity).init(allocator);
}
group_data.owned = allocator.dupe(u32, owned) catch unreachable;
group_data.include = allocator.dupe(u32, include) catch unreachable;
group_data.exclude = allocator.dupe(u32, exclude) catch unreachable;
group_data.registry = registry;
group_data.current = 0;
return group_data;
}
pub fn deinit(self: *GroupData, allocator: std.mem.Allocator) void {
// only deinit th SparseSet for non-owning groups
if (self.owned.len == 0) {
self.entity_set.deinit();
}
allocator.free(self.owned);
allocator.free(self.include);
allocator.free(self.exclude);
allocator.destroy(self);
}
pub fn maybeValidIf(self: *GroupData, entity: Entity) void {
const isValid: bool = blk: {
for (self.owned) |tid| {
const ptr = self.registry.components.get(tid).?;
if (!@as(*Storage(u1), @ptrFromInt(ptr)).contains(entity))
break :blk false;
}
for (self.include) |tid| {
const ptr = self.registry.components.get(tid).?;
if (!@as(*Storage(u1), @ptrFromInt(ptr)).contains(entity))
break :blk false;
}
for (self.exclude) |tid| {
const ptr = self.registry.components.get(tid).?;
if (@as(*Storage(u1), @ptrFromInt(ptr)).contains(entity))
break :blk false;
}
break :blk true;
};
if (self.owned.len == 0) {
if (isValid and !self.entity_set.contains(entity)) {
self.entity_set.add(entity);
}
} else {
if (isValid) {
const ptr = self.registry.components.get(self.owned[0]).?;
if (!(@as(*Storage(u1), @ptrFromInt(ptr)).set.index(entity) < self.current)) {
for (self.owned) |tid| {
// store.swap hides a safe version that types it correctly
const store_ptr = self.registry.components.get(tid).?;
var store = @as(*Storage(u1), @ptrFromInt(store_ptr));
store.swap(store.data()[self.current], entity);
}
self.current += 1;
}
}
std.debug.assert(self.owned.len >= 0);
}
}
pub fn discardIf(self: *GroupData, entity: Entity) void {
if (self.owned.len == 0) {
if (self.entity_set.contains(entity)) {
self.entity_set.remove(entity);
}
} else {
const ptr = self.registry.components.get(self.owned[0]).?;
var store = @as(*Storage(u1), @ptrFromInt(ptr));
if (store.contains(entity) and store.set.index(entity) < self.current) {
self.current -= 1;
for (self.owned) |tid| {
const store_ptr = self.registry.components.get(tid).?;
store = @as(*Storage(u1), @ptrFromInt(store_ptr));
store.swap(store.data()[self.current], entity);
}
}
}
}
/// finds the insertion point for this group by finding anything in the group family (overlapping owned)
/// and finds the least specialized (based on size). This allows the least specialized to update first
/// which ensures more specialized (ie less matches) will always be swapping inside the bounds of
/// the less specialized groups.
fn findInsertionIndex(self: GroupData, groups: []*GroupData) ?usize {
for (groups, 0..) |grp, i| {
var overlapping: u8 = 0;
for (grp.owned) |grp_owned| {
if (std.mem.indexOfScalar(u32, self.owned, grp_owned)) |_| overlapping += 1;
}
if (overlapping > 0 and self.size <= grp.size) return i;
}
return null;
}
// TODO: is this the right logic? Should this return just the previous item in the family or be more specific about
// the group size for the index it returns?
/// for discards, the most specialized group in the family needs to do its discard and swap first. This will ensure
/// as each more specialized group does their discards the entity will always remain outside of the "current" index
/// for all groups in the family.
fn findPreviousIndex(self: GroupData, groups: []*GroupData, index: ?usize) ?usize {
if (groups.len == 0) return null;
// we iterate backwards and either index or groups.len is one tick passed where we want to start
var i = if (index) |ind| ind else groups.len;
if (i > 0) i -= 1;
while (i >= 0) : (i -= 1) {
var overlapping: u8 = 0;
for (groups[i].owned) |grp_owned| {
if (std.mem.indexOfScalar(u32, self.owned, grp_owned)) |_| overlapping += 1;
}
if (overlapping > 0) return i;
if (i == 0) return null;
}
return null;
}
};
pub fn init(allocator: std.mem.Allocator) Registry {
return Registry{
.handles = EntityHandles.init(allocator),
.components = std.AutoHashMap(u32, usize).init(allocator),
.contexts = std.AutoHashMap(u32, usize).init(allocator),
.groups = std.ArrayList(*GroupData).init(allocator),
.type_store = TypeStore.init(allocator),
.allocator = allocator,
};
}
pub fn deinit(self: *Registry) void {
var iter = self.components.valueIterator();
while (iter.next()) |ptr| {
// HACK: we dont know the Type here but we need to call deinit
var storage = @as(*Storage(u1), @ptrFromInt(ptr.*));
storage.deinit();
}
for (self.groups.items) |grp| {
grp.deinit(self.allocator);
}
self.components.deinit();
self.contexts.deinit();
self.groups.deinit();
self.type_store.deinit();
self.handles.deinit();
}
pub fn assure(self: *Registry, comptime T: type) *Storage(T) {
var type_id = utils.typeId(T);
if (self.components.getEntry(type_id)) |kv| {
return @as(*Storage(T), @ptrFromInt(kv.value_ptr.*));
}
var comp_set = Storage(T).initPtr(self.allocator);
var comp_set_ptr = @intFromPtr(comp_set);
_ = self.components.put(type_id, comp_set_ptr) catch unreachable;
return comp_set;
}
/// Prepares a pool for the given type if required
pub fn prepare(self: *Registry, comptime T: type) void {
_ = self.assure(T);
}
/// Returns the number of existing components of the given type
pub fn len(self: *Registry, comptime T: type) usize {
return self.assure(T).len();
}
/// Increases the capacity of the registry or of the pools for the given component
pub fn reserve(self: *Registry, comptime T: type, cap: usize) void {
self.assure(T).reserve(cap);
}
/// Direct access to the list of components of a given pool
pub fn raw(self: Registry, comptime T: type) []T {
return self.assure(T).raw();
}
/// Direct access to the list of entities of a given pool
pub fn data(self: Registry, comptime T: type) []Entity {
return self.assure(T).data().*;
}
pub fn valid(self: *Registry, entity: Entity) bool {
return self.handles.alive(entity);
}
/// Returns the entity identifier without the version
pub fn entityId(_: Registry, entity: Entity) Entity {
return entity & entity_traits.entity_mask;
}
/// Returns the version stored along with an entity identifier
pub fn version(_: *Registry, entity: Entity) entity_traits.version_type {
return @as(entity_traits.version_type, @truncate(entity >> entity_traits.entity_shift));
}
/// Creates a new entity and returns it
pub fn create(self: *Registry) Entity {
return self.handles.create();
}
/// Destroys an entity
pub fn destroy(self: *Registry, entity: Entity) void {
assert(self.valid(entity));
self.removeAll(entity);
self.handles.remove(entity) catch unreachable;
}
/// returns an interator that iterates all live entities
pub fn entities(self: Registry) EntityHandles.Iterator {
return self.handles.iterator();
}
pub fn add(self: *Registry, entity: Entity, value: anytype) void {
assert(self.valid(entity));
self.assure(@TypeOf(value)).add(entity, value);
}
/// shortcut for adding raw comptime_int/float without having to @as cast
pub fn addTyped(self: *Registry, comptime T: type, entity: Entity, value: T) void {
self.add(entity, value);
}
/// adds all the component types passed in as zero-initialized values
pub fn addTypes(self: *Registry, entity: Entity, comptime types: anytype) void {
inline for (types) |t| {
self.assure(t).add(entity, std.mem.zeroes(t));
}
}
/// Replaces the given component for an entity
pub fn replace(self: *Registry, entity: Entity, value: anytype) void {
assert(self.valid(entity));
self.assure(@TypeOf(value)).replace(entity, value);
}
/// shortcut for replacing raw comptime_int/float without having to @as cast
pub fn replaceTyped(self: *Registry, comptime T: type, entity: Entity, value: T) void {
self.replace(entity, value);
}
pub fn addOrReplace(self: *Registry, entity: Entity, value: anytype) void {
assert(self.valid(entity));
const store = self.assure(@TypeOf(value));
if (store.tryGet(entity)) |found| {
found.* = value;
store.update.publish(entity);
} else {
store.add(entity, value);
}
}
/// shortcut for add-or-replace raw comptime_int/float without having to @as cast
pub fn addOrReplaceTyped(self: *Registry, comptime T: type, entity: Entity, value: T) void {
self.addOrReplace(entity, value);
}
/// Removes the given component from an entity
pub fn remove(self: *Registry, comptime T: type, entity: Entity) void {
assert(self.valid(entity));
self.assure(T).remove(entity);
}
pub fn removeIfExists(self: *Registry, comptime T: type, entity: Entity) void {
assert(self.valid(entity));
var store = self.assure(T);
if (store.contains(entity)) {
store.remove(entity);
}
}
/// Removes all the components from an entity and makes it orphaned
pub fn removeAll(self: *Registry, entity: Entity) void {
assert(self.valid(entity));
var iter = self.components.valueIterator();
while (iter.next()) |value| {
// HACK: we dont know the Type here but we need to be able to call methods on the Storage(T)
var store = @as(*Storage(u1), @ptrFromInt(value.*));
store.removeIfContains(entity);
}
}
pub fn has(self: *Registry, comptime T: type, entity: Entity) bool {
assert(self.valid(entity));
return self.assure(T).set.contains(entity);
}
pub fn get(self: *Registry, comptime T: type, entity: Entity) *T {
assert(self.valid(entity));
return self.assure(T).get(entity);
}
pub fn getConst(self: *Registry, comptime T: type, entity: Entity) T {
assert(self.valid(entity));
return self.assure(T).getConst(entity);
}
/// Returns a reference to the given component for an entity creating it if necessary
pub fn getOrAdd(self: *Registry, comptime T: type, entity: Entity) *T {
if (!self.has(T, entity)) {
self.addTyped(T, entity, .{});
}
return self.get(T, entity);
}
pub fn tryGet(self: *Registry, comptime T: type, entity: Entity) ?*T {
return self.assure(T).tryGet(entity);
}
/// Returns a Sink object for the given component to add/remove listeners with
pub fn onConstruct(self: *Registry, comptime T: type) Sink(Entity) {
return self.assure(T).onConstruct();
}
/// Returns a Sink object for the given component to add/remove listeners with
pub fn onUpdate(self: *Registry, comptime T: type) Sink(Entity) {
return self.assure(T).onUpdate();
}
/// Returns a Sink object for the given component to add/remove listeners with
pub fn onDestruct(self: *Registry, comptime T: type) Sink(Entity) {
return self.assure(T).onDestruct();
}
/// Binds an object to the context of the registry
pub fn setContext(self: *Registry, context: anytype) void {
std.debug.assert(@typeInfo(@TypeOf(context)) == .Pointer);
var type_id = utils.typeId(@typeInfo(@TypeOf(context)).Pointer.child);
_ = self.contexts.put(type_id, @intFromPtr(context)) catch unreachable;
}
/// Unsets a context variable if it exists
pub fn unsetContext(self: *Registry, comptime T: type) void {
std.debug.assert(@typeInfo(T) != .Pointer);
_ = self.contexts.put(utils.typeId(T), 0) catch unreachable;
}
/// Returns a pointer to an object in the context of the registry
pub fn getContext(self: *Registry, comptime T: type) ?*T {
std.debug.assert(@typeInfo(T) != .Pointer);
return if (self.contexts.get(utils.typeId(T))) |ptr|
return if (ptr > 0) @as(*T, @ptrFromInt(ptr)) else null
else
null;
}
/// provides access to a TypeStore letting you add singleton components to the registry
pub fn singletons(self: *Registry) *TypeStore {
return &self.type_store;
}
pub fn sort(self: *Registry, comptime T: type, comptime lessThan: *const fn (void, T, T) bool) void {
const comp = self.assure(T);
std.debug.assert(comp.super == 0);
comp.sort(T, comp.len(), {}, lessThan);
}
/// Checks whether the given component belongs to any group. If so, it is not sortable directly.
pub fn sortable(self: *Registry, comptime T: type) bool {
return self.assure(T).super == 0;
}
pub fn view(self: *Registry, comptime includes: anytype, comptime excludes: anytype) ViewType(includes, excludes) {
std.debug.assert(@typeInfo(@TypeOf(includes)) == .Struct);
std.debug.assert(@typeInfo(@TypeOf(excludes)) == .Struct);
std.debug.assert(includes.len > 0);
// just one include so use the optimized BasicView
if (includes.len == 1 and excludes.len == 0)
return BasicView(includes[0]).init(self.assure(includes[0]));
var includes_arr: [includes.len]u32 = undefined;
inline for (includes, 0..) |t, i| {
_ = self.assure(t);
includes_arr[i] = utils.typeId(t);
}
var excludes_arr: [excludes.len]u32 = undefined;
inline for (excludes, 0..) |t, i| {
_ = self.assure(t);
excludes_arr[i] = utils.typeId(t);
}
return MultiView(includes.len, excludes.len).init(self, includes_arr, excludes_arr);
}
/// returns the Type that a view will be based on the includes and excludes
fn ViewType(comptime includes: anytype, comptime excludes: anytype) type {
if (includes.len == 1 and excludes.len == 0) return BasicView(includes[0]);
return MultiView(includes.len, excludes.len);
}
/// creates an optimized group for iterating components
pub fn group(self: *Registry, comptime owned: anytype, comptime includes: anytype, comptime excludes: anytype) (if (owned.len == 0) BasicGroup else OwningGroup) {
std.debug.assert(@typeInfo(@TypeOf(owned)) == .Struct);
std.debug.assert(@typeInfo(@TypeOf(includes)) == .Struct);
std.debug.assert(@typeInfo(@TypeOf(excludes)) == .Struct);
std.debug.assert(owned.len + includes.len > 0);
std.debug.assert(owned.len + includes.len + excludes.len > 1);
// create a unique hash to identify the group so that we can look it up
const hash = comptime hashGroupTypes(owned, includes, excludes);
for (self.groups.items) |grp| {
if (grp.hash == hash) {
if (owned.len == 0) {
return BasicGroup.init(self, grp);
}
var first_owned = self.assure(owned[0]);
return OwningGroup.init(self, grp, &first_owned.super);
}
}
// gather up all our Types as typeIds
var includes_arr: [includes.len]u32 = undefined;
inline for (includes, 0..) |t, i| {
_ = self.assure(t);
includes_arr[i] = utils.typeId(t);
}
var excludes_arr: [excludes.len]u32 = undefined;
inline for (excludes, 0..) |t, i| {
_ = self.assure(t);
excludes_arr[i] = utils.typeId(t);
}
var owned_arr: [owned.len]u32 = undefined;
inline for (owned, 0..) |t, i| {
_ = self.assure(t);
owned_arr[i] = utils.typeId(t);
}
// we need to create a new GroupData
var new_group_data = GroupData.initPtr(self.allocator, self, hash, owned_arr[0..], includes_arr[0..], excludes_arr[0..]);
// before adding the group we need to do some checks to make sure there arent other owning groups with the same types
if (builtin.mode == .Debug and owned.len > 0) {
for (self.groups.items) |grp| {
if (grp.owned.len == 0) continue;
var overlapping: u8 = 0;
for (grp.owned) |grp_owned| {
if (std.mem.indexOfScalar(u32, &owned_arr, grp_owned)) |_| overlapping += 1;
}
var sz: u8 = overlapping;
for (grp.include) |grp_include| {
if (std.mem.indexOfScalar(u32, &includes_arr, grp_include)) |_| sz += 1;
}
for (grp.exclude) |grp_exclude| {
if (std.mem.indexOfScalar(u32, &excludes_arr, grp_exclude)) |_| sz += 1;
}
const check = overlapping == 0 or ((sz == new_group_data.size) or (sz == grp.size));
std.debug.assert(check);
}
}
var maybe_valid_if: ?*GroupData = null;
var discard_if: ?*GroupData = null;
if (owned.len == 0) {
self.groups.append(new_group_data) catch unreachable;
} else {
// if this is a group in a family, we may need to do an insert so get the insertion index first
const maybe_index = new_group_data.findInsertionIndex(self.groups.items);
// if there is a previous group in this family, we use it for inserting our discardIf calls
if (new_group_data.findPreviousIndex(self.groups.items, maybe_index)) |prev| {
discard_if = self.groups.items[prev];
}
if (maybe_index) |index| {
maybe_valid_if = self.groups.items[index];
self.groups.insert(index, new_group_data) catch unreachable;
} else {
self.groups.append(new_group_data) catch unreachable;
}
// update super on all owned Storages to be the max of size and their current super value
inline for (owned) |t| {
var storage = self.assure(t);
storage.super = @max(storage.super, new_group_data.size);
}
}
// wire up our listeners
inline for (owned) |t| self.onConstruct(t).beforeBound(maybe_valid_if).connectBound(new_group_data, "maybeValidIf");
inline for (includes) |t| self.onConstruct(t).beforeBound(maybe_valid_if).connectBound(new_group_data, "maybeValidIf");
inline for (excludes) |t| self.onDestruct(t).beforeBound(maybe_valid_if).connectBound(new_group_data, "maybeValidIf");
inline for (owned) |t| self.onDestruct(t).beforeBound(discard_if).connectBound(new_group_data, "discardIf");
inline for (includes) |t| self.onDestruct(t).beforeBound(discard_if).connectBound(new_group_data, "discardIf");
inline for (excludes) |t| self.onConstruct(t).beforeBound(discard_if).connectBound(new_group_data, "discardIf");
// pre-fill the GroupData with any existing entitites that match
if (owned.len == 0) {
var view_instance = self.view(owned ++ includes, excludes);
var view_iter = view_instance.entityIterator();
while (view_iter.next()) |entity| {
new_group_data.entity_set.add(entity);
}
} else {
// 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);
}
// for(auto *first = std::get<0>(cpools).data(), *last = first + std::get<0>(cpools).size(); first != last; ++first) {
// handler->template maybe_valid_if<std::tuple_element_t<0, std::tuple<std::decay_t<Owned>...>>>(*this, *first);
// }
}
if (owned.len == 0) {
return BasicGroup.init(self, new_group_data);
} else {
var first_owned_storage = self.assure(owned[0]);
return OwningGroup.init(self, new_group_data, &first_owned_storage.super);
}
}
/// given the 3 group Types arrays, generates a (mostly) unique u64 hash. Simultaneously ensures there are no duped types between
/// the 3 groups.
inline fn hashGroupTypes(comptime owned: anytype, comptime includes: anytype, comptime excludes: anytype) u64 {
comptime {
for (owned) |t1| {
for (includes) |t2| {
std.debug.assert(t1 != t2);
for (excludes) |t3| {
std.debug.assert(t1 != t3);
std.debug.assert(t2 != t3);
}
}
}
const owned_str = concatTypes(owned);
const includes_str = concatTypes(includes);
const excludes_str = concatTypes(excludes);
return utils.hashStringFnv(u64, owned_str ++ includes_str ++ excludes_str);
}
}
/// expects a tuple of types. Convertes them to type names, sorts them then concatenates and returns the string.
inline fn concatTypes(comptime types: anytype) []const u8 {
comptime {
if (types.len == 0) return "_";
const impl = struct {
fn asc(_: void, lhs: []const u8, rhs: []const u8) bool {
return std.mem.lessThan(u8, lhs, rhs);
}
};
var names: [types.len][]const u8 = undefined;
for (&names, 0..) |*name, i| {
name.* = @typeName(types[i]);
}
std.sort.pdq([]const u8, &names, {}, impl.asc);
comptime var res: []const u8 = "";
inline for (names) |name| res = res ++ name;
return res;
}
}
};

@ -0,0 +1,367 @@
const std = @import("std");
const utils = @import("utils.zig");
const registry = @import("registry.zig");
const ReverseSliceIterator = @import("utils.zig").ReverseSliceIterator;
/// NOTE: This is a copy of `std.sort.insertionSort` with fixed function pointer
/// syntax to avoid compilation errors.
///
/// Stable in-place sort. O(n) best case, O(pow(n, 2)) worst case.
/// O(1) memory (no allocator required).
/// This can be expressed in terms of `insertionSortContext` but the glue
/// code is slightly longer than the direct implementation.
fn std_sort_insertionSort_clone(
comptime T: type,
items: []T,
context: anytype,
comptime lessThan: *const fn (context: @TypeOf(context), lhs: T, rhs: T) bool,
) void {
var i: usize = 1;
while (i < items.len) : (i += 1) {
const x = items[i];
var j: usize = i;
while (j > 0 and lessThan(context, x, items[j - 1])) : (j -= 1) {
items[j] = items[j - 1];
}
items[j] = x;
}
}
// TODO: fix entity_mask. it should come from EntityTraitsDefinition.
pub fn SparseSet(comptime SparseT: type) type {
return struct {
const Self = @This();
const page_size: usize = 4096;
sparse: std.ArrayList(?[]SparseT),
dense: std.ArrayList(SparseT),
entity_mask: SparseT,
allocator: ?std.mem.Allocator,
pub fn initPtr(allocator: std.mem.Allocator) *Self {
var set = allocator.create(Self) catch unreachable;
set.sparse = std.ArrayList(?[]SparseT).initCapacity(allocator, 16) catch unreachable;
set.dense = std.ArrayList(SparseT).initCapacity(allocator, 16) catch unreachable;
set.entity_mask = registry.entity_traits.entity_mask;
set.allocator = allocator;
return set;
}
pub fn init(allocator: std.mem.Allocator) Self {
return Self{
.sparse = std.ArrayList(?[]SparseT).init(allocator),
.dense = std.ArrayList(SparseT).init(allocator),
.entity_mask = registry.entity_traits.entity_mask,
.allocator = null,
};
}
pub fn deinit(self: *Self) void {
for (self.sparse.items) |array| {
if (array) |arr| {
self.sparse.allocator.free(arr);
}
}
self.dense.deinit();
self.sparse.deinit();
if (self.allocator) |allocator| {
allocator.destroy(self);
}
}
pub fn page(self: Self, sparse: SparseT) usize {
return (sparse & self.entity_mask) / page_size;
}
fn offset(_: Self, sparse: SparseT) usize {
return sparse & (page_size - 1);
}
fn assure(self: *Self, pos: usize) []SparseT {
if (pos >= self.sparse.items.len) {
const start_pos = self.sparse.items.len;
self.sparse.resize(pos + 1) catch unreachable;
self.sparse.expandToCapacity();
@memset(self.sparse.items[start_pos..], null);
}
if (self.sparse.items[pos] == null) {
var new_page = self.sparse.allocator.alloc(SparseT, page_size) catch unreachable;
@memset(new_page, std.math.maxInt(SparseT));
self.sparse.items[pos] = new_page;
}
return self.sparse.items[pos].?;
}
/// Increases the capacity of a sparse sets index array
pub fn reserve(self: *Self, cap: usize) void {
self.sparse.resize(cap) catch unreachable;
}
/// Returns the number of dense elements that a sparse set has currently allocated space for
pub fn capacity(self: *Self) usize {
return self.dense.capacity;
}
/// Returns the number of dense elements in a sparse set
pub fn len(self: Self) usize {
return self.dense.items.len;
}
pub fn empty(self: *Self) bool {
return self.dense.items.len == 0;
}
pub fn data(self: Self) []const SparseT {
return self.dense.items;
}
pub fn dataPtr(self: Self) *const []SparseT {
return &self.dense.items;
}
pub fn contains(self: Self, sparse: SparseT) bool {
const curr = self.page(sparse);
return curr < self.sparse.items.len and
self.sparse.items[curr] != null and
self.sparse.items[curr].?[self.offset(sparse)] != std.math.maxInt(SparseT);
}
/// Returns the position of an entity in a sparse set
pub fn index(self: Self, sparse: SparseT) SparseT {
std.debug.assert(self.contains(sparse));
return self.sparse.items[self.page(sparse)].?[self.offset(sparse)];
}
/// Assigns an entity to a sparse set
pub fn add(self: *Self, sparse: SparseT) void {
std.debug.assert(!self.contains(sparse));
// assure(page(entt))[offset(entt)] = packed.size()
self.assure(self.page(sparse))[self.offset(sparse)] = @as(SparseT, @intCast(self.dense.items.len));
_ = self.dense.append(sparse) catch unreachable;
}
/// Removes an entity from a sparse set
pub fn remove(self: *Self, sparse: SparseT) void {
std.debug.assert(self.contains(sparse));
const curr = self.page(sparse);
const pos = self.offset(sparse);
const last_dense = self.dense.items[self.dense.items.len - 1];
self.dense.items[self.sparse.items[curr].?[pos]] = last_dense;
self.sparse.items[self.page(last_dense)].?[self.offset(last_dense)] = self.sparse.items[curr].?[pos];
self.sparse.items[curr].?[pos] = std.math.maxInt(SparseT);
_ = self.dense.pop();
}
/// Swaps two entities in the internal packed and sparse arrays
pub fn swap(self: *Self, lhs: SparseT, rhs: SparseT) void {
var from = &self.sparse.items[self.page(lhs)].?[self.offset(lhs)];
var to = &self.sparse.items[self.page(rhs)].?[self.offset(rhs)];
std.mem.swap(SparseT, &self.dense.items[from.*], &self.dense.items[to.*]);
std.mem.swap(SparseT, from, to);
}
/// Sort elements according to the given comparison function
pub fn sort(self: *Self, context: anytype, comptime lessThan: *const fn (@TypeOf(context), SparseT, SparseT) bool) void {
std_sort_insertionSort_clone(SparseT, self.dense.items, context, lessThan);
for (self.dense.items, 0..) |_, i| {
const item = @as(SparseT, @intCast(i));
self.sparse.items[self.page(self.dense.items[self.page(item)])].?[self.offset(self.dense.items[self.page(item)])] = @as(SparseT, @intCast(i));
}
}
/// Sort elements according to the given comparison function. Use this when a data array needs to stay in sync with the SparseSet
/// by passing in a "swap_context" that contains a "swap" method with a sig of fn(ctx,SparseT,SparseT)void
pub fn arrange(self: *Self, length: usize, context: anytype, comptime lessThan: *const fn (@TypeOf(context), SparseT, SparseT) bool, swap_context: anytype) void {
std_sort_insertionSort_clone(SparseT, self.dense.items[0..length], context, lessThan);
for (self.dense.items[0..length], 0..) |_, pos| {
var curr = @as(SparseT, @intCast(pos));
var next = self.index(self.dense.items[curr]);
while (curr != next) {
swap_context.swap(self.dense.items[curr], self.dense.items[next]);
self.sparse.items[self.page(self.dense.items[curr])].?[self.offset(self.dense.items[curr])] = curr;
curr = next;
next = self.index(self.dense.items[curr]);
}
}
}
/// Sort entities according to their order in another sparse set. Other is the master in this case.
pub fn respect(self: *Self, other: *Self) void {
var pos = @as(SparseT, 0);
var i = @as(SparseT, 0);
while (i < other.dense.items.len) : (i += 1) {
if (self.contains(other.dense.items[i])) {
if (other.dense.items[i] != self.dense.items[pos]) {
self.swap(self.dense.items[pos], other.dense.items[i]);
}
pos += 1;
}
}
}
pub fn clear(self: *Self) void {
for (self.sparse.items, 0..) |array, i| {
if (array) |arr| {
self.sparse.allocator.free(arr);
self.sparse.items[i] = null;
}
}
self.sparse.items.len = 0;
self.dense.items.len = 0;
}
pub fn reverseIterator(self: *Self) ReverseSliceIterator(SparseT) {
return ReverseSliceIterator(SparseT).init(self.dense.items);
}
};
}
fn printSet(set: *SparseSet(u32, u8)) void {
std.debug.print("\nsparse -----\n", .{});
for (set.sparse.items) |sparse| {
std.debug.print("{}\t", .{sparse});
}
std.debug.print("\ndense -----\n", .{});
for (set.dense.items) |dense| {
std.debug.print("{}\t", .{dense});
}
std.debug.print("\n\n", .{});
}
test "add/remove/clear" {
var set = SparseSet(u32).initPtr(std.testing.allocator);
defer set.deinit();
set.add(4);
set.add(3);
try std.testing.expectEqual(set.len(), 2);
try std.testing.expectEqual(set.index(4), 0);
try std.testing.expectEqual(set.index(3), 1);
set.remove(4);
try std.testing.expectEqual(set.len(), 1);
set.clear();
try std.testing.expectEqual(set.len(), 0);
}
test "grow" {
var set = SparseSet(u32).initPtr(std.testing.allocator);
defer set.deinit();
var i = @as(usize, std.math.maxInt(u8));
while (i > 0) : (i -= 1) {
set.add(@as(u32, @intCast(i)));
}
try std.testing.expectEqual(set.len(), std.math.maxInt(u8));
}
test "swap" {
var set = SparseSet(u32).initPtr(std.testing.allocator);
defer set.deinit();
set.add(4);
set.add(3);
try std.testing.expectEqual(set.index(4), 0);
try std.testing.expectEqual(set.index(3), 1);
set.swap(4, 3);
try std.testing.expectEqual(set.index(3), 0);
try std.testing.expectEqual(set.index(4), 1);
}
test "data() synced" {
var set = SparseSet(u32).initPtr(std.testing.allocator);
defer set.deinit();
set.add(0);
set.add(1);
set.add(2);
set.add(3);
var data = set.data();
try std.testing.expectEqual(data[1], 1);
try std.testing.expectEqual(set.len(), data.len);
set.remove(0);
set.remove(1);
try std.testing.expectEqual(set.len(), set.data().len);
}
test "iterate" {
var set = SparseSet(u32).initPtr(std.testing.allocator);
defer set.deinit();
set.add(0);
set.add(1);
set.add(2);
set.add(3);
var i: u32 = @as(u32, @intCast(set.len())) - 1;
var iter = set.reverseIterator();
while (iter.next()) |entity| {
try std.testing.expectEqual(i, entity);
if (i > 0) i -= 1;
}
}
test "respect 1" {
var set1 = SparseSet(u32).initPtr(std.testing.allocator);
defer set1.deinit();
var set2 = SparseSet(u32).initPtr(std.testing.allocator);
defer set2.deinit();
set1.add(3);
set1.add(4);
set1.add(5);
set1.add(6);
set1.add(7);
set2.add(8);
set2.add(6);
set2.add(4);
set1.respect(set2);
try std.testing.expectEqual(set1.dense.items[0], set2.dense.items[1]);
try 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();
set.add(5);
set.add(2);
set.add(4);
set.add(1);
set.add(3);
set.sort({}, desc_u32);
for (set.dense.items, 0..) |item, i| {
if (i < set.dense.items.len - 1) {
std.debug.assert(item > set.dense.items[i + 1]);
}
}
}

@ -0,0 +1,89 @@
const std = @import("std");
const utils = @import("utils.zig");
/// stores a single object of type T for each T added
pub const TypeStore = struct {
map: std.AutoHashMap(u32, []u8),
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) TypeStore {
return TypeStore{
.map = std.AutoHashMap(u32, []u8).init(allocator),
.allocator = allocator,
};
}
pub fn deinit(self: *TypeStore) void {
var iter = self.map.valueIterator();
while (iter.next()) |val_ptr| {
self.allocator.free(val_ptr.*);
}
self.map.deinit();
}
/// adds instance, returning a pointer to the item as it lives in the store
pub fn add(self: *TypeStore, instance: anytype) void {
var bytes = self.allocator.alloc(u8, @sizeOf(@TypeOf(instance))) catch unreachable;
std.mem.copy(u8, bytes, std.mem.asBytes(&instance));
_ = self.map.put(utils.typeId(@TypeOf(instance)), bytes) catch unreachable;
}
pub fn get(self: *TypeStore, comptime T: type) *T {
if (self.map.get(utils.typeId(T))) |bytes| {
return @as(*T, @ptrCast(@alignCast(bytes)));
}
unreachable;
}
pub fn getConst(self: *TypeStore, comptime T: type) T {
return self.get(T).*;
}
pub fn getOrAdd(self: *TypeStore, comptime T: type) *T {
if (!self.has(T)) {
var instance = std.mem.zeroes(T);
self.add(instance);
}
return self.get(T);
}
pub fn remove(self: *TypeStore, comptime T: type) void {
if (self.map.get(utils.typeId(T))) |bytes| {
self.allocator.free(bytes);
_ = self.map.remove(utils.typeId(T));
}
}
pub fn has(self: *TypeStore, comptime T: type) bool {
return self.map.contains(utils.typeId(T));
}
};
test "TypeStore" {
const Vector = struct { x: f32 = 0, y: f32 = 0, z: f32 = 0 };
var store = TypeStore.init(std.testing.allocator);
defer store.deinit();
var orig = Vector{ .x = 5, .y = 6, .z = 8 };
store.add(orig);
try std.testing.expect(store.has(Vector));
try std.testing.expectEqual(store.get(Vector).*, orig);
var v = store.get(Vector);
try std.testing.expectEqual(v.*, Vector{ .x = 5, .y = 6, .z = 8 });
v.*.x = 666;
var v2 = store.get(Vector);
try std.testing.expectEqual(v2.*, Vector{ .x = 666, .y = 6, .z = 8 });
store.remove(Vector);
try std.testing.expect(!store.has(Vector));
var v3 = store.getOrAdd(u32);
try std.testing.expectEqual(v3.*, 0);
v3.* = 777;
_ = store.get(u32);
try std.testing.expectEqual(v3.*, 777);
}

@ -0,0 +1,139 @@
const std = @import("std");
pub const ErasedPtr = struct {
ptr: usize,
pub fn init(ptr: anytype) ErasedPtr {
if (@sizeOf(@TypeOf(ptr)) == 0) {
return .{ .ptr = undefined };
}
return .{ .ptr = @intFromPtr(ptr) };
}
pub fn as(self: ErasedPtr, comptime T: type) *T {
if (@sizeOf(T) == 0)
return @as(T, undefined);
return self.asPtr(*T);
}
pub fn asPtr(self: ErasedPtr, comptime PtrT: type) PtrT {
if (@sizeOf(PtrT) == 0)
return @as(PtrT, undefined);
return @as(PtrT, @ptrFromInt(self.ptr));
}
};
pub fn ReverseSliceIterator(comptime T: type) type {
return struct {
slice: []T,
index: usize,
pub fn init(slice: []T) @This() {
return .{
.slice = slice,
.index = slice.len,
};
}
pub fn next(self: *@This()) ?T {
if (self.index == 0) return null;
self.index -= 1;
return self.slice[self.index];
}
pub fn reset(self: *@This()) void {
self.index = self.slice.len;
}
};
}
/// 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, comptime lessThan: *const 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) {
items[j] = items[j - 1];
sub_items[j] = sub_items[j - 1];
}
items[j] = x;
sub_items[j] = y;
}
}
pub fn sortSubSub(comptime T1: type, comptime T2: type, items: []T1, sub_items: []T2, context: anytype, comptime lessThan: *const fn (@TypeOf(context), 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(context, x, items[j - 1])) : (j -= 1) {
items[j] = items[j - 1];
sub_items[j] = sub_items[j - 1];
}
items[j] = x;
sub_items[j] = y;
}
}
/// comptime string hashing for the type names
pub fn typeId(comptime T: type) u32 {
return hashStringFnv(u32, @typeName(T));
}
/// comptime string hashing for the type names
pub fn typeId64(comptime T: type) u64 {
return hashStringFnv(u64, @typeName(T));
}
/// u32 Fowler-Noll-Vo string hash
pub fn hashString(comptime str: []const u8) u32 {
return hashStringFnv(u32, str);
}
/// FowlerNollVo string hash. ReturnType should be u32/u64
pub fn hashStringFnv(comptime ReturnType: type, comptime str: []const u8) ReturnType {
std.debug.assert(ReturnType == u32 or ReturnType == u64);
const prime = if (ReturnType == u32) @as(u32, 16777619) else @as(u64, 1099511628211);
var value = if (ReturnType == u32) @as(u32, 2166136261) else @as(u64, 14695981039346656037);
for (str) |c| {
value = (value ^ @as(u32, @intCast(c))) *% prime;
}
return value;
}
/// comptime string hashing, djb2 by Dan Bernstein. Fails on large strings.
pub fn hashStringDjb2(comptime str: []const u8) comptime_int {
var hash: comptime_int = 5381;
for (str) |c| {
hash = ((hash << 5) + hash) + @as(comptime_int, @intCast(c));
}
return hash;
}
pub fn isComptime(comptime T: type) bool {
return switch (@typeInfo(T)) {
.ComptimeInt, .ComptimeFloat => true,
else => false,
};
}
test "ReverseSliceIterator" {
var slice = std.testing.allocator.alloc(usize, 10) catch unreachable;
defer std.testing.allocator.free(slice);
for (slice, 0..) |*item, i| {
item.* = i;
}
var iter = ReverseSliceIterator(usize).init(slice);
var i: usize = 9;
while (iter.next()) |val| {
try std.testing.expectEqual(i, val);
if (i > 0) i -= 1;
}
}

@ -0,0 +1,295 @@
const std = @import("std");
const utils = @import("utils.zig");
const Registry = @import("registry.zig").Registry;
const Storage = @import("registry.zig").Storage;
const Entity = @import("registry.zig").Entity;
const ReverseSliceIterator = @import("utils.zig").ReverseSliceIterator;
/// single item view. Iterating raw() directly is the fastest way to get at the data. An iterator is also available to iterate
/// either the Entities or the Components. If T is sorted note that raw() will be in the reverse order so it should be looped
/// backwards. The iterators will return data in the sorted order though.
pub fn BasicView(comptime T: type) type {
return struct {
const Self = @This();
storage: *Storage(T),
pub fn init(storage: *Storage(T)) Self {
return Self{
.storage = storage,
};
}
pub fn len(self: Self) usize {
return self.storage.len();
}
/// Direct access to the array of components
pub fn raw(self: Self) []T {
return self.storage.raw();
}
/// Direct access to the array of entities
pub fn data(self: Self) []const Entity {
return self.storage.data();
}
/// Returns the object associated with an entity
pub fn get(self: Self, entity: Entity) *T {
return self.storage.get(entity);
}
pub fn getConst(self: *Self, entity: Entity) T {
return self.storage.getConst(entity);
}
pub fn iterator(self: Self) utils.ReverseSliceIterator(T) {
return utils.ReverseSliceIterator(T).init(self.storage.instances.items);
}
pub fn entityIterator(self: Self) utils.ReverseSliceIterator(Entity) {
return self.storage.set.reverseIterator();
}
};
}
pub fn MultiView(comptime n_includes: usize, comptime n_excludes: usize) type {
return struct {
const Self = @This();
registry: *Registry,
type_ids: [n_includes]u32,
exclude_type_ids: [n_excludes]u32,
pub const Iterator = struct {
view: *Self,
internal_it: ReverseSliceIterator(Entity),
pub fn init(view: *Self) Iterator {
const ptr = view.registry.components.get(view.type_ids[0]).?;
const internal_it = @as(*Storage(u8), @ptrFromInt(ptr)).set.reverseIterator();
return .{ .view = view, .internal_it = internal_it };
}
pub fn next(it: *Iterator) ?Entity {
while (it.internal_it.next()) |entity| blk: {
// entity must be in all other Storages
for (it.view.type_ids) |tid| {
const ptr = it.view.registry.components.get(tid).?;
if (!@as(*Storage(u1), @ptrFromInt(ptr)).contains(entity)) {
break :blk;
}
}
// entity must not be in all other excluded Storages
for (it.view.exclude_type_ids) |tid| {
const ptr = it.view.registry.components.get(tid).?;
if (@as(*Storage(u1), @ptrFromInt(ptr)).contains(entity)) {
break :blk;
}
}
return entity;
}
return null;
}
// Reset the iterator to the initial index
pub fn reset(it: *Iterator) void {
// Assign new iterator instance in case entities have been
// removed or added.
it.internal_it = it.getInternalIteratorInstance();
}
fn getInternalIteratorInstance(it: *Iterator) ReverseSliceIterator(Entity) {
const ptr = it.view.registry.components.get(it.view.type_ids[0]).?;
return @as(*Storage(u8), @ptrFromInt(ptr)).set.reverseIterator();
}
};
pub fn init(registry: *Registry, type_ids: [n_includes]u32, exclude_type_ids: [n_excludes]u32) Self {
return Self{
.registry = registry,
.type_ids = type_ids,
.exclude_type_ids = exclude_type_ids,
};
}
pub fn get(self: *Self, comptime T: type, entity: Entity) *T {
return self.registry.assure(T).get(entity);
}
pub fn getConst(self: *Self, comptime T: type, entity: Entity) T {
return self.registry.assure(T).getConst(entity);
}
fn sort(self: *Self) void {
// get our component counts in an array so we can sort the type_ids based on how many entities are in each
var sub_items: [n_includes]usize = undefined;
for (self.type_ids, 0..) |tid, i| {
const ptr = self.registry.components.get(tid).?;
const store = @as(*Storage(u8), @ptrFromInt(ptr));
sub_items[i] = store.len();
}
const asc_usize = struct {
fn sort(_: void, a: usize, b: usize) bool {
return a < b;
}
};
utils.sortSub(usize, u32, sub_items[0..], self.type_ids[0..], asc_usize.sort);
}
pub fn entityIterator(self: *Self) Iterator {
self.sort();
return Iterator.init(self);
}
};
}
test "single basic view" {
var store = Storage(f32).init(std.testing.allocator);
defer store.deinit();
store.add(3, 30);
store.add(5, 50);
store.add(7, 70);
var view = BasicView(f32).init(&store);
try std.testing.expectEqual(view.len(), 3);
store.remove(7);
try std.testing.expectEqual(view.len(), 2);
var i: usize = 0;
var iter = view.iterator();
while (iter.next()) |comp| {
if (i == 0) try std.testing.expectEqual(comp, 50);
if (i == 1) try std.testing.expectEqual(comp, 30);
i += 1;
}
i = 0;
var entIter = view.entityIterator();
while (entIter.next()) |ent| {
if (i == 0) {
try std.testing.expectEqual(ent, 5);
try std.testing.expectEqual(view.getConst(ent), 50);
}
if (i == 1) {
try std.testing.expectEqual(ent, 3);
try std.testing.expectEqual(view.getConst(ent), 30);
}
i += 1;
}
}
test "single basic view data" {
var store = Storage(f32).init(std.testing.allocator);
defer store.deinit();
store.add(3, 30);
store.add(5, 50);
store.add(7, 70);
var view = BasicView(f32).init(&store);
try std.testing.expectEqual(view.get(3).*, 30);
for (view.data(), 0..) |entity, i| {
if (i == 0)
try std.testing.expectEqual(entity, 3);
if (i == 1)
try std.testing.expectEqual(entity, 5);
if (i == 2)
try std.testing.expectEqual(entity, 7);
}
for (view.raw(), 0..) |data, i| {
if (i == 0)
try std.testing.expectEqual(data, 30);
if (i == 1)
try std.testing.expectEqual(data, 50);
if (i == 2)
try std.testing.expectEqual(data, 70);
}
try std.testing.expectEqual(view.len(), 3);
}
test "basic multi view" {
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var e0 = reg.create();
var e1 = reg.create();
var e2 = reg.create();
reg.add(e0, @as(i32, -0));
reg.add(e1, @as(i32, -1));
reg.add(e2, @as(i32, -2));
reg.add(e0, @as(u32, 0));
reg.add(e2, @as(u32, 2));
_ = reg.view(.{u32}, .{});
var view = reg.view(.{ i32, u32 }, .{});
var iterated_entities: usize = 0;
var iter = view.entityIterator();
while (iter.next()) |_| {
iterated_entities += 1;
}
try std.testing.expectEqual(iterated_entities, 2);
iterated_entities = 0;
reg.remove(u32, e0);
iter.reset();
while (iter.next()) |_| {
iterated_entities += 1;
}
try std.testing.expectEqual(iterated_entities, 1);
}
test "basic multi view with excludes" {
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var e0 = reg.create();
var e1 = reg.create();
var e2 = reg.create();
reg.add(e0, @as(i32, -0));
reg.add(e1, @as(i32, -1));
reg.add(e2, @as(i32, -2));
reg.add(e0, @as(u32, 0));
reg.add(e2, @as(u32, 2));
reg.add(e2, @as(u8, 255));
var view = reg.view(.{ i32, u32 }, .{u8});
var iterated_entities: usize = 0;
var iter = view.entityIterator();
while (iter.next()) |_| {
iterated_entities += 1;
}
try std.testing.expectEqual(iterated_entities, 1);
iterated_entities = 0;
reg.remove(u8, e2);
iter.reset();
while (iter.next()) |_| {
iterated_entities += 1;
}
try std.testing.expectEqual(iterated_entities, 2);
}

@ -0,0 +1,100 @@
const std = @import("std");
/// Processes are run by the Scheduler. They use a similar pattern to Allocators in that they are created and
/// added as fields in a parent struct, your actual process that will be run.
pub const Process = struct {
const State = enum(u8) { uninitialized, running, paused, succeeded, failed, aborted, finished };
updateFn: *const fn (self: *Process) void,
startFn: ?*const fn (self: *Process) void = null,
abortedFn: ?*const fn (self: *Process) void = null,
failedFn: ?*const fn (self: *Process) void = null,
succeededFn: ?*const fn (self: *Process) void = null,
deinit: *const fn (self: *Process, allocator: std.mem.Allocator) void = undefined,
state: State = .uninitialized,
stopped: bool = false,
next: ?*Process = null,
pub fn getParent(self: *Process, comptime T: type) *T {
return @fieldParentPtr(T, "process", self);
}
/// Terminates a process with success if it's still alive
pub fn succeed(self: *Process) void {
if (self.alive()) self.state = .succeeded;
}
/// Terminates a process with errors if it's still alive
pub fn fail(self: *Process) void {
if (self.alive()) self.state = .failed;
}
/// Stops a process if it's in a running state
pub fn pause(self: *Process) void {
if (self.state == .running) self.state = .paused;
}
/// Restarts a process if it's paused
pub fn unpause(self: *Process) void {
if (self.state == .paused) self.state = .running;
}
/// Aborts a process if it's still alive
pub fn abort(self: *Process, immediately: bool) void {
if (self.alive()) {
self.state = .aborted;
if (immediately) {
self.tick();
}
}
}
/// Returns true if a process is either running or paused
pub fn alive(self: Process) bool {
return self.state == .running or self.state == .paused;
}
/// Returns true if a process is already terminated
pub fn dead(self: Process) bool {
return self.state == .finished;
}
pub fn rejected(self: Process) bool {
return self.stopped;
}
/// Updates a process and its internal state
pub fn tick(self: *Process) void {
switch (self.state) {
.uninitialized => {
if (self.startFn) |func| func(self);
self.state = .running;
},
.running => {
self.updateFn(self);
},
else => {},
}
// if it's dead, it must be notified and removed immediately
switch (self.state) {
.succeeded => {
if (self.succeededFn) |func| func(self);
self.state = .finished;
},
.failed => {
if (self.failedFn) |func| func(self);
self.state = .finished;
self.stopped = true;
},
.aborted => {
if (self.abortedFn) |func| func(self);
self.state = .finished;
self.stopped = true;
},
else => {},
}
}
};

@ -0,0 +1,238 @@
const std = @import("std");
const Process = @import("process.zig").Process;
/// Cooperative scheduler for processes. Each process is invoked once per tick. If a process terminates, it's
/// removed automatically from the scheduler and it's never invoked again. A process can also have a child. In
/// this case, the process is replaced with its child when it terminates if it returns with success. In case of errors,
/// both the process and its child are discarded. In order to invoke all scheduled processes, call the `update` member function
/// Processes add themselves by calling `attach` and must satisfy the following conditions:
/// - have a field `process: Process`
/// - have a method `initialize(self: *@This(), data: var) void` that initializes all fields and takes in a the data passed to `attach`
/// - when initializing the `process` field it ust be given an `updateFn`. All other callbacks are optional.
/// - in any callback you can get your oiginal struct back via `process.getParent(@This())`
pub const Scheduler = struct {
processes: std.ArrayList(*Process),
allocator: std.mem.Allocator,
/// helper to create and prepare a process
fn createProcessHandler(comptime T: type, data: anytype, allocator: std.mem.Allocator) *Process {
var proc = allocator.create(T) catch unreachable;
proc.initialize(data);
// get a closure so that we can safely deinit this later
proc.process.deinit = struct {
fn deinit(process: *Process, alloc: std.mem.Allocator) void {
if (process.next) |next_process| {
next_process.deinit(next_process, alloc);
}
alloc.destroy(@fieldParentPtr(T, "process", process));
}
}.deinit;
return &proc.process;
}
/// returned when appending a process so that sub-processes can be added to the process
const Continuation = struct {
process: *Process,
allocator: std.mem.Allocator,
pub fn init(process: *Process, allocator: std.mem.Allocator) Continuation {
return .{ .process = process, .allocator = allocator };
}
pub fn next(self: *@This(), comptime T: type, data: anytype) *@This() {
self.process.next = createProcessHandler(T, data, self.allocator);
self.process = self.process.next.?;
return self;
}
};
pub fn init(allocator: std.mem.Allocator) Scheduler {
return .{
.processes = std.ArrayList(*Process).init(allocator),
.allocator = allocator,
};
}
pub fn deinit(self: *Scheduler) void {
self.clear();
self.processes.deinit();
}
/// Schedules a process for the next tick
pub fn attach(self: *Scheduler, comptime T: type, data: anytype) Continuation {
std.debug.assert(@hasDecl(T, "initialize"));
std.debug.assert(@hasField(T, "process"));
var process = createProcessHandler(T, data, self.allocator);
process.tick();
self.processes.append(process) catch unreachable;
return Continuation.init(process, self.allocator);
}
fn updateProcess(process: **Process, allocator: std.mem.Allocator) bool {
const current_process = process.*;
current_process.tick();
if (current_process.dead()) {
if (!current_process.rejected() and current_process.next != null) {
// grab the next process and null it out so we dont double-free it later
const next_process = current_process.next.?;
current_process.next = null;
process.* = next_process;
// kill the old Process parent
current_process.deinit(current_process, allocator);
return updateProcess(process, allocator);
} else {
return true;
}
}
return false;
}
/// Updates all scheduled processes
pub fn update(self: *Scheduler) void {
if (self.processes.items.len == 0) return;
var i: usize = self.processes.items.len - 1;
while (true) : (i -= 1) {
if (updateProcess(&self.processes.items[i], self.allocator)) {
var dead_process = self.processes.swapRemove(i);
dead_process.deinit(dead_process, self.allocator);
}
if (i == 0) break;
}
}
/// gets the number of processes still running
pub fn len(self: Scheduler) usize {
return self.processes.items.len;
}
/// resets the scheduler to its initial state and discards all the processes
pub fn clear(self: *Scheduler) void {
for (self.processes.items) |process| {
process.deinit(process, self.allocator);
}
self.processes.items.len = 0;
}
/// Aborts all scheduled processes. Unless an immediate operation is requested, the abort is scheduled for the next tick
pub fn abort(self: *Scheduler, immediately: bool) void {
for (self.processes.items) |handler| {
handler.process.abort(immediately);
}
}
};
test "scheduler.update" {
std.debug.print("\n", .{});
const Tester = struct {
process: Process,
fart: usize,
pub fn initialize(self: *@This(), data: anytype) void {
self.process = .{
.startFn = start,
.updateFn = update,
.abortedFn = aborted,
.failedFn = failed,
.succeededFn = succeeded,
};
self.fart = data;
}
fn start(process: *Process) void {
_ = process.getParent(@This());
// std.debug.print("start {}\n", .{self.fart});
}
fn aborted(process: *Process) void {
_ = process.getParent(@This());
// std.debug.print("aborted {}\n", .{self.fart});
}
fn failed(process: *Process) void {
_ = process.getParent(@This());
// std.debug.print("failed {}\n", .{self.fart});
}
fn succeeded(process: *Process) void {
_ = process.getParent(@This());
// std.debug.print("succeeded {}\n", .{self.fart});
}
fn update(process: *Process) void {
_ = process.getParent(@This());
// std.debug.print("update {}\n", .{self.fart});
process.succeed();
}
};
var scheduler = Scheduler.init(std.testing.allocator);
defer scheduler.deinit();
var continuation = scheduler.attach(Tester, 33);
_ = continuation.next(Tester, 66).next(Tester, 88).next(Tester, 99);
scheduler.update();
scheduler.update();
scheduler.update();
scheduler.update();
scheduler.update();
}
test "scheduler.clear" {
const Tester = struct {
process: Process,
pub fn initialize(self: *@This(), _: anytype) void {
self.process = .{ .updateFn = update };
}
fn update(_: *Process) void {
std.debug.assert(false);
}
};
var scheduler = Scheduler.init(std.testing.allocator);
defer scheduler.deinit();
var continuation = scheduler.attach(Tester, {});
_ = continuation.next(Tester, {});
scheduler.clear();
scheduler.update();
}
test "scheduler.attach.next" {
const Tester = struct {
process: Process,
counter: *usize,
pub fn initialize(self: *@This(), data: anytype) void {
self.process = .{ .updateFn = update };
self.counter = data;
}
fn update(process: *Process) void {
const self = process.getParent(@This());
self.counter.* += 1;
process.succeed();
}
};
var scheduler = Scheduler.init(std.testing.allocator);
defer scheduler.deinit();
var counter: usize = 0;
var continuation = scheduler.attach(Tester, &counter);
_ = continuation.next(Tester, &counter);
scheduler.update();
scheduler.update();
try std.testing.expectEqual(counter, 2);
}

@ -0,0 +1,99 @@
const std = @import("std");
const utils = @import("../ecs/utils.zig");
const Cache = @import("cache.zig").Cache;
pub const Assets = struct {
caches: std.AutoHashMap(u32, usize),
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) Assets {
return Assets{
.caches = std.AutoHashMap(u32, usize).init(allocator),
.allocator = allocator,
};
}
pub fn deinit(self: *Assets) void {
var iter = self.caches.iterator();
while (iter.next()) |ptr| {
// HACK: we dont know the Type here but we need to call deinit
@as(*Cache(u1), @ptrFromInt(ptr.value_ptr.*)).deinit();
}
self.caches.deinit();
}
pub fn get(self: *Assets, comptime AssetT: type) *Cache(AssetT) {
if (self.caches.get(utils.typeId(AssetT))) |tid| {
return @as(*Cache(AssetT), @ptrFromInt(tid));
}
var cache = Cache(AssetT).initPtr(self.allocator);
_ = self.caches.put(utils.typeId(AssetT), @intFromPtr(cache)) catch unreachable;
return cache;
}
pub fn load(self: *Assets, id: u16, comptime loader: anytype) ReturnType(loader, false) {
return self.get(ReturnType(loader, true)).load(id, loader);
}
fn ReturnType(comptime loader: anytype, comptime strip_ptr: bool) type {
var ret = @typeInfo(@typeInfo(@TypeOf(@field(loader, "load"))).Pointer.child).Fn.return_type.?;
if (strip_ptr) {
return std.meta.Child(ret);
}
return ret;
}
};
test "assets" {
const Thing = struct {
fart: i32,
pub fn deinit(self: *@This()) void {
std.testing.allocator.destroy(self);
}
};
const OtherThing = struct {
fart: i32,
pub fn deinit(self: *@This()) void {
std.testing.allocator.destroy(self);
}
};
const OtherThingLoadArgs = struct {
// Use actual field "load" as function pointer to avoid zig v0.10.0
// compiler error: "error: no field named 'load' in struct '...'"
load: *const fn (_: @This()) *OtherThing,
pub fn loadFn(_: @This()) *OtherThing {
return std.testing.allocator.create(OtherThing) catch unreachable;
}
};
const ThingLoadArgs = struct {
// Use actual field "load" as function pointer to avoid zig v0.10.0
// compiler error: "error: no field named 'load' in struct '...'"
load: *const fn (_: @This()) *Thing,
pub fn loadFn(_: @This()) *Thing {
return std.testing.allocator.create(Thing) catch unreachable;
}
};
var assets = Assets.init(std.testing.allocator);
defer assets.deinit();
_ = assets.get(Thing).load(6, ThingLoadArgs{ .load = ThingLoadArgs.loadFn });
try std.testing.expectEqual(assets.get(Thing).size(), 1);
_ = assets.load(4, ThingLoadArgs{ .load = ThingLoadArgs.loadFn });
try std.testing.expectEqual(assets.get(Thing).size(), 2);
_ = assets.get(OtherThing).load(6, OtherThingLoadArgs{ .load = OtherThingLoadArgs.loadFn });
try std.testing.expectEqual(assets.get(OtherThing).size(), 1);
_ = assets.load(8, OtherThingLoadArgs{ .load = OtherThingLoadArgs.loadFn });
try std.testing.expectEqual(assets.get(OtherThing).size(), 2);
assets.get(OtherThing).clear();
try std.testing.expectEqual(assets.get(OtherThing).size(), 0);
}

@ -0,0 +1,116 @@
const std = @import("std");
const ErasedPtr = @import("../ecs/utils.zig").ErasedPtr;
/// Simple cache for resources of a given type. If any resource has a deinit method it will be called when clear
/// or remove is called. Implementing a "loader" which is passed to "load" is a struct with one method:
/// - load(self: @This()) *T.
pub fn Cache(comptime T: type) type {
return struct {
const Self = @This();
safe_deinit: *const fn (*@This()) void,
resources: std.AutoHashMap(u32, *T),
allocator: ?std.mem.Allocator = null,
pub fn initPtr(allocator: std.mem.Allocator) *@This() {
var cache = allocator.create(@This()) catch unreachable;
cache.safe_deinit = struct {
fn deinit(self: *Self) void {
self.clear();
self.resources.deinit();
self.allocator.?.destroy(self);
}
}.deinit;
cache.resources = std.AutoHashMap(u32, *T).init(allocator);
cache.allocator = allocator;
return cache;
}
pub fn init(allocator: std.mem.Allocator) @This() {
return .{
.safe_deinit = struct {
fn deinit(self: *Self) void {
self.clear();
self.resources.deinit();
}
}.deinit,
.resources = std.AutoHashMap(u32, *T).init(allocator),
};
}
pub fn deinit(self: *@This()) void {
self.safe_deinit(self);
}
pub fn load(self: *@This(), id: u32, comptime loader: anytype) @typeInfo(@typeInfo(@TypeOf(@field(loader, "load"))).Pointer.child).Fn.return_type.? {
if (self.resources.get(id)) |resource| {
return resource;
}
var resource = loader.load(loader);
_ = self.resources.put(id, resource) catch unreachable;
return resource;
}
pub fn contains(self: *@This(), id: u32) bool {
return self.resources.contains(id);
}
pub fn remove(self: *@This(), id: u32) void {
if (self.resources.fetchRemove(id)) |kv| {
if (@hasDecl(T, "deinit")) {
@call(.always_inline, T.deinit, .{kv.value});
}
}
}
pub fn clear(self: *@This()) void {
// optionally deinit any resources that have a deinit method
if (@hasDecl(T, "deinit")) {
var iter = self.resources.iterator();
while (iter.next()) |kv| {
@call(.always_inline, T.deinit, .{kv.value_ptr.*});
}
}
self.resources.clearAndFree();
}
pub fn size(self: @This()) usize {
return self.resources.count();
}
};
}
test "cache" {
const utils = @import("../ecs/utils.zig");
const Thing = struct {
fart: i32,
pub fn deinit(self: *@This()) void {
std.testing.allocator.destroy(self);
}
};
const ThingLoadArgs = struct {
// Use actual field "load" as function pointer to avoid zig v0.10.0
// compiler error: "error: no field named 'load' in struct '...'"
load: *const fn (self: @This()) *Thing,
pub fn loadFn(self: @This()) *Thing {
_ = self;
return std.testing.allocator.create(Thing) catch unreachable;
}
};
var cache = Cache(Thing).init(std.testing.allocator);
defer cache.deinit();
_ = cache.load(utils.hashString("my/id"), ThingLoadArgs{ .load = ThingLoadArgs.loadFn });
_ = cache.load(utils.hashString("another/id"), ThingLoadArgs{ .load = ThingLoadArgs.loadFn });
try std.testing.expectEqual(cache.size(), 2);
cache.remove(utils.hashString("my/id"));
try std.testing.expectEqual(cache.size(), 1);
cache.clear();
try std.testing.expectEqual(cache.size(), 0);
}

@ -0,0 +1,88 @@
const std = @import("std");
/// wraps either a free function or a bound function that takes an Event as a parameter
pub fn Delegate(comptime Event: type) type {
return struct {
const Self = @This();
ctx_ptr_address: usize = 0,
callback: union(enum) {
free: *const fn (Event) void,
bound: *const fn (usize, Event) void,
},
/// sets a bound function as the Delegate callback
pub fn initBound(ctx: anytype, comptime fn_name: []const u8) Self {
std.debug.assert(@typeInfo(@TypeOf(ctx)) == .Pointer);
std.debug.assert(@intFromPtr(ctx) != 0);
const T = @TypeOf(ctx);
const BaseT = @typeInfo(T).Pointer.child;
return Self{
.ctx_ptr_address = @intFromPtr(ctx),
.callback = .{
.bound = struct {
fn cb(self: usize, param: Event) void {
@call(.always_inline, @field(BaseT, fn_name), .{ @as(T, @ptrFromInt(self)), param });
}
}.cb,
},
};
}
/// sets a free function as the Delegate callback
pub fn initFree(func: *const fn (Event) void) Self {
return Self{
.callback = .{ .free = func },
};
}
pub fn trigger(self: Self, param: Event) void {
switch (self.callback) {
.free => |func| @call(.auto, func, .{param}),
.bound => |func| @call(.auto, func, .{ self.ctx_ptr_address, param }),
}
}
pub fn containsFree(self: Self, callback: *const fn (Event) void) bool {
return switch (self.callback) {
.free => |func| func == callback,
else => false,
};
}
pub fn containsBound(self: Self, ctx: anytype) bool {
std.debug.assert(@intFromPtr(ctx) != 0);
std.debug.assert(@typeInfo(@TypeOf(ctx)) == .Pointer);
return switch (self.callback) {
.bound => @intFromPtr(ctx) == self.ctx_ptr_address,
else => false,
};
}
};
}
fn tester(param: u32) void {
std.testing.expectEqual(@as(u32, 666), param) catch unreachable;
}
const Thing = struct {
field: f32 = 0,
pub fn tester(_: *Thing, param: u32) void {
std.testing.expectEqual(@as(u32, 777), param) catch unreachable;
}
};
test "free Delegate" {
var d = Delegate(u32).initFree(tester);
d.trigger(666);
}
test "bound Delegate" {
var thing = Thing{};
var d = Delegate(u32).initBound(&thing, "tester");
d.trigger(777);
}

@ -0,0 +1,47 @@
const std = @import("std");
const Sink = @import("sink.zig").Sink;
const Signal = @import("signal.zig").Signal;
const utils = @import("../ecs/utils.zig");
pub const Dispatcher = struct {
signals: std.AutoHashMap(u32, usize),
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) Dispatcher {
return Dispatcher{
.signals = std.AutoHashMap(u32, usize).init(allocator),
.allocator = allocator,
};
}
pub fn deinit(self: *Dispatcher) void {
var iter = self.signals.iterator();
while (iter.next()) |ptr| {
// HACK: we dont know the Type here but we need to call deinit
var signal = @as(*Signal(void), @ptrFromInt(ptr.value_ptr.*));
signal.deinit();
}
self.signals.deinit();
}
fn assure(self: *Dispatcher, comptime T: type) *Signal(T) {
var type_id = utils.typeId(T);
if (self.signals.get(type_id)) |value| {
return @as(*Signal(T), @ptrFromInt(value));
}
var signal = Signal(T).create(self.allocator);
var signal_ptr = @intFromPtr(signal);
_ = self.signals.put(type_id, signal_ptr) catch unreachable;
return signal;
}
pub fn sink(self: *Dispatcher, comptime T: type) Sink(T) {
return self.assure(T).sink();
}
pub fn trigger(self: *Dispatcher, comptime T: type, value: T) void {
self.assure(T).publish(value);
}
};

@ -0,0 +1,105 @@
const std = @import("std");
const Sink = @import("sink.zig").Sink;
const Delegate = @import("delegate.zig").Delegate;
pub fn Signal(comptime Event: type) type {
return struct {
const Self = @This();
calls: std.ArrayList(Delegate(Event)),
allocator: ?std.mem.Allocator = null,
pub fn init(allocator: std.mem.Allocator) Self {
// we purposely do not store the allocator locally in this case so we know not to destroy ourself in deint!
return Self{
.calls = std.ArrayList(Delegate(Event)).init(allocator),
};
}
/// heap allocates a Signal
pub fn create(allocator: std.mem.Allocator) *Self {
var signal = allocator.create(Self) catch unreachable;
signal.calls = std.ArrayList(Delegate(Event)).init(allocator);
signal.allocator = allocator;
return signal;
}
pub fn deinit(self: *Self) void {
self.calls.deinit();
// optionally destroy ourself as well if we came from an allocator
if (self.allocator) |allocator| allocator.destroy(self);
}
pub fn size(self: Self) usize {
return self.calls.items.len;
}
pub fn empty(self: Self) bool {
return self.size == 0;
}
/// Disconnects all the listeners from a signal
pub fn clear(self: *Self) void {
self.calls.items.len = 0;
}
pub fn publish(self: Self, arg: Event) void {
for (self.calls.items) |call| {
call.trigger(arg);
}
}
/// Constructs a sink that is allowed to modify a given signal
pub fn sink(self: *Self) Sink(Event) {
return Sink(Event).init(self);
}
};
}
fn tester(param: u32) void {
std.testing.expectEqual(@as(u32, 666), param) catch unreachable;
}
const Thing = struct {
field: f32 = 0,
pub fn tester(_: *Thing, param: u32) void {
std.testing.expectEqual(@as(u32, 666), param) catch unreachable;
}
};
test "Signal/Sink" {
var signal = Signal(u32).init(std.testing.allocator);
defer signal.deinit();
var sink = signal.sink();
sink.connect(tester);
try std.testing.expectEqual(@as(usize, 1), signal.size());
// bound listener
var thing = Thing{};
sink.connectBound(&thing, "tester");
signal.publish(666);
sink.disconnect(tester);
signal.publish(666);
try std.testing.expectEqual(@as(usize, 1), signal.size());
sink.disconnectBound(&thing);
try std.testing.expectEqual(@as(usize, 0), signal.size());
}
test "Sink Before null" {
var signal = Signal(u32).init(std.testing.allocator);
defer signal.deinit();
var sink = signal.sink();
sink.connect(tester);
try std.testing.expectEqual(@as(usize, 1), signal.size());
var thing = Thing{};
sink.before(null).connectBound(&thing, "tester");
try std.testing.expectEqual(@as(usize, 2), signal.size());
}

@ -0,0 +1,115 @@
const std = @import("std");
const Signal = @import("signal.zig").Signal;
const Delegate = @import("delegate.zig").Delegate;
/// helper used to connect and disconnect listeners on the fly from a Signal. Listeners are wrapped in Delegates
/// and can be either free functions or functions bound to a struct.
pub fn Sink(comptime Event: type) type {
return struct {
const Self = @This();
insert_index: usize,
/// the Signal this Sink is temporarily wrapping
var owning_signal: *Signal(Event) = undefined;
pub fn init(signal: *Signal(Event)) Self {
owning_signal = signal;
return Self{ .insert_index = owning_signal.calls.items.len };
}
pub fn before(self: Self, callback: ?*const fn (Event) void) Self {
if (callback) |cb| {
if (self.indexOf(cb)) |index| {
return Self{ .insert_index = index };
}
}
return self;
}
pub fn beforeBound(self: Self, ctx: anytype) Self {
if (@typeInfo(@TypeOf(ctx)) == .Pointer) {
if (self.indexOfBound(ctx)) |index| {
return Self{ .insert_index = index };
}
}
return self;
}
pub fn connect(self: Self, callback: *const fn (Event) void) void {
std.debug.assert(self.indexOf(callback) == null);
_ = owning_signal.calls.insert(self.insert_index, Delegate(Event).initFree(callback)) catch unreachable;
}
pub fn connectBound(self: Self, ctx: anytype, comptime fn_name: []const u8) void {
std.debug.assert(self.indexOfBound(ctx) == null);
_ = owning_signal.calls.insert(self.insert_index, Delegate(Event).initBound(ctx, fn_name)) catch unreachable;
}
pub fn disconnect(self: Self, callback: *const fn (Event) void) void {
if (self.indexOf(callback)) |index| {
_ = owning_signal.calls.swapRemove(index);
}
}
pub fn disconnectBound(self: Self, ctx: anytype) void {
if (self.indexOfBound(ctx)) |index| {
_ = owning_signal.calls.swapRemove(index);
}
}
fn indexOf(_: Self, callback: *const fn (Event) void) ?usize {
for (owning_signal.calls.items, 0..) |call, i| {
if (call.containsFree(callback)) {
return i;
}
}
return null;
}
fn indexOfBound(_: Self, ctx: anytype) ?usize {
for (owning_signal.calls.items, 0..) |call, i| {
if (call.containsBound(ctx)) {
return i;
}
}
return null;
}
};
}
fn tester(param: u32) void {
std.testing.expectEqual(@as(u32, 666), param) catch unreachable;
}
const Thing = struct {
field: f32 = 0,
pub fn tester(_: *Thing, param: u32) void {
std.testing.expectEqual(@as(u32, 666), param) catch unreachable;
}
};
test "Sink Before free" {
var signal = Signal(u32).init(std.testing.allocator);
defer signal.deinit();
signal.sink().connect(tester);
try std.testing.expectEqual(signal.sink().indexOf(tester).?, 0);
var thing = Thing{};
signal.sink().before(tester).connectBound(&thing, "tester");
try std.testing.expectEqual(signal.sink().indexOfBound(&thing).?, 0);
}
test "Sink Before bound" {
var signal = Signal(u32).init(std.testing.allocator);
defer signal.deinit();
var thing = Thing{};
signal.sink().connectBound(&thing, "tester");
try std.testing.expectEqual(signal.sink().indexOfBound(&thing).?, 0);
signal.sink().beforeBound(&thing).connect(tester);
try std.testing.expectEqual(signal.sink().indexOf(tester).?, 0);
}

@ -0,0 +1,23 @@
// include all files with tests
comptime {
// ecs
_ = @import("ecs/actor.zig");
_ = @import("ecs/component_storage.zig");
_ = @import("ecs/entity.zig");
_ = @import("ecs/handles.zig");
_ = @import("ecs/sparse_set.zig");
_ = @import("ecs/views.zig");
_ = @import("ecs/groups.zig");
_ = @import("ecs/type_store.zig");
// signals
_ = @import("signals/delegate.zig");
_ = @import("signals/signal.zig");
// resources
_ = @import("resources/cache.zig");
_ = @import("resources/assets.zig");
// process
_ = @import("process/scheduler.zig");
}

@ -0,0 +1,39 @@
const std = @import("std");
const Dispatcher = @import("ecs").Dispatcher;
fn tester(param: u32) void {
std.testing.expectEqual(@as(u32, 666), param) catch unreachable;
}
fn tester2(param: i32) void {
std.testing.expectEqual(@as(i32, -543), param) catch unreachable;
}
const Thing = struct {
field: f32 = 0,
pub fn testU32(_: *Thing, param: u32) void {
std.testing.expectEqual(@as(u32, 666), param) catch unreachable;
}
pub fn testI32(_: *Thing, param: i32) void {
std.testing.expectEqual(@as(i32, -543), param) catch unreachable;
}
};
test "Dispatcher" {
var thing = Thing{};
var d = Dispatcher.init(std.testing.allocator);
defer d.deinit();
var sink = d.sink(u32);
sink.connect(tester);
sink.connectBound(&thing, "testU32");
d.trigger(u32, 666);
var sink2 = d.sink(i32);
sink2.connect(tester2);
sink2.connectBound(&thing, "testI32");
d.trigger(i32, -543);
}

@ -0,0 +1,242 @@
const std = @import("std");
const ecs = @import("ecs");
const Registry = @import("ecs").Registry;
const BasicGroup = @import("ecs").BasicGroup;
const OwningGroup = @import("ecs").OwningGroup;
const Velocity = struct { x: f32 = 0, y: f32 = 0 };
const Position = struct { x: f32 = 0, y: f32 = 0 };
const Empty = struct {};
const Sprite = struct { x: f32 = 0 };
const Transform = struct { x: f32 = 0 };
const Renderable = struct { x: f32 = 0 };
const Rotation = struct { x: f32 = 0 };
fn printStore(store: anytype, name: []const u8) void {
std.debug.print("--- {} ---\n", .{name});
for (store.set.dense.items, 0..) |e, i| {
std.debug.print("e[{}] s[{}]{}", .{ e, store.set.page(store.set.dense.items[i]), store.set.sparse.items[store.set.page(store.set.dense.items[i])] });
std.debug.print(" ({d:.2}) ", .{store.instances.items[i]});
}
std.debug.print("\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 = @as(f32, @floatFromInt(i)) });
reg.add(e, Renderable{ .x = @as(f32, @floatFromInt(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| {
try std.testing.expectEqual(val, group.getConst(Sprite, entity).x);
val += 1;
}
}
test "sort BasicGroup by Component" {
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 = @as(f32, @floatFromInt(i)) });
reg.add(e, Renderable{ .x = @as(f32, @floatFromInt(i)) });
}
const SortContext = struct {
fn sort(_: 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| {
try 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();
var group = reg.group(.{ Sprite, Renderable }, .{}, .{});
var i: usize = 0;
while (i < 5) : (i += 1) {
var e = reg.create();
reg.add(e, Sprite{ .x = @as(f32, @floatFromInt(i)) });
reg.add(e, Renderable{ .x = @as(f32, @floatFromInt(i)) });
}
const SortContext = struct {
group: OwningGroup,
fn sort(this: @This(), a: ecs.Entity, b: ecs.Entity) bool {
const sprite_a = this.group.getConst(Sprite, a);
const sprite_b = this.group.getConst(Sprite, b);
return sprite_a.x > sprite_b.x;
}
};
const context = SortContext{ .group = group };
group.sort(ecs.Entity, context, SortContext.sort);
var val: f32 = 0;
var iter = group.iterator(struct { s: *Sprite, r: *Renderable });
while (iter.next()) |entity| {
try std.testing.expectEqual(val, entity.s.*.x);
val += 1;
}
}
test "sort OwningGroup by Component" {
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 = @as(f32, @floatFromInt(i)) });
reg.add(e, Renderable{ .x = @as(f32, @floatFromInt(i)) });
}
const SortContext = struct {
fn sort(_: void, a: Sprite, b: Sprite) bool {
return a.x > b.x;
}
};
group.sort(Sprite, {}, SortContext.sort);
var val: f32 = 0;
var iter = group.iterator(struct { s: *Sprite, r: *Renderable });
while (iter.next()) |entity| {
try std.testing.expectEqual(val, entity.s.*.x);
val += 1;
}
}
test "sort OwningGroup by Component ensure unsorted non-matches" {
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 = @as(f32, @floatFromInt(i)) });
reg.add(e, Renderable{ .x = @as(f32, @floatFromInt(i)) });
var e2 = reg.create();
reg.add(e2, Sprite{ .x = @as(f32, @floatFromInt(i + 1 * 50)) });
}
try std.testing.expectEqual(group.len(), 5);
try std.testing.expectEqual(reg.len(Sprite), 10);
const SortContext = struct {
fn sort(_: void, a: Sprite, b: Sprite) bool {
// sprites with x > 50 shouldnt match in the group
std.testing.expect(a.x < 50 and b.x < 50) catch unreachable;
return a.x > b.x;
}
};
group.sort(Sprite, {}, SortContext.sort);
// all the
var view = reg.view(.{Sprite}, .{});
var count: usize = 0;
var iter = view.iterator();
while (iter.next()) |sprite| {
count += 1;
// all sprite.x > 50 should be at the end and we iterate backwards
if (count < 6) {
try std.testing.expect(sprite.x >= 50);
}
}
}
test "nested OwningGroups add/remove components" {
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var group1 = reg.group(.{Sprite}, .{Renderable}, .{});
var group2 = reg.group(.{ Sprite, Transform }, .{Renderable}, .{});
var group3 = reg.group(.{ Sprite, Transform }, .{ Renderable, Rotation }, .{});
try std.testing.expect(!reg.sortable(Sprite));
try std.testing.expect(!reg.sortable(Transform));
try std.testing.expect(reg.sortable(Renderable));
var e1 = reg.create();
reg.addTypes(e1, .{ Sprite, Renderable, Rotation });
try std.testing.expectEqual(group1.len(), 1);
try std.testing.expectEqual(group2.len(), 0);
try std.testing.expectEqual(group3.len(), 0);
reg.add(e1, Transform{});
try std.testing.expectEqual(group3.len(), 1);
reg.remove(Sprite, e1);
try std.testing.expectEqual(group1.len(), 0);
try std.testing.expectEqual(group2.len(), 0);
try std.testing.expectEqual(group3.len(), 0);
}
test "nested OwningGroups entity order" {
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var group1 = reg.group(.{Sprite}, .{Renderable}, .{});
var group2 = reg.group(.{ Sprite, Transform }, .{Renderable}, .{});
var i: usize = 0;
while (i < 5) : (i += 1) {
var e = reg.create();
reg.add(e, Sprite{ .x = @as(f32, @floatFromInt(i)) });
reg.add(e, Renderable{ .x = @as(f32, @floatFromInt(i)) });
}
try std.testing.expectEqual(group1.len(), 5);
try std.testing.expectEqual(group2.len(), 0);
_ = reg.assure(Sprite);
_ = reg.assure(Transform);
// printStore(sprite_store, "Sprite");
reg.add(1, Transform{ .x = 1 });
// printStore(sprite_store, "Sprite");
// printStore(transform_store, "Transform");
// std.debug.print("group2.current: {}\n", .{group2.group_data.current});
}

@ -0,0 +1,128 @@
const std = @import("std");
const ecs = @import("ecs");
const Registry = @import("ecs").Registry;
const Velocity = struct { x: f32, y: f32 };
const Position = struct { x: f32 = 0, y: f32 = 0 };
const Empty = struct {};
const BigOne = struct { pos: Position, vel: Velocity, accel: Velocity };
test "entity traits" {
_ = ecs.EntityTraitsType(.large).init();
}
test "Registry" {
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var e1 = reg.create();
reg.addTypes(e1, .{ Empty, Position });
reg.add(e1, BigOne{ .pos = Position{ .x = 5, .y = 5 }, .vel = Velocity{ .x = 5, .y = 5 }, .accel = Velocity{ .x = 5, .y = 5 } });
try std.testing.expect(reg.has(Empty, e1));
try std.testing.expect(reg.has(Position, e1));
try std.testing.expect(reg.has(BigOne, e1));
var iter = reg.entities();
while (iter.next()) |e| try std.testing.expectEqual(e1, e);
reg.remove(Empty, e1);
try std.testing.expect(!reg.has(Empty, e1));
}
test "context get/set/unset" {
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var ctx = reg.getContext(Position);
try std.testing.expectEqual(ctx, null);
var pos = Position{ .x = 5, .y = 5 };
reg.setContext(&pos);
ctx = reg.getContext(Position);
try std.testing.expectEqual(ctx.?, &pos);
reg.unsetContext(Position);
ctx = reg.getContext(Position);
try std.testing.expectEqual(ctx, null);
}
// this test should fail
test "context not pointer" {
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var pos = Position{ .x = 5, .y = 5 };
_ = pos;
// reg.setContext(pos);
}
test "context get/set/unset typed" {
const SomeType = struct { dummy: u1 };
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var ctx = reg.getContext(SomeType);
try std.testing.expectEqual(ctx, null);
var pos = SomeType{ .dummy = 0 };
reg.setContext(&pos);
ctx = reg.getContext(SomeType);
try std.testing.expectEqual(ctx.?, &pos);
reg.unsetContext(SomeType);
ctx = reg.getContext(SomeType);
try std.testing.expectEqual(ctx, null);
}
test "singletons" {
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var pos = Position{ .x = 5, .y = 5 };
reg.singletons().add(pos);
try std.testing.expect(reg.singletons().has(Position));
try std.testing.expectEqual(reg.singletons().get(Position).*, pos);
reg.singletons().remove(Position);
try std.testing.expect(!reg.singletons().has(Position));
}
test "destroy" {
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var i = @as(u8, 0);
while (i < 255) : (i += 1) {
const e = reg.create();
reg.add(e, Position{ .x = @as(f32, @floatFromInt(i)), .y = @as(f32, @floatFromInt(i)) });
}
reg.destroy(3);
reg.destroy(4);
i = 0;
while (i < 6) : (i += 1) {
if (i != 3 and i != 4)
try std.testing.expectEqual(Position{ .x = @as(f32, @floatFromInt(i)), .y = @as(f32, @floatFromInt(i)) }, reg.getConst(Position, i));
}
}
test "remove all" {
var reg = Registry.init(std.testing.allocator);
defer reg.deinit();
var e = reg.create();
reg.add(e, Position{ .x = 1, .y = 1 });
reg.addTyped(u32, e, 666);
try std.testing.expect(reg.has(Position, e));
try std.testing.expect(reg.has(u32, e));
reg.removeAll(e);
try std.testing.expect(!reg.has(Position, e));
try std.testing.expect(!reg.has(u32, e));
}

@ -0,0 +1,5 @@
test "ecs test suite" {
_ = @import("dispatcher_test.zig");
_ = @import("registry_test.zig");
_ = @import("groups_test.zig");
}

@ -0,0 +1,4 @@
id: 8cw92n7j19xzpjm8kwq0scvx3vx6k7b730vn3i6rl3t25z08
name: ecs
main: src/ecs.zig
dependencies:
Loading…
Cancel
Save