Base commit

This commit is contained in:
adrien
2026-04-08 22:57:33 +02:00
commit 0c04307db8
10 changed files with 527 additions and 0 deletions
+61
View File
@@ -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);
}
+29
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+36
View File
@@ -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)
}
}