Base commit
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const Node = @import("Node.zig");
|
||||
|
||||
const Engine = @This();
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
nodes: std.ArrayList(Node) = .{},
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) !Engine {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Engine) void {
|
||||
for (self.nodes.items) |*n| n.deinit();
|
||||
self.nodes.deinit(self.allocator);
|
||||
}
|
||||
|
||||
pub fn addNode(self: *Engine, name: []const u8, script: []const u8) !void {
|
||||
const node = try Node.init(self.allocator, name, script);
|
||||
try self.nodes.append(self.allocator, node);
|
||||
}
|
||||
|
||||
/// Poll every node's file watcher; reload changed scripts.
|
||||
pub fn checkReloads(self: *Engine) !void {
|
||||
for (self.nodes.items) |*node| {
|
||||
if (node.watcher.poll()) try node.loadScript();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(self: *Engine, dt: f32) void {
|
||||
for (self.nodes.items) |*n| n.update(dt);
|
||||
}
|
||||
|
||||
pub fn render(self: *Engine) void {
|
||||
for (self.nodes.items) |*n| n.render();
|
||||
}
|
||||
|
||||
/// Editor overlay: simple node-list panel on the right edge.
|
||||
pub fn drawEditorUI(self: *Engine) void {
|
||||
const px: i32 = 1280 - 220;
|
||||
rl.drawRectangle(px, 0, 220, 720, .{ .r = 20, .g = 20, .b = 20, .a = 210 });
|
||||
rl.drawText("NODES", px + 10, 10, 12, rl.Color.gray);
|
||||
|
||||
for (self.nodes.items, 0..) |*node, i| {
|
||||
const y: i32 = 32 + @as(i32, @intCast(i)) * 44;
|
||||
rl.drawRectangle(px + 4, y, 212, 38, .{ .r = 45, .g = 45, .b = 45, .a = 255 });
|
||||
|
||||
const name_z = std.fmt.allocPrintSentinel(self.allocator, "{s}", .{node.name}, 0) catch continue;
|
||||
defer self.allocator.free(name_z);
|
||||
rl.drawText(name_z, px + 10, y + 4, 13, rl.Color.white);
|
||||
|
||||
const path_z = std.fmt.allocPrintSentinel(self.allocator, "{s}", .{node.script_path}, 0) catch continue;
|
||||
defer self.allocator.free(path_z);
|
||||
rl.drawText(path_z, px + 10, y + 22, 10, rl.Color.light_gray);
|
||||
}
|
||||
|
||||
rl.drawText("Edit .lua to hot reload", px + 6, 700, 9, rl.Color.dark_gray);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
const std = @import("std");
|
||||
|
||||
/// Polls a single file's mtime to detect saves.
|
||||
/// Cross-platform — works on Linux, macOS, Windows.
|
||||
const FileWatcher = @This();
|
||||
|
||||
path: []const u8,
|
||||
last_mtime: i128 = 0,
|
||||
|
||||
pub fn init(path: []const u8) FileWatcher {
|
||||
var w = FileWatcher{ .path = path };
|
||||
w.last_mtime = w.getMtime() catch 0;
|
||||
return w;
|
||||
}
|
||||
|
||||
/// Returns true (and updates internal mtime) if the file changed.
|
||||
pub fn poll(self: *FileWatcher) bool {
|
||||
const mtime = self.getMtime() catch return false;
|
||||
if (mtime != self.last_mtime) {
|
||||
self.last_mtime = mtime;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn getMtime(self: *const FileWatcher) !i128 {
|
||||
const stat = try std.fs.cwd().statFile(self.path);
|
||||
return stat.mtime;
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
const std = @import("std");
|
||||
const zlua = @import("zlua");
|
||||
const Lua = zlua.Lua;
|
||||
const lua_api = @import("lua_api.zig");
|
||||
const FileWatcher = @import("FileWatcher.zig");
|
||||
|
||||
/// A scene node with an attached Lua script.
|
||||
/// Each node owns a dedicated Lua state → scripts are fully isolated.
|
||||
const Node = @This();
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
name: []const u8,
|
||||
script_path: []const u8,
|
||||
lua: *Lua,
|
||||
watcher: FileWatcher,
|
||||
|
||||
// Node transform — synced to/from Lua as self_x, self_y globals
|
||||
x: f64 = 400,
|
||||
y: f64 = 300,
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
name: []const u8,
|
||||
script_path: []const u8,
|
||||
) !Node {
|
||||
const lua = try Lua.init(allocator);
|
||||
lua.openLibs();
|
||||
lua_api.registerAll(lua); // expose engine API to this state
|
||||
|
||||
var node = Node{
|
||||
.allocator = allocator,
|
||||
.name = name,
|
||||
.script_path = script_path,
|
||||
.lua = lua,
|
||||
.watcher = FileWatcher.init(script_path),
|
||||
};
|
||||
try node.loadScript();
|
||||
return node;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Node) void {
|
||||
self.lua.deinit();
|
||||
}
|
||||
|
||||
/// Load (or hot-reload) the Lua script from disk.
|
||||
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 });
|
||||
self.lua.pop(1);
|
||||
return; // keep running with old code rather than crashing
|
||||
};
|
||||
|
||||
self.syncToLua(); // give on_init() the current position
|
||||
self.callVoid("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;
|
||||
if (t != .function) {
|
||||
self.lua.pop(1);
|
||||
return;
|
||||
}
|
||||
self.lua.pushNumber(@floatCast(dt));
|
||||
self.lua.protectedCall(.{ .args = 1 }) catch |err| self.logErr("on_update", err);
|
||||
self.syncFromLua();
|
||||
}
|
||||
|
||||
/// Called every frame after update.
|
||||
pub fn render(self: *Node) void {
|
||||
self.syncToLua();
|
||||
self.callVoid("on_render");
|
||||
}
|
||||
|
||||
// ── internals ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Write node state into Lua globals so scripts can read/modify them.
|
||||
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");
|
||||
}
|
||||
|
||||
/// Read back any position changes the script made.
|
||||
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.getGlobal("self_y") catch {};
|
||||
self.y = self.lua.toNumber(-1) catch self.y;
|
||||
self.lua.pop(1);
|
||||
}
|
||||
|
||||
/// Call a zero-arg Lua function by name (no-op if it doesn't exist).
|
||||
fn callVoid(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);
|
||||
}
|
||||
|
||||
fn logErr(self: *Node, ctx: []const u8, err: anyerror) void {
|
||||
const msg = self.lua.toString(-1) catch "?";
|
||||
std.log.err("[{s}] {s}: {s} ({})", .{ self.script_path, ctx, msg, err });
|
||||
self.lua.pop(1);
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const zlua = @import("zlua");
|
||||
const Lua = zlua.Lua;
|
||||
|
||||
/// Register all engine API functions + constants into a Lua state.
|
||||
pub fn registerAll(lua: *Lua) void {
|
||||
// Drawing
|
||||
reg(lua, "draw_rect", drawRect);
|
||||
reg(lua, "draw_circle", drawCircle);
|
||||
reg(lua, "draw_text", drawText);
|
||||
reg(lua, "draw_line", drawLine);
|
||||
// Input
|
||||
reg(lua, "key_down", keyDown);
|
||||
reg(lua, "key_pressed", keyPressed);
|
||||
reg(lua, "mouse_down", mouseDown);
|
||||
reg(lua, "mouse_x", mouseX);
|
||||
reg(lua, "mouse_y", mouseY);
|
||||
// Util
|
||||
reg(lua, "log", luaLog);
|
||||
|
||||
// Key constants table: Key.LEFT, Key.W, Key.SPACE, ...
|
||||
lua.newTable();
|
||||
keyConst(lua, "LEFT", .left);
|
||||
keyConst(lua, "RIGHT", .right);
|
||||
keyConst(lua, "UP", .up);
|
||||
keyConst(lua, "DOWN", .down);
|
||||
keyConst(lua, "SPACE", .space);
|
||||
keyConst(lua, "W", .w);
|
||||
keyConst(lua, "A", .a);
|
||||
keyConst(lua, "S", .s);
|
||||
keyConst(lua, "D", .d);
|
||||
lua.setGlobal("Key");
|
||||
}
|
||||
|
||||
fn reg(lua: *Lua, name: [:0]const u8, comptime f: fn (*Lua) i32) void {
|
||||
lua.pushFunction(zlua.wrap(f));
|
||||
lua.setGlobal(name);
|
||||
}
|
||||
fn keyConst(lua: *Lua, name: [:0]const u8, key: rl.KeyboardKey) void {
|
||||
_ = lua.pushString(name);
|
||||
lua.pushInteger(@intFromEnum(key));
|
||||
lua.setTable(-3);
|
||||
}
|
||||
|
||||
// ── Drawing ────────────────────────────────────────────────────────────────
|
||||
// Helpers to read color args from Lua stack at a given base index
|
||||
fn argColor(lua: *Lua, base: i32) rl.Color {
|
||||
return .{
|
||||
.r = @intFromFloat(lua.toNumber(base) catch 255),
|
||||
.g = @intFromFloat(lua.toNumber(base + 1) catch 255),
|
||||
.b = @intFromFloat(lua.toNumber(base + 2) catch 255),
|
||||
.a = @intFromFloat(lua.toNumber(base + 3) catch 255),
|
||||
};
|
||||
}
|
||||
// draw_rect(x, y, w, h, r, g, b [,a])
|
||||
fn drawRect(lua: *Lua) i32 {
|
||||
rl.drawRectangle(
|
||||
@intFromFloat(lua.toNumber(1) catch 0),
|
||||
@intFromFloat(lua.toNumber(2) catch 0),
|
||||
@intFromFloat(lua.toNumber(3) catch 10),
|
||||
@intFromFloat(lua.toNumber(4) catch 10),
|
||||
argColor(lua, 5),
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
// draw_circle(x, y, radius, r, g, b [,a])
|
||||
fn drawCircle(lua: *Lua) i32 {
|
||||
rl.drawCircleV(.{
|
||||
.x = @floatCast(lua.toNumber(1) catch 0),
|
||||
.y = @floatCast(lua.toNumber(2) catch 0),
|
||||
}, @floatCast(lua.toNumber(3) catch 10), argColor(lua, 4));
|
||||
return 0;
|
||||
}
|
||||
// draw_text(text, x, y, size, r, g, b [,a])
|
||||
fn drawText(lua: *Lua) i32 {
|
||||
const text = lua.toString(1) catch return 0;
|
||||
rl.drawText(
|
||||
text,
|
||||
@intFromFloat(lua.toNumber(2) catch 0),
|
||||
@intFromFloat(lua.toNumber(3) catch 0),
|
||||
@intFromFloat(lua.toNumber(4) catch 20),
|
||||
argColor(lua, 5),
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
// draw_line(x1, y1, x2, y2, r, g, b [,a])
|
||||
fn drawLine(lua: *Lua) i32 {
|
||||
rl.drawLine(
|
||||
@intFromFloat(lua.toNumber(1) catch 0),
|
||||
@intFromFloat(lua.toNumber(2) catch 0),
|
||||
@intFromFloat(lua.toNumber(3) catch 0),
|
||||
@intFromFloat(lua.toNumber(4) catch 0),
|
||||
argColor(lua, 5),
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── Input ─────────────────────────────────────────────────────────────────
|
||||
// key_down(Key.X) → bool
|
||||
fn keyDown(lua: *Lua) i32 {
|
||||
const k = lua.toInteger(1) catch return 0;
|
||||
lua.pushBoolean(rl.isKeyDown(@enumFromInt(k)));
|
||||
return 1;
|
||||
}
|
||||
// key_pressed(Key.X) → bool (true only on the frame it was pressed)
|
||||
fn keyPressed(lua: *Lua) i32 {
|
||||
const k = lua.toInteger(1) catch return 0;
|
||||
lua.pushBoolean(rl.isKeyPressed(@enumFromInt(k)));
|
||||
return 1;
|
||||
}
|
||||
// mouse_down(0|1|2) → bool 0=left 1=right 2=middle
|
||||
fn mouseDown(lua: *Lua) i32 {
|
||||
const b = lua.toInteger(1) catch 0;
|
||||
lua.pushBoolean(rl.isMouseButtonDown(@enumFromInt(b)));
|
||||
return 1;
|
||||
}
|
||||
fn mouseX(lua: *Lua) i32 {
|
||||
lua.pushNumber(@floatFromInt(rl.getMouseX()));
|
||||
return 1;
|
||||
}
|
||||
fn mouseY(lua: *Lua) i32 {
|
||||
lua.pushNumber(@floatFromInt(rl.getMouseY()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ── Util ──────────────────────────────────────────────────────────────────
|
||||
fn luaLog(lua: *Lua) i32 {
|
||||
const msg = lua.toString(1) catch "nil";
|
||||
std.log.info("[lua] {s}", .{msg});
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const Engine = @import("Engine.zig");
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
rl.initWindow(1280, 720, "Zig + Raylib + Lua Engine");
|
||||
rl.setTargetFPS(60);
|
||||
defer rl.closeWindow();
|
||||
|
||||
var engine = try Engine.init(allocator);
|
||||
defer engine.deinit();
|
||||
|
||||
// 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");
|
||||
|
||||
while (!rl.windowShouldClose()) {
|
||||
const dt = rl.getFrameTime();
|
||||
|
||||
// Poll file mtimes — reloads any script that changed on disk
|
||||
try engine.checkReloads();
|
||||
|
||||
engine.update(dt); // calls on_update(dt) in each script
|
||||
|
||||
rl.beginDrawing();
|
||||
defer rl.endDrawing();
|
||||
rl.clearBackground(rl.Color.black);
|
||||
|
||||
engine.render(); // calls on_render() in each script
|
||||
engine.drawEditorUI(); // node-list panel (right side)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user