njin
Engine 2D gọn nhẹ để tự tay làm game theo ý mình. Vibe coding cũng ổn luôn
Loading...
Searching...
No Matches
Script Lua

Trang này cho game viết luật chơi và hành vi của entity bằng Lua 5.4 thay vì C++: sửa file .lua, lưu lại, và game đang chạy đổi theo ngay, không phải biên dịch lại. Phần C++ vẫn dựng thế giới, nạp tài nguyên và giữ những gì cần nhanh; script gọi vào đó qua module njin. Mọi thứ khai báo trong njin_script.h; Lua và sol2 nằm trong engine, game không include header của chúng.

Cần biết trước: Entity, component và event (entity, component, system) và Bắt đầu. Phần 2D dùng các khối của Làm game platformer; phần 3D dùng Đồ họa 3D.

Game không dùng script thì không tốn gì: máy Lua chỉ được tạo ở lần gọi script_* đầu tiên.

Chạy script và gọi hàm

script_run_file() chạy một file .lua (đường dẫn như mọi tài nguyên); hàm và biến toàn cục nó tạo ra dùng được từ C++. script_call() gọi một hàm theo tên (có dấu chấm thì tìm trong bảng), script_set_global() và script_get_global() đọc ghi biến.

njin::script_run_file(ctx, "scripts/rules.lua");
const njin::script_result r = njin::script_call(ctx, "rules.coin_value", {3.0});
if (r.ok)
NJIN_INFO("một xu đáng %g điểm", std::get<double>(r.value));
njin::script_set_global(ctx, "config.difficulty", 2.0);
#define NJIN_INFO(...)
Ghi log mức info. Tham số giống printf.
Definition njin_log.h:98
bool script_run_file(context &ctx, const char *path)
Chạy một file Lua (đường dẫn như mọi tài nguyên: cạnh file chạy hay thư mục làm việc).
void script_set_global(context &ctx, const char *name, const script_value &value)
Đặt một biến toàn cục bên Lua (tên có dấu chấm đặt trong bảng).
script_result script_call(context &ctx, const char *function, std::initializer_list< script_value > args={})
Gọi một hàm Lua toàn cục theo tên.
Kết quả của script_call().
Definition njin_script.h:56
script_value value
Giá trị trả về đầu tiên (nil nếu không trả gì).
Definition njin_script.h:58
bool ok
Hàm có và chạy không lỗi.
Definition njin_script.h:57

Giá trị đi qua lại là njin::script_value: nil, bool, số (f64), chuỗi, vec2, vec3 hay entity. Bên Lua, entity là một số nguyên.

Gọi C++ từ Lua

script_register() đặt một hàm C++ dưới một tên bên Lua. Hàm có thể nhận và trả bool, số, chuỗi, vec2, vec3, entity, và nhận context & ở đầu; tham số Lua thiếu hay sai kiểu thành giá trị mặc định.

njin::script_register(ctx, "add_score", [](int points) { score += points; });
njin::script_register(ctx, "game.player", [&] { return player; });
void script_register(context &ctx, const char *name, script_fn fn)
Đăng ký một hàm C++ dưới tên name (toàn cục bên Lua; có dấu chấm thì đặt trong bảng,...
add_score(10)
local p = njin.position(game.player())

Lỗi C++ ném ra trong hàm đó thành lỗi Lua, có file và dòng của chỗ gọi.

Script gắn trên entity

script_attach() gắn một file vào một entity. File trả về một bảng, như một lớp; mỗi entity có một bảng self riêng, nhận làm tham số đầu của mọi hàm:

Hàm Khi nào
on_start(self) Một lần, ở lần cập nhật đầu tiên sau khi gắn
on_fixed_update(self, dt) Theo nhịp cố định, trong phase_fixed_update (dt là bước cố định), ngay trước bước vật lý 3D
on_update(self, dt) Mỗi frame, trong phase_update, trước system của game
on_render(self) Mỗi frame, trong phase_render, để vẽ bằng njin.draw_* (cả 3D, giữa njin.begin_3d và njin.end_3d)
on_ui(self) Mỗi frame, trong phase_post_render (không gian màn hình), để dựng giao diện bằng njin.ui_*
on_destroy(self) Khi entity bị hủy hay script bị gỡ (script_detach())
on_reload(self) Sau khi file được nạp lại (hot reload)
on_load(self) Sau khi script_load_state() đổ dữ liệu đã lưu vào self (Lưu và nạp game)

self.entity là entity. Dữ liệu game ghi vào self (máu, số xu, trạng thái) ở lại với entity; C++ đọc ghi nó bằng script_field() và script_set_field(). Nhiều entity dùng chung một file thì file chỉ nạp một lần.

Ví dụ một nhân vật platformer: script đọc phím, đưa vào njin::platformer_body, rồi lật sprite và đổi clip theo trạng thái mà bộ điều khiển trả về.

-- Nhân vật platformer điều khiển bằng script: chạy, nhảy, lật sprite, đổi clip.
-- Entity đã có transform, collider, platformer_body, sprite và animator (do C++ tạo).
local Player = {}
function Player:on_start()
self.coins = 0
self.jump_sound = njin.sound_load("assets/jump.wav")
end
function Player:on_update(dt)
local move = 0
if njin.key_held("left") then move = move - 1 end
if njin.key_held("right") then move = move + 1 end
local jump = njin.key_pressed("space")
njin.platformer_input(self.entity, move, jump, njin.key_held("space"))
local body = njin.platformer(self.entity)
if move ~= 0 then njin.sprite_flip(self.entity, move < 0) end
if not body.grounded then
njin.anim_play(self.entity, "jump")
elseif move ~= 0 then
njin.anim_play(self.entity, "run")
else
njin.anim_play(self.entity, "idle")
end
if body.jumped then njin.sound_play(self.jump_sound) end
end
function Player:on_render()
njin.draw_text("xu: " .. self.coins, njin.vec2(8, 8), 16, 1, 1, 1)
end
return Player

Phía C++ dựng entity, đăng ký hàm cho script và gắn file:

#include <njin.h>
namespace {
using namespace njin;
i32 score = 0;
entt::entity player = entt::null;
void load(context &ctx) {
// Hàm C++ gọi được từ Lua: kiểu tham số và kiểu trả về tự đổi.
script_register(ctx, "add_score", [](i32 points) { score += points; });
script_register(ctx, "game.score", [] { return score; });
// Hàm và biến toàn cục: nạp một file, rồi gọi hàm của nó.
script_run_file(ctx, "scripts/rules.lua");
const script_result r = script_call(ctx, "rules.coin_value", {3.0});
if (r.ok)
NJIN_INFO("một xu đáng %g điểm", std::get<f64>(r.value));
// Script gắn trên entity: on_start, on_update(dt), on_render mỗi frame.
entt::registry &w = world(ctx);
player = w.create();
w.emplace<transform>(player, transform{.pos = {32, 64}});
w.emplace<collider>(player, collider{.size = {12, 14}});
w.emplace<platformer_body>(player);
script_attach(ctx, player, "scripts/player.lua");
#ifndef NDEBUG
hot_reload_enable(ctx, true); // sửa file .lua, lưu, game đổi theo ngay
#endif
}
void update(context &ctx) {
// Đọc trạng thái mà script giữ trong `self`.
const script_value coins = script_field(ctx, player, "coins");
if (const f64 *n = std::get_if<f64>(&coins); n != nullptr && *n >= 100)
NJIN_INFO("đủ 100 xu");
}
void setup(context &ctx) {
ecs_register(ctx, phase_startup, load, "load");
ecs_register(ctx, phase_update, update, "update");
}
} // namespace
mod_desc scripted_game_module() { return {.name = "scripted_game", .setup = setup}; }
void ecs_register(context &ctx, sys_phase phase, sys_fnc fnc, const char *name=nullptr)
Thêm một system vào lịch chạy của một phase.
entt::registry & world(context &ctx)
Trả về registry của EnTT chứa mọi entity và component của game.
@ phase_update
Mỗi frame, logic chính của game.
Definition _mod.h:35
@ phase_startup
Một lần, trước frame đầu tiên.
Definition _mod.h:32
void hot_reload_enable(context &ctx, bool on, f32 interval=0.25f)
Bật hoặc tắt hot reload: sửa file ảnh, shader hoặc script Lua khi game đang chạy, lưu lại,...
std::variant< std::monostate, bool, f64, std::string, vec2, vec3, entt::entity > script_value
Một giá trị đi qua lại giữa C++ và Lua: nil, bool, số, chuỗi, vec2, vec3 hay entity.
Definition njin_script.h:22
bool script_attach(context &ctx, entt::entity entity, const char *path)
Gắn một script vào entity.
script_value script_field(context &ctx, entt::entity entity, const char *key)
Đọc trường key của bảng self của script gắn trên entity.
double f64
Số thực 64 bit.
Definition _types.h:29
std::int32_t i32
Số nguyên có dấu 32 bit.
Definition _types.h:14
Hình va chạm của một entity.
Definition njin_collision.h:46
Mô tả một module: một nhóm system có tên.
Definition _mod.h:67
Nhân vật platformer: chạy, nhảy, rơi, trèo dốc, đứng trên bục di chuyển.
Definition njin_body.h:44
Vị trí, góc xoay và tỉ lệ của một entity trong thế giới.
Definition _comps.h:14

Module njin

Mọi hàm của module chỉ gọi thẳng API C++ cùng tên, không thêm hành vi. Entity là số nguyên; vị trí là vec2 hay vec3 (cộng, trừ, nhân với số, :length(), :normalized(), :dot(), :cross()). Màu là bốn số r, g, b, a (0..1, a có thể bỏ).

Nhóm Hàm
Thời gian delta, elapsed, time_scale, set_time_scale
Ngẫu nhiên random, random_range(lo, hi), random_int(lo, hi)
Entity entity_create, entity_destroy, entity_valid
Transform position, set_position, rotation, set_rotation, scale, set_scale; bản 3D thêm 3d: position3d...
Nhập liệu key_pressed("space"), key_held, key_released, mouse_pos, mouse_delta, mouse_wheel, mouse_pressed("left"), action_pressed("jump"), action_held, action_released, axis("move")
Hẹn giờ after(giây, hàm), every(giây, hàm, số_lần), cancel(id), tween_move, tween_scale, tween_rotate, tween_value(từ, đến, giây, hàm), tween_cancel
Âm thanh sound_load, sound_play, sound_play_at(id, vec2), sound_play3d(id, vec3 hay entity), sound_stop
Scene scene_set(tên), scene_fade(tên)
Vẽ 2D draw_rect, draw_circle, draw_line, draw_text
Thân 2D platformer_input(e, move_x, jump, jump_held, drop), platformer(e), platformer_set_velocity, topdown_input(e, vec2, dash), topdown(e), topdown_set_velocity
Va chạm 2D collision_move(e, vec2), overlap_rect(x, y, w, h), overlap_point(vec2), raycast2d(từ, đến, bỏ_qua)
Sprite sprite_flip(e, x, y), sprite_visible, sprite_tint, anim_play(e, clip), anim_stop, anim_resume, anim_current, anim_set(e, tham_số, giá_trị), anim_trigger
Camera 2D camera_spawn(zoom, vec2), camera_follow(camera, mục_tiêu, {offset, deadzone, lookahead, smoothing, bounds = {x, y, w, h}})
Tilemap tile_get(map, cột, hàng), tile_set, tile_cell(map, vec2), tile_solid(map, vec2)
Hạt 2D particles_burst(e, số), particles_spawn(mẫu, vec2, số)
Vật lý 3D raycast3d(gốc, hướng, xa_nhất), body_velocity, body_set_velocity, body_impulse, character_move(e, vec3), character_position, character_grounded
Tìm đường 3D nav3d_path(navmesh, from, to, [filter]), nav3d_set_target(agent, đích), nav3d_stop, nav3d_position, nav3d_velocity, nav3d_arrived; vùng và vật cản: nav3d_set_filter, nav3d_agent_filter, nav3d_add_area, nav3d_remove_area, nav3d_add_obstacle, nav3d_move_obstacle, nav3d_remove_obstacle (xem Vùng và chi phí)
Vẽ 3D begin_3d({position, target, up, fovy, near, far}), end_3d, draw_cube3d(tâm, cỡ, màu), draw_sphere3d(tâm, bán_kính, màu), draw_cylinder3d(từ, đến, bán_kính, màu), draw_capsule3d, draw_plane3d(tâm, vec2, màu), draw_shape3d({kind, position, rotation, size, radius, height, thickness, rounding}, màu), material3d_set({specular, shininess, emission, rim, unlit, cast_shadows, reflect, world_uv})
Model 3D model_load(đường_dẫn), model_valid, draw_model(m, transform, {anim, time, loop, blend_anim, blend_time, blend, morphs = {tên = trọng_số}, tint}), model_anim_count, model_anim_find, model_anim_name, model_anim_duration(m, tên hay chỉ_số), model_morph_count, model_bone_count, model_bone_find, model_bone_position(m, xương, transform, tư_thế), model3d_set(e, {model, anim, time, loop, speed, tint, visible}), model3d(e)
Ánh sáng 3D, trời light3d_set({direction, color, ambient, shadows, shadow_range, shadow_softness, fog_color, fog_density}), light3d_get, light3d_add({kind = "point"/"spot", position, direction, color, intensity, radius, cone, softness, shadows}), draw_sky3d({hour, latitude, season, north, weather, ...}), sky3d_sun_direction, weather3d_preset("clear"/"overcast"/"rain"/"snow"/"fog")
Ánh sáng 2D lighting_set({enabled, ambient, exposure}), light2d_set(e, {kind, color, temperature, intensity, radius, size, angle, cone, softness, height, elevation, cast_shadows, enabled})
Giao diện ui_begin({id, title, anchor, pivot, offset, width, background, navigable}), ui_end, ui_row(số_cột), ui_label, ui_space, ui_button(nhãn, bật), ui_toggle(nhãn, giá_trị), ui_slider(nhãn, giá_trị, min, max, bước, phần_trăm), ui_choice(nhãn, chỉ_số, {lựa_chọn...}), ui_progress, ui_back, ui_active, ui_mouse_over, ui_last_rect, ui_toast(chữ, giây)
Chữ font_load(đường_dẫn, cỡ), draw_text_font(chữ, vec2, cỡ, font, màu), text_measure(chữ, cỡ, font), tr(khóa), trf(khóa, ...)
Spline spline_create({điểm...}, {kind = "catmull_rom"/"bezier", closed, alpha, steps, owner}), spline_set_points, spline_destroy, spline_valid, spline_length, spline_point(id, t), spline_point_at(id, khoảng_cách), spline_tangent_at, spline_nearest(id, điểm), spline_follow(id, {distance, speed, end}, dt), spline_draw_debug
Hiệu ứng 3D post3d_set({ssao, ssao_radius, ssao_half, ssr, motion_blur, shafts, flare, taa, taa_sharpen}), post3d_off, decal3d_add({position, normal hay rotation, size, color, lifetime, fade, paint}), decal3d_remove, decal3d_clear
Log log, warn, error (và print), kèm file và dòng của script

Hàm trả về một bảng: platformer(e) có velocity, grounded, on_slope, on_wall, facing, jumped, landed; topdown(e) có velocity, facing, moving, dashing; collision_move có moved, hit_x, hit_y, grounded, other_x, other_y; raycast2d và raycast3d có point, normal, distance, entity (và body cho 3D), hoặc nil nếu không trúng. Entity không có component cần thiết thì hàm không làm gì (hay trả nil).

platformer_input và topdown_input ghi vào input của thân như phía C++: lần bấm nhảy hay lướt được giữ đến nhịp vật lý kế tiếp, nên gọi mỗi frame là đủ.

Cảnh 3D, ánh sáng và giao diện từ Lua

Một script dựng được cả một cảnh 3D: nạp model, vẽ nó có ánh sáng và bóng đổ dưới bầu trời theo giờ, thêm đèn, cho một quả bóng chạy theo spline, và dựng menu. Vẽ 3D nằm trong on_render, giữa njin.begin_3d và njin.end_3d như phía C++; giao diện nằm trong on_ui, vì các hàm ui_* của engine chỉ chạy trong phase_post_render.

-- Một cảnh 3D dựng từ Lua: nhân vật glTF dưới bầu trời buổi chiều, bóng đổ, một đèn
-- điểm, một quả bóng chạy vòng theo spline, và menu đổi động tác.
local M = {}
function M.on_start(self)
self.model = njin.model_load("models/hero.glb")
self.clip = "Idle"
self.t = 0
self.speed = 1
-- Đường kín qua bốn điểm; spline mất theo entity này.
self.path = njin.spline_create({ { -2, 0.4, -1 }, { 0, 0.4, 1.2 }, { 2, 0.4, -1 }, { 0, 0.4, -2.5 } },
{ closed = true, owner = self.entity })
self.follow = { distance = 0, speed = 1.5, ["end"] = "loop" }
self.ball = njin.vec3(0, 0.4, 0)
end
function M.on_update(self, dt)
self.t = self.t + dt * self.speed
self.ball = njin.spline_follow(self.path, self.follow, dt)
end
function M.on_render(self)
njin.light3d_set({ shadows = true, shadow_range = 8 })
njin.begin_3d({ position = { 0, 1.7, 4.2 }, target = { 0, 0.8, 0 }, fovy = 50 })
njin.draw_sky3d({ hour = 15, weather = "clear" }) -- đặt cả hướng nắng và ánh sáng nền
njin.draw_plane3d({ 0, 0, 0 }, { 20, 20 }, { 0.55, 0.57, 0.52, 1 })
if self.model then
njin.draw_model(self.model, { position = { 0, 0, 0 } }, { anim = self.clip, time = self.t })
end
njin.draw_sphere3d(self.ball, 0.25, { 1, 0.3, 0.2, 1 })
njin.light3d_add({ kind = "point", position = { 1.2, 0.6, 1.2 }, color = { 0.2, 0.5, 1, 1 }, intensity = 4,
radius = 3 })
njin.end_3d()
end
function M.on_ui(self)
njin.ui_begin({ id = "menu", title = "Nhân vật", anchor = { 0, 0 }, pivot = { 0, 0 }, offset = { 16, 16 },
width = 240 })
njin.ui_label("Động tác: " .. self.clip)
if njin.ui_button("Đi bộ") then
self.clip = "Walk"
end
if njin.ui_button("Đứng yên") then
self.clip = "Idle"
end
local _
_, self.speed = njin.ui_slider("Tốc độ", self.speed, 0, 2)
njin.ui_end()
end
return M

Vài quy ước của nhóm hàm này:

  • Vị trí và màu nhận cả kiểu sẵn có lẫn bảng: njin.vec3(1, 2, 3) hay {1, 2, 3} hay {x = 1, y = 2, z = 3}; màu là bảng {r, g, b, a} hay {1, 0, 0, 1}. Transform là bảng {position, rotation, scale} (scale là một số hay một vec3), hoặc chỉ một vec3 vị trí.
  • Animation gọi theo tên clip hay chỉ số (từ 0, như phía C++). Trọng số morph đặt theo tên trong morphs.
  • model_load nhớ model theo đường dẫn: gọi lại với cùng file trả về cùng số, không nạp lần hai.
  • Hàm trả về hai giá trị theo kiểu Lua: changed, value = njin.ui_slider("Âm lượng", value, 0, 1). ui_choice đếm từ 1 như bảng Lua.
  • Spline là một số do spline_create trả về, sống đến spline_destroy, hay đến khi entity owner bị hủy. Một spline của bảng hai số là 2D (trả về vec2), ba số là 3D. Trạng thái đi dọc đường (distance, speed, end, finished) nằm trong một bảng của script, spline_follow sửa nó tại chỗ.
  • Số không hợp lệ (model chưa nạp, spline đã hủy, entity không còn, kiểu thời tiết lạ) là lỗi Lua có file và dòng của script, ví dụ scripts/scene.lua:21: njin.draw_model: 424242 is not a loaded model; game vẫn chạy tiếp.

Lưu và nạp game

Dữ liệu trong self (máu, túi đồ, cửa đã mở) đi vào save game bằng script_save_state(): nó trả về một njin::json_value để ghi cùng phần còn lại của save bằng json_save() (Cửa sổ, file và lưu game). Khi nạp, game dựng lại màn, gắn script, rồi gọi script_load_state(): mỗi script nhận lại các trường đã lưu, sau đó on_load(self) chạy.

Entity tạo lại có số khác, nên mỗi entity cần lưu phải có một tên lưu giống nhau ở mọi lần dựng màn: script_set_save_id() phía C++, hay self.save_id = "cua_kho" trong script. Entity không có tên lưu thì không được lưu (đạn, hạt, những gì dựng lại từ đầu là đủ).

#include <njin.h>
namespace {
using namespace njin;
i32 level = 1;
// Dựng màn: mỗi entity có script và một tên lưu riêng, giống nhau mỗi lần dựng.
void build_level(context &ctx) {
entt::registry &w = world(ctx);
const entt::entity chest = w.create();
script_attach(ctx, chest, "scripts/chest.lua");
script_set_save_id(ctx, chest, "chest_1");
const entt::entity guard = w.create();
script_attach(ctx, guard, "scripts/guard.lua");
script_set_save_id(ctx, guard, "guard_east");
}
void save_game(context &ctx) {
save.set("level", level).set("scripts", script_save_state(ctx));
json_save(save_path(ctx, "save.json").c_str(), save);
}
void load_game(context &ctx) {
json_value save;
if (!json_load(save_path(ctx, "save.json").c_str(), save))
return;
level = save["level"].int_or(1);
build_level(ctx); // entity mới, số mới
script_load_state(ctx, save["scripts"]); // đổ lại self theo tên lưu, rồi gọi on_load
}
void update(context &ctx) {
if (key_pressed(ctx, key_f5))
save_game(ctx);
if (key_pressed(ctx, key_f9)) {
world(ctx).clear();
load_game(ctx);
}
}
void setup(context &ctx) {
ecs_register(ctx, phase_startup, build_level, "build_level");
ecs_register(ctx, phase_update, update, "update");
}
} // namespace
mod_desc save_module() { return {.name = "save", .setup = setup}; }
std::string save_path(const context &ctx, const char *file_name)
Đường dẫn đầy đủ của một file lưu game, trong thư mục riêng của người dùng.
bool key_pressed(const context &ctx, key_code key)
Đúng ở frame mà phím vừa được nhấn xuống (chỉ một frame).
@ key_f9
Phím F9.
Definition _types.h:392
@ key_f5
Phím F5.
Definition _types.h:388
bool json_save(const char *path, const json_value &value, bool pretty=true)
Ghi một giá trị ra file JSON, tạo thư mục cha nếu cần.
bool json_load(const char *path, json_value &out)
Đọc và phân tích một file JSON.
json_value script_save_state(context &ctx)
Trạng thái của mọi script có tên lưu, để ghi vào save game: các trường của self (số,...
i32 script_load_state(context &ctx, const json_value &state)
Đổ trạng thái đã lưu bằng script_save_state() vào các script đang gắn có cùng tên lưu,...
void script_set_save_id(context &ctx, entt::entity entity, const char *id)
Đặt tên lưu (self.save_id) cho script gắn trên entity: khóa để script_load_state() tìm lại đúng entit...
Một giá trị JSON: null, bool, số, chuỗi, mảng hoặc object.
Definition njin_json.h:33
i32 int_or(i32 fallback) const
Số nguyên (làm tròn về 0), hoặc fallback.
Definition njin_json.h:124
static json_value make_object()
Một object rỗng.
Definition njin_json.h:69
json_value & set(std::string_view key, json_value value)
Đặt thành viên key (thay nếu đã có, thêm vào cuối nếu chưa).
Trong self Lưu thành
Số, bool, chuỗi Như cũ (số nguyên vẫn là số nguyên khi nạp)
njin.vec2, njin.vec3 {"$vec2": [x, y]}, {"$vec3": [x, y, z]}, nạp lại đúng từng số
Bảng có khóa 1..n liền nhau Mảng JSON
Bảng khác Object; khóa số nguyên ghi là "#n"
self.entity Không lưu: script_attach() đặt lại
Hàm, userdata khác, số không hữu hạn, vòng tham chiếu, khóa không phải chuỗi hay số nguyên Bỏ, kèm một cảnh báo có đường dẫn, ví dụ chest_1.self.inv[3]

Hai điều cần nhớ:

  • on_start của script vừa gắn vẫn chạy ở lần cập nhật đầu tiên, sau on_load. Đặt giá trị mặc định trong bảng của file (M.hp = 10: self.hp đọc ra 10 cho đến khi entity tự đặt) hay viết self.hp = self.hp or 10, để on_start không đè lên dữ liệu vừa nạp.
  • Số entity cất trong self (mục tiêu đang đuổi) được lưu như số thường và không còn đúng sau khi nạp. Lưu tên lưu của entity đó thay vì số của nó.

Sửa script khi game đang chạy

Khi hot_reload_enable() bật (Vẽ ảnh và shader), engine theo dõi cả file script. File gắn trên entity được chạy lại và các hàm mới thay hàm cũ cho mọi entity dùng nó, còn dữ liệu trong self giữ nguyên; on_reload(self) được gọi sau đó. File nạp bằng script_run_file() được chạy lại từ đầu. File mới có lỗi thì giữ các hàm cũ và ghi lỗi vào log. Mỗi lần nạp lại gửi một njin::asset_reloaded có script = true.

Lỗi và an toàn

Lỗi Lua (cú pháp hay lúc chạy) không làm game dừng: hàm trả về thất bại và log ghi thông báo kèm file và dòng, ví dụ scripts/enemy.lua:12: attempt to index a nil value. Một on_update lỗi thì lỗi mỗi frame: mỗi thông báo chỉ ghi một lần, cho đến lần hot reload kế tiếp.

Script chạy trong một máy Lua bị giới hạn: không có os.execute, io.popen, package.loadlib hay nạp bytecode (load chỉ nhận chữ); mặc định không có io, dofile, loadfile. require("ai.patrol") vẫn dùng được: nó tìm ai/patrol.lua (hay scripts/ai/patrol.lua) như mọi tài nguyên. Công cụ tin cậy (trình sửa màn, bản dựng) có thể mở io bằng script_init(ctx, {.allow_io = true}), gọi trước mọi hàm script_* khác.

Hiệu năng và giới hạn

Đo bằng harness với 1000 entity có on_update rỗng: bản Debug của engine thêm khoảng 0,8 ms mỗi frame, bản Release khoảng 0,3 ms. Hàm của module njin chỉ là một lệnh gọi C++, nhưng mỗi lần qua lại giữa Lua và C++ vẫn tốn hơn một lệnh gọi C++ thường: vòng lặp nóng trên hàng nghìn vật (hạt, đạn) nên để ở C++.

  • Module có phần gameplay chung, vẽ 3D, ánh sáng, giao diện, spline và hiệu ứng 3D; các phần khác (địa hình, nước, vật lý mềm, video, âm thanh 3D theo voice) chưa có hàm Lua. Game cần thì tự thêm bằng script_register().
  • Chuyển động và vật lý cần chạy như nhau ở mọi FPS thì đặt trong on_fixed_update: nó chạy đúng số bước cố định của engine (mặc định 60 lần mỗi giây, config::fixed_hz), trước bước vật lý 3D, nên njin.body_set_velocity đặt ở đó có hiệu lực ngay trong bước. Không script nào có hàm này thì engine không đi qua các entity ở nhịp cố định.
  • Dữ liệu self giữ qua hot reload, và vào file save qua script_save_state() khi entity có tên lưu (Lưu và nạp game). Script không tự ghi file được (không có io): việc lưu do C++ gọi.