parent
5e1ad2b90a
commit
1dda16fa01
7 changed files with 392 additions and 91 deletions
@ -0,0 +1,324 @@ |
|||||||
|
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