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