Compare commits
No commits in common. 'wip/new-system-init' and 'main' have entirely different histories.
wip/new-sy
...
main
7 changed files with 91 additions and 392 deletions
@ -1,324 +0,0 @@ |
|||||||
const std = @import("std"); |
|
||||||
const os_api = @import("./os_api.zig"); |
|
||||||
|
|
||||||
const c = @import("./c.zig"); |
|
||||||
const errors = @import("./errors.zig"); |
|
||||||
const meta = @import("./meta.zig"); |
|
||||||
const EntityError = @import("./entity.zig").EntityError; |
|
||||||
|
|
||||||
const flecszigble = @import("./main.zig"); |
|
||||||
const flecs = @import("./builtin/flecs.zig"); |
|
||||||
const DependsOn = flecs.core.DependsOn; |
|
||||||
const OnUpdate = flecs.pipeline.OnUpdate; |
|
||||||
|
|
||||||
pub fn System(comptime ctx: anytype) type { |
|
||||||
return struct { |
|
||||||
const Context = @import("./context.zig").Context(ctx); |
|
||||||
const World = Context.World; |
|
||||||
const Entity = Context.Entity; |
|
||||||
const Iter = Context.Iter; |
|
||||||
|
|
||||||
/// Registers a system with the `World`, that is associated with a |
|
||||||
/// specific query matching certain entities, calling a function for |
|
||||||
/// said matched entities. |
|
||||||
/// |
|
||||||
/// Returns the `Entity` of the newly created system. |
|
||||||
/// |
|
||||||
/// Example: |
|
||||||
/// ``` |
|
||||||
/// const PositionUpdate = struct { |
|
||||||
/// // The name of the system and resulting entity. |
|
||||||
/// // (Optional for pre-defined, named structs.) |
|
||||||
/// pub const name = "PositionUpdate"; |
|
||||||
/// // When to invoke the system. (Defaults to `OnUpdate`) |
|
||||||
/// pub const phase = flecs.pipeline.OnUpdate; |
|
||||||
/// // Query expression. (Required) |
|
||||||
/// pub const expr = "[inout] Position, [in] Velocity"; |
|
||||||
/// |
|
||||||
/// // Callback invoked for every matched archtable. (Required) |
|
||||||
/// pub fn callback(it: Iter) void { |
|
||||||
/// const pos_col = it.field(Position, 1); |
|
||||||
/// const vel_col = it.field(Velocity, 2); |
|
||||||
/// |
|
||||||
/// // We know the columns to be the same |
|
||||||
/// // size, we can iterate them together. |
|
||||||
/// for (pos_col, vel_col) |*pos, vel| { |
|
||||||
/// pos.x += vel.x; |
|
||||||
/// pos.y += vel.y; |
|
||||||
/// } |
|
||||||
/// } |
|
||||||
/// }; |
|
||||||
/// |
|
||||||
/// world.system(PositionUpdate) |
|
||||||
/// // or |
|
||||||
/// world.system(.{ |
|
||||||
/// .name = "PositionUpdate", |
|
||||||
/// .phase = flecs.pipeline.OnUpdate, |
|
||||||
/// .expr = "[inout] Position, [in] Velocity", |
|
||||||
/// .callback = PositionUpdate.callback, |
|
||||||
/// }) |
|
||||||
/// ``` |
|
||||||
pub fn init(world: *World, config: anytype) !Entity { |
|
||||||
// TODO: const isComponent = @typeInfo(T).Struct.fields.len > 0; |
|
||||||
|
|
||||||
const name: [:0]const u8 = if (@TypeOf(config) == type) meta.flecsTypeName(config) else @field(config, "name"); |
|
||||||
const expr: ?[:0]const u8 = fieldOr(config, "expr", null); |
|
||||||
|
|
||||||
// If `phase` is not specified, it defaults to `OnUpdate`. |
|
||||||
// If a literal `null` is specified, no phase is added to the system. |
|
||||||
const phase = fieldOr(config, "phase", OnUpdate); |
|
||||||
const entity = try world.entity( |
|
||||||
.{ .name = name }, |
|
||||||
if (@TypeOf(phase) == @TypeOf(null)) .{} else blk: { |
|
||||||
const phase_entity = Context.anyToEntity(phase); |
|
||||||
if (phase_entity == 0) return EntityError.IsNotAlive; |
|
||||||
break :blk .{ .{ DependsOn, phase_entity }, phase_entity }; |
|
||||||
}, |
|
||||||
); |
|
||||||
|
|
||||||
const isEach = !has(config, "callback") and has(config, "each"); |
|
||||||
const callback = @field(config, if (isEach) "each" else "callback"); |
|
||||||
const return_type = @typeInfo(@typeInfo(@TypeOf(callback)).Fn.return_type.?); |
|
||||||
const returns_error = switch (return_type) { |
|
||||||
.Void => false, |
|
||||||
.ErrorUnion => |e| if (e.payload == void) true else @compileError("Function return must be 'void' or '!void'"), |
|
||||||
else => @compileError("Function return must be 'void' or '!void'"), |
|
||||||
}; |
|
||||||
|
|
||||||
const SystemCallback = struct { |
|
||||||
// FIXME: Dependency loop. |
|
||||||
// Currently needs manual changing of the generated C code. |
|
||||||
// fn invoke(it: ?*c.ecs_iter_t) callconv(.C) void { |
|
||||||
fn invoke(any_it: *anyopaque) callconv(.C) void { |
|
||||||
const ecs_iter: ?*c.ecs_iter_t = @alignCast(@ptrCast(any_it)); |
|
||||||
const world_: *World = @alignCast(@ptrCast(ecs_iter.?.binding_ctx)); |
|
||||||
const it = Iter.fromRawPtr(world_, ecs_iter.?); |
|
||||||
|
|
||||||
const Args = std.meta.ArgsTuple(@TypeOf(callback)); |
|
||||||
var args: Args = undefined; |
|
||||||
|
|
||||||
comptime var field_index = 0; |
|
||||||
inline for (std.meta.fields(Args), 0..) |field, i| |
|
||||||
switch (field.type) { |
|
||||||
Iter => args[i] = it, |
|
||||||
*World => args[i] = it.world, |
|
||||||
f32 => args[i] = it.deltaTime(), |
|
||||||
Entity => if (comptime !isEach) |
|
||||||
@compileError("Entitiy variables only supported with 'each'"), |
|
||||||
else => { |
|
||||||
field_index += 1; |
|
||||||
switch (@typeInfo(field.type)) { |
|
||||||
.Struct => if (comptime !isEach) { |
|
||||||
args[i] = it.field(field.type, field_index)[0]; |
|
||||||
}, |
|
||||||
.Pointer => |p| if (comptime !isEach) { |
|
||||||
switch (comptime p.size) { |
|
||||||
.One => args[i] = &it.field(p.child, field_index)[0], |
|
||||||
.Slice => args[i] = it.field(p.child, field_index), |
|
||||||
else => {}, |
|
||||||
} |
|
||||||
} else if (comptime p.size == .One and p.is_const == true and @typeInfo(p.child) == .Pointer) { |
|
||||||
const pInner = @typeInfo(p.child).Pointer; |
|
||||||
if (pInner.size == .One) |
|
||||||
args[i] = &it.field(pInner.child, field_index)[0] |
|
||||||
else |
|
||||||
@compileError("Unsupported type '" ++ @typeName(field.type) ++ "'"); |
|
||||||
} else @compileError("Unsupported type '" ++ @typeName(field.type) ++ "'"), |
|
||||||
else => @compileError("Unsupported type '" ++ @typeName(field.type) ++ "'"), |
|
||||||
} |
|
||||||
}, |
|
||||||
}; |
|
||||||
|
|
||||||
// if (comptime std.mem.eql(u8, field.name, "this")) |
|
||||||
// it.entity() |
|
||||||
// else |
|
||||||
// @compileError("Entity variables other than 'this' not supported yet"), |
|
||||||
|
|
||||||
if (comptime isEach) { |
|
||||||
for (0..count) |i| { |
|
||||||
const err = @call(.auto, callback, args); |
|
||||||
if (comptime returns_error) err catch |e| handleError(it, e); |
|
||||||
} |
|
||||||
} else { |
|
||||||
const err = @call(.auto, callback, args); |
|
||||||
if (comptime returns_error) err catch |e| handleError(it, e); |
|
||||||
} |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
const result = c.ecs_system_init(world.raw, &.{ |
|
||||||
.entity = entity.raw, |
|
||||||
.binding_ctx = world, |
|
||||||
.callback = SystemCallback.invoke, |
|
||||||
.query = .{ .filter = .{ .expr = if (expr) |e| e else null } }, |
|
||||||
}); |
|
||||||
if (result == 0) return errors.getLastErrorOrUnknown(); |
|
||||||
|
|
||||||
if (comptime has(config, "interval")) |
|
||||||
_ = c.ecs_set_interval(world.raw, result, @field(config, "interval")); |
|
||||||
|
|
||||||
return Entity.fromRaw(world, result); |
|
||||||
} |
|
||||||
|
|
||||||
inline fn handleFields(args: anytype, it: Iter, comptime isEach: bool) void { |
|
||||||
comptime var field_index = 0; |
|
||||||
inline for (std.meta.fields(@TypeOf(args)), 0..) |field, i| |
|
||||||
switch (field.type) { |
|
||||||
Iter => if (comptime !isEach) { |
|
||||||
args[i] = it; |
|
||||||
}, |
|
||||||
*World => args[i] = it.world, |
|
||||||
f32 => args[i] = it.deltaTime(), |
|
||||||
Entity => if (comptime !isEach) |
|
||||||
@compileError("Entitiy variables only supported with 'each'"), |
|
||||||
else => { |
|
||||||
field_index += 1; |
|
||||||
switch (@typeInfo(field.type)) { |
|
||||||
.Struct => if (comptime !isEach) { |
|
||||||
args[i] = it.field(field.type, field_index)[0]; |
|
||||||
}, |
|
||||||
.Pointer => |p| if (comptime !isEach) { |
|
||||||
switch (comptime p.size) { |
|
||||||
.One => args[i] = &it.field(p.child, field_index)[0], |
|
||||||
.Slice => args[i] = it.field(p.child, field_index), |
|
||||||
else => {}, |
|
||||||
} |
|
||||||
} else if (comptime p.size == .One and p.is_const == true and @typeInfo(p.child) == .Pointer) { |
|
||||||
const pInner = @typeInfo(p.child).Pointer; |
|
||||||
if (pInner.size == .One) |
|
||||||
args[i] = &it.field(pInner.child, field_index)[0] |
|
||||||
else |
|
||||||
@compileError("Unsupported type '" ++ @typeName(field.type) ++ "'"); |
|
||||||
} else @compileError("Unsupported type '" ++ @typeName(field.type) ++ "'"), |
|
||||||
else => @compileError("Unsupported type '" ++ @typeName(field.type) ++ "'"), |
|
||||||
} |
|
||||||
}, |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
/// Called when a Zig error is returned from a system's callback function. |
|
||||||
fn handleError(it: Iter, err: anyerror) void { |
|
||||||
const system_name = if (it.system()) |s| s.name() orelse "<unknown>" else "<unknown>"; |
|
||||||
std.debug.print("Error in system '{s}': {}\n", .{ system_name, err }); |
|
||||||
} |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
fn has(container: anytype, comptime name: []const u8) bool { |
|
||||||
return comptime switch (@typeInfo(@TypeOf(container))) { |
|
||||||
.Type => @hasDecl(container, name), |
|
||||||
.Struct => @hasField(@TypeOf(container), name), |
|
||||||
else => @compileError("Expected type or struct"), |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
fn fieldOr( |
|
||||||
container: anytype, |
|
||||||
comptime name: []const u8, |
|
||||||
comptime default: anytype, |
|
||||||
) @TypeOf(if (comptime has(container, name)) @field(container, name) else default) { |
|
||||||
return if (comptime has(container, name)) @field(container, name) else default; |
|
||||||
} |
|
||||||
|
|
||||||
const expect = @import("./test/expect.zig"); |
|
||||||
|
|
||||||
test "System with struct type" { |
|
||||||
flecszigble.init(std.testing.allocator); |
|
||||||
var world = try flecszigble.World(void).init(); |
|
||||||
defer world.deinit(); |
|
||||||
|
|
||||||
const Position = struct { x: i32, y: i32 }; |
|
||||||
const Velocity = struct { x: i32, y: i32 }; |
|
||||||
|
|
||||||
_ = try world.component(Position); |
|
||||||
_ = try world.component(Velocity); |
|
||||||
|
|
||||||
const entity = try world.entity(.{}, .{ Position, Velocity }); |
|
||||||
entity.set(Position, .{ .x = 10, .y = 20 }); |
|
||||||
entity.set(Velocity, .{ .x = 1, .y = 2 }); |
|
||||||
|
|
||||||
const PositionUpdate = struct { |
|
||||||
pub const expr = "[inout] Position, [in] Velocity"; |
|
||||||
pub fn callback(pos_col: []Position, vel_col: []Velocity) void { |
|
||||||
for (pos_col, vel_col) |*pos, vel| { |
|
||||||
pos.x += vel.x; |
|
||||||
pos.y += vel.y; |
|
||||||
} |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
const system = try world.system(PositionUpdate); |
|
||||||
try expect.equal("PositionUpdate", system.name()); |
|
||||||
// TODO: Test more things. |
|
||||||
|
|
||||||
try expect.equal(.{ .x = 10, .y = 20 }, entity.get(Position)); |
|
||||||
_ = world.progress(0.0); |
|
||||||
try expect.equal(.{ .x = 11, .y = 22 }, entity.get(Position)); |
|
||||||
_ = world.progress(0.0); |
|
||||||
try expect.equal(.{ .x = 12, .y = 24 }, entity.get(Position)); |
|
||||||
} |
|
||||||
|
|
||||||
test "System with anonymous struct" { |
|
||||||
flecszigble.init(std.testing.allocator); |
|
||||||
var world = try flecszigble.World(void).init(); |
|
||||||
defer world.deinit(); |
|
||||||
|
|
||||||
const Position = struct { x: i32, y: i32 }; |
|
||||||
const Velocity = struct { x: i32, y: i32 }; |
|
||||||
|
|
||||||
_ = try world.component(Position); |
|
||||||
_ = try world.component(Velocity); |
|
||||||
|
|
||||||
const entity = try world.entity(.{}, .{ Position, Velocity }); |
|
||||||
entity.set(Position, .{ .x = 10, .y = 20 }); |
|
||||||
entity.set(Velocity, .{ .x = 1, .y = 2 }); |
|
||||||
|
|
||||||
const positionUpdateFn = struct { |
|
||||||
fn callback(pos_col: []Position, vel_col: []Velocity) void { |
|
||||||
for (pos_col, vel_col) |*pos, vel| { |
|
||||||
pos.x += vel.x; |
|
||||||
pos.y += vel.y; |
|
||||||
} |
|
||||||
} |
|
||||||
}.callback; |
|
||||||
|
|
||||||
const system = try world.system(.{ |
|
||||||
.name = "PositionUpdate", |
|
||||||
.expr = "[inout] Position, [in] Velocity", |
|
||||||
.callback = positionUpdateFn, |
|
||||||
}); |
|
||||||
try expect.equal("PositionUpdate", system.name()); |
|
||||||
// TODO: Test more things. |
|
||||||
|
|
||||||
try expect.equal(.{ .x = 10, .y = 20 }, entity.get(Position)); |
|
||||||
_ = world.progress(0.0); |
|
||||||
try expect.equal(.{ .x = 11, .y = 22 }, entity.get(Position)); |
|
||||||
_ = world.progress(0.0); |
|
||||||
try expect.equal(.{ .x = 12, .y = 24 }, entity.get(Position)); |
|
||||||
} |
|
||||||
|
|
||||||
test "System built-in parameters" { |
|
||||||
const World = flecszigble.World(void); |
|
||||||
const Iter = flecszigble.Iter(void); |
|
||||||
|
|
||||||
flecszigble.init(std.testing.allocator); |
|
||||||
var world = try World.init(); |
|
||||||
defer world.deinit(); |
|
||||||
|
|
||||||
const Test = struct { |
|
||||||
var times_called: usize = 0; |
|
||||||
var captured_world: ?*World = null; |
|
||||||
|
|
||||||
pub fn callback(it: Iter, world_: *World, delta: f32) !void { |
|
||||||
times_called += 1; |
|
||||||
captured_world = world_; |
|
||||||
try expect.equal(12.34, delta); |
|
||||||
try expect.equal(0, it.count()); |
|
||||||
} |
|
||||||
}; |
|
||||||
_ = try world.system(Test); |
|
||||||
|
|
||||||
_ = world.progress(12.34); |
|
||||||
try expect.equal(1, Test.times_called); |
|
||||||
try expect.equal(world, Test.captured_world); |
|
||||||
} |
|
Loading…
Reference in new issue