Basic multi node type
This commit is contained in:
+19
-1
@@ -19,10 +19,28 @@ pub fn deinit(self: *Engine) void {
|
||||
}
|
||||
|
||||
pub fn addNode(self: *Engine, name: []const u8, script: []const u8) !void {
|
||||
const node = try Node.init(self.allocator, name, script);
|
||||
const node = try Node.init(self.allocator, name, .base, script);
|
||||
try self.nodes.append(self.allocator, node);
|
||||
}
|
||||
|
||||
/// A rectangle node. Engine auto-draws the rect from Zig properties.
|
||||
/// Script receives a RectNode self and can change width/height/color.
|
||||
pub fn addRectNode(
|
||||
self: *Engine,
|
||||
name: []const u8,
|
||||
width: f32,
|
||||
height: f32,
|
||||
color: rl.Color,
|
||||
script: []const u8,
|
||||
) !void {
|
||||
const kind: Node.NodeKind = .{ .rectangle = .{
|
||||
.width = width,
|
||||
.height = height,
|
||||
.color = color,
|
||||
} };
|
||||
try self.nodes.append(self.allocator, try Node.init(self.allocator, name, kind, script));
|
||||
}
|
||||
|
||||
/// Poll every node's file watcher; reload changed scripts.
|
||||
pub fn checkReloads(self: *Engine) !void {
|
||||
for (self.nodes.items) |*node| {
|
||||
|
||||
+137
-30
@@ -1,9 +1,22 @@
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const zlua = @import("zlua");
|
||||
const Lua = zlua.Lua;
|
||||
const lua_api = @import("lua_api.zig");
|
||||
const FileWatcher = @import("FileWatcher.zig");
|
||||
|
||||
pub const RectData = struct {
|
||||
width: f32 = 50,
|
||||
height: f32 = 50,
|
||||
color: rl.Color = rl.Color.white,
|
||||
};
|
||||
|
||||
pub const NodeKind = union(enum) {
|
||||
base: void,
|
||||
rectangle: RectData,
|
||||
// extend here: sprite, audio_emitter, tilemap, ...
|
||||
};
|
||||
|
||||
/// A scene node with an attached Lua script.
|
||||
/// Each node owns a dedicated Lua state → scripts are fully isolated.
|
||||
const Node = @This();
|
||||
@@ -13,27 +26,33 @@ name: []const u8,
|
||||
script_path: []const u8,
|
||||
lua: *Lua,
|
||||
watcher: FileWatcher,
|
||||
/// Lua registry key for the persistent self table.
|
||||
/// Survives hot-reloads so script-side state is preserved.
|
||||
self_ref: i32 = 0,
|
||||
|
||||
// Node transform — synced to/from Lua as self_x, self_y globals
|
||||
x: f64 = 400,
|
||||
y: f64 = 300,
|
||||
x: f32 = 400,
|
||||
y: f32 = 300,
|
||||
kind: NodeKind = .base,
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
name: []const u8,
|
||||
kind: NodeKind,
|
||||
script_path: []const u8,
|
||||
) !Node {
|
||||
const lua = try Lua.init(allocator);
|
||||
lua.openLibs();
|
||||
lua_api.registerAll(lua); // expose engine API to this state
|
||||
lua_api.registerAll(lua);
|
||||
|
||||
var node = Node{
|
||||
.allocator = allocator,
|
||||
.name = name,
|
||||
.kind = kind,
|
||||
.script_path = script_path,
|
||||
.lua = lua,
|
||||
.watcher = FileWatcher.init(script_path),
|
||||
};
|
||||
try node.createSelfTable();
|
||||
try node.loadScript();
|
||||
return node;
|
||||
}
|
||||
@@ -42,26 +61,25 @@ pub fn deinit(self: *Node) void {
|
||||
self.lua.deinit();
|
||||
}
|
||||
|
||||
/// Load (or hot-reload) the Lua script from disk.
|
||||
/// Load or hot-reload the Lua script from disk.
|
||||
/// The self table (and any data stored in it) survives the reload.
|
||||
pub fn loadScript(self: *Node) !void {
|
||||
std.log.info("(re)loading {s}", .{self.script_path});
|
||||
|
||||
const path_z = try self.allocator.dupeZ(u8, self.script_path);
|
||||
defer self.allocator.free(path_z);
|
||||
|
||||
self.lua.doFile(path_z) catch |err| {
|
||||
const msg = self.lua.toString(-1) catch "?";
|
||||
std.log.err("[{s}] load error: {s} ({})", .{ self.script_path, msg, err });
|
||||
std.log.err("[{s}] {s} ({})", .{ self.script_path, msg, err });
|
||||
self.lua.pop(1);
|
||||
return; // keep running with old code rather than crashing
|
||||
return; // keep old functions running — don't crash on syntax error
|
||||
};
|
||||
|
||||
self.syncToLua(); // give on_init() the current position
|
||||
self.callVoid("on_init");
|
||||
self.syncToLua();
|
||||
self.callWithSelf("on_init");
|
||||
self.syncFromLua();
|
||||
}
|
||||
|
||||
/// Called every frame. Passes dt as an argument.
|
||||
pub fn update(self: *Node, dt: f32) void {
|
||||
self.syncToLua();
|
||||
const t = self.lua.getGlobal("on_update") catch return;
|
||||
@@ -69,46 +87,99 @@ pub fn update(self: *Node, dt: f32) void {
|
||||
self.lua.pop(1);
|
||||
return;
|
||||
}
|
||||
_ = self.lua.rawGetIndex(zlua.registry_index, self.self_ref);
|
||||
self.lua.pushNumber(@floatCast(dt));
|
||||
self.lua.protectedCall(.{ .args = 1 }) catch |err| self.logErr("on_update", err);
|
||||
self.lua.protectedCall(.{ .args = 2 }) catch |err| self.logErr("on_update", err);
|
||||
self.syncFromLua();
|
||||
}
|
||||
|
||||
/// Called every frame after update.
|
||||
pub fn render(self: *Node) void {
|
||||
// 1. Automatic draw from Zig-side properties
|
||||
switch (self.kind) {
|
||||
.rectangle => |r| {
|
||||
rl.drawRectangle(
|
||||
@intFromFloat(self.x),
|
||||
@intFromFloat(self.y),
|
||||
@intFromFloat(r.width),
|
||||
@intFromFloat(r.height),
|
||||
r.color,
|
||||
);
|
||||
},
|
||||
.base => {},
|
||||
}
|
||||
// 2. Script overlay (labels, effects, custom shapes, etc.)
|
||||
self.syncToLua();
|
||||
self.callVoid("on_render");
|
||||
self.callWithSelf("on_render");
|
||||
}
|
||||
|
||||
// ── internals ──────────────────────────────────────────────────────────
|
||||
// ── Self table ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Write node state into Lua globals so scripts can read/modify them.
|
||||
fn createSelfTable(self: *Node) !void {
|
||||
self.lua.newTable();
|
||||
// luaL_ref pops the table and returns an integer key in the registry.
|
||||
// Note: ziglua may return !i32 or !i64 depending on version.
|
||||
self.self_ref = try self.lua.ref(zlua.registry_index);
|
||||
}
|
||||
|
||||
/// Write Zig state → Lua self table before any callback.
|
||||
fn syncToLua(self: *Node) void {
|
||||
self.lua.pushNumber(self.x);
|
||||
self.lua.setGlobal("self_x");
|
||||
self.lua.pushNumber(self.y);
|
||||
self.lua.setGlobal("self_y");
|
||||
_ = self.lua.rawGetIndex(zlua.registry_index, self.self_ref);
|
||||
// self table is now at stack -1
|
||||
|
||||
setNum(self.lua, "x", self.x);
|
||||
setNum(self.lua, "y", self.y);
|
||||
|
||||
switch (self.kind) {
|
||||
.rectangle => |r| {
|
||||
setNum(self.lua, "width", r.width);
|
||||
setNum(self.lua, "height", r.height);
|
||||
pushColorTable(self.lua, r.color); // pushes {r,g,b,a} at -1
|
||||
self.lua.setField(-2, "color"); // self_tbl.color = color_tbl; pops color_tbl
|
||||
},
|
||||
.base => {},
|
||||
}
|
||||
|
||||
self.lua.pop(1); // pop self table
|
||||
}
|
||||
|
||||
/// Read back any position changes the script made.
|
||||
/// Read back Lua self table → Zig state after any callback.
|
||||
fn syncFromLua(self: *Node) void {
|
||||
_ = self.lua.getGlobal("self_x") catch {};
|
||||
self.x = self.lua.toNumber(-1) catch self.x;
|
||||
self.lua.pop(1);
|
||||
_ = self.lua.rawGetIndex(zlua.registry_index, self.self_ref);
|
||||
// self table at -1
|
||||
|
||||
_ = self.lua.getGlobal("self_y") catch {};
|
||||
self.y = self.lua.toNumber(-1) catch self.y;
|
||||
self.lua.pop(1);
|
||||
self.x = getNum(self.lua, "x", self.x);
|
||||
self.y = getNum(self.lua, "y", self.y);
|
||||
|
||||
switch (self.kind) {
|
||||
.rectangle => |*r| {
|
||||
r.width = getNum(self.lua, "width", r.width);
|
||||
r.height = getNum(self.lua, "height", r.height);
|
||||
_ = self.lua.getField(-1, "color"); // stack: [..., self_tbl, color_tbl]
|
||||
if (self.lua.typeOf(-1) == .table) {
|
||||
r.color.r = getU8(self.lua, "r", r.color.r);
|
||||
r.color.g = getU8(self.lua, "g", r.color.g);
|
||||
r.color.b = getU8(self.lua, "b", r.color.b);
|
||||
r.color.a = getU8(self.lua, "a", r.color.a);
|
||||
}
|
||||
self.lua.pop(1); // pop color_tbl (or nil)
|
||||
},
|
||||
.base => {},
|
||||
}
|
||||
|
||||
self.lua.pop(1); // pop self table
|
||||
}
|
||||
|
||||
/// Call a zero-arg Lua function by name (no-op if it doesn't exist).
|
||||
fn callVoid(self: *Node, name: [:0]const u8) void {
|
||||
// ── Callback helpers ──────────────────────────────────────────────────────
|
||||
|
||||
/// Call lua_fn_name(self) — no-op if the function is not defined.
|
||||
fn callWithSelf(self: *Node, name: [:0]const u8) void {
|
||||
const t = self.lua.getGlobal(name) catch return;
|
||||
if (t != .function) {
|
||||
self.lua.pop(1);
|
||||
return;
|
||||
}
|
||||
self.lua.protectedCall(.{}) catch |err| self.logErr(name, err);
|
||||
_ = self.lua.rawGetIndex(zlua.registry_index, self.self_ref);
|
||||
self.lua.protectedCall(.{ .args = 1 }) catch |err| self.logErr(name, err);
|
||||
}
|
||||
|
||||
fn logErr(self: *Node, ctx: []const u8, err: anyerror) void {
|
||||
@@ -116,3 +187,39 @@ fn logErr(self: *Node, ctx: []const u8, err: anyerror) void {
|
||||
std.log.err("[{s}] {s}: {s} ({})", .{ self.script_path, ctx, msg, err });
|
||||
self.lua.pop(1);
|
||||
}
|
||||
|
||||
/// Set a number field on the table currently at stack -1.
|
||||
fn setNum(lua: *Lua, key: [:0]const u8, val: f32) void {
|
||||
lua.pushNumber(@floatCast(val));
|
||||
lua.setField(-2, key); // pops num, sets table[-2][key] = num
|
||||
}
|
||||
|
||||
/// Get a number field from the table currently at stack -1.
|
||||
fn getNum(lua: *Lua, key: [:0]const u8, default: f32) f32 {
|
||||
_ = lua.getField(-1, key);
|
||||
const v = lua.toNumber(-1) catch @as(f64, @floatCast(default));
|
||||
lua.pop(1);
|
||||
return @floatCast(v);
|
||||
}
|
||||
|
||||
/// Get a u8 color component (0-255) from the table currently at stack -1.
|
||||
fn getU8(lua: *Lua, key: [:0]const u8, default: u8) u8 {
|
||||
_ = lua.getField(-1, key);
|
||||
const v = lua.toNumber(-1) catch @as(f64, @floatFromInt(default));
|
||||
lua.pop(1);
|
||||
return @intFromFloat(std.math.clamp(v, 0, 255));
|
||||
}
|
||||
|
||||
/// Push a new {r, g, b, a} table onto the stack.
|
||||
fn pushColorTable(lua: *Lua, c: rl.Color) void {
|
||||
lua.newTable();
|
||||
lua.pushNumber(@floatFromInt(c.r));
|
||||
lua.setField(-2, "r");
|
||||
lua.pushNumber(@floatFromInt(c.g));
|
||||
lua.setField(-2, "g");
|
||||
lua.pushNumber(@floatFromInt(c.b));
|
||||
lua.setField(-2, "b");
|
||||
lua.pushNumber(@floatFromInt(c.a));
|
||||
lua.setField(-2, "a");
|
||||
// color table is now at stack top — caller sets it as a field
|
||||
}
|
||||
|
||||
+14
-1
@@ -16,7 +16,20 @@ pub fn main() !void {
|
||||
|
||||
// Attach Lua scripts to nodes — add as many as you like
|
||||
try engine.addNode("Player", "scripts/player.lua");
|
||||
try engine.addNode("Box", "scripts/box.lua");
|
||||
try engine.addRectNode(
|
||||
"HealthBar",
|
||||
200,
|
||||
20,
|
||||
.{ .r = 60, .g = 200, .b = 80, .a = 255 },
|
||||
"scripts/healthbar.lua",
|
||||
);
|
||||
try engine.addRectNode(
|
||||
"Enemy",
|
||||
55,
|
||||
55,
|
||||
.{ .r = 220, .g = 60, .b = 60, .a = 255 },
|
||||
"scripts/enemy_rect.lua",
|
||||
);
|
||||
|
||||
while (!rl.windowShouldClose()) {
|
||||
const dt = rl.getFrameTime();
|
||||
|
||||
Reference in New Issue
Block a user