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
Module, system và phase

Logic của game trong njin được chia thành ba khái niệm:

Khái niệm Là gì Kiểu
System Một hàm chạy ở một thời điểm nhất định trong frame njin::sys_fnc
Phase Một giai đoạn của frame mà system được gắn vào njin::sys_phase
Module Một nhóm system có tên, đăng ký cùng lúc njin::mod_desc

System

System là một hàm thường, nhận context của engine và không trả về gì:

void move(njin::context &ctx);

Nó không có trạng thái riêng. Dữ liệu nằm trong component của entity (xem Entity, component và event) hoặc trong biến của file.

Phase

Mỗi frame gọi các phase theo thứ tự cố định. Xem chi tiết ở Vòng lặp game.

Phase Khi nào chạy Dùng để
njin::phase_startup Một lần, trước frame đầu Tạo entity, nạp tài nguyên, gắn phím
njin::phase_pre_update Mỗi frame Chuẩn bị trước khi cập nhật
njin::phase_fixed_update Theo nhịp cố định, 0 hoặc nhiều lần mỗi frame Vật lý, va chạm (xem Thời gian, fixed update, timer và tween)
njin::phase_update Mỗi frame Logic chính
njin::phase_post_update Mỗi frame Việc cần làm sau khi mọi thứ đã cập nhật, ví dụ camera đi theo người chơi
njin::phase_pre_render Mỗi frame Chuẩn bị vẽ
njin::phase_render Mỗi frame Vẽ trong không gian thế giới
njin::phase_post_render Mỗi frame Vẽ trong không gian màn hình (UI)
njin::phase_shutdown Một lần, sau khi cửa sổ đóng Dọn dẹp

Module

Module là nơi bạn khai báo system. Nó gồm một cái tên và một hàm setup:

#include <njin.h>
namespace {
// Một system là hàm thường nhận context&.
void spawn(njin::context &ctx) {
entt::registry &registry = njin::world(ctx);
const entt::entity entity = registry.create();
registry.emplace<njin::transform>(entity);
NJIN_INFO("đã tạo entity đầu tiên");
}
// setup được gọi đúng một lần khi đăng ký module.
void setup(njin::context &ctx) {
}
} // namespace
njin::mod_desc hello_module() {
return {.name = "hello", .setup = setup};
}
int main() {
const njin::config cfg{.title = "hello",
.width = 800.0f,
.height = 600.0f,
.target_fps = 60.0f};
njin::context *ctx = njin::create(cfg);
njin::mod_register(*ctx, hello_module()); // trước run
njin::run(*ctx);
return 0;
}
void run(context &ctx)
Chạy vòng lặp chính cho đến khi cửa sổ đóng.
context * create(const config &cfg)
Mở cửa sổ và trả về context của engine.
void destroy(context *ctx)
Giải phóng mọi tài nguyên của engine và đóng cửa sổ.
#define NJIN_INFO(...)
Ghi log mức info. Tham số giống printf.
Definition njin_log.h:98
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.
void mod_register(context &ctx, const mod_desc &desc)
Chạy setup của module và đưa các system của nó vào lịch chạy.
entt::registry & world(context &ctx)
Trả về registry của EnTT chứa mọi entity và component của game.
@ phase_startup
Một lần, trước frame đầu tiên.
Definition _mod.h:32
Cấu hình cửa sổ và vòng lặp, truyền vào create().
Definition njin_cfg.h:11
Mô tả một module: một nhóm system có tên.
Definition _mod.h:67
Vị trí, góc xoay và tỉ lệ của một entity trong thế giới.
Definition _comps.h:14

Cách hoạt động:

  1. mod_register() gọi setup đúng một lần.
  2. Trong setup, bạn gọi ecs_register() cho từng system để gắn nó vào một phase.
  3. Từ đó, system chạy mỗi khi phase của nó chạy.

Các quy tắc:

  • mod_register() phải được gọi trước run().
  • Tên module phải duy nhất. Đăng ký hai lần cùng một tên bị bỏ qua và ghi cảnh báo.
  • ecs_register() chỉ hợp lệ bên trong setup. Gọi ở nơi khác bị bỏ qua và ghi cảnh báo.
  • Module lõi của engine (camera, âm thanh, sprite) đã được đăng ký sẵn bởi create().

Đăng ký nhiều module một lần

Thay vì gọi mod_register() cho từng module, truyền cả danh sách:

njin::mod_register(*ctx, {input_module(), physics_module(), ui_module()});

Danh sách tạo lúc chạy (std::vector<njin::mod_desc>, std::array) cũng được:

std::vector<njin::mod_desc> mods{input_module(), physics_module()};
if (debug)
mods.push_back(debug_overlay_module());
njin::mod_register(*ctx, mods);

Kết quả giống hệt gọi từng module lần lượt: module đứng trước chạy trước trong cùng phase, và một module lỗi (ví dụ trùng tên) chỉ bị bỏ qua riêng nó, các module còn lại vẫn được đăng ký.

Thứ tự chạy

Có hai tầng:

  1. Giữa các module: chạy theo thứ tự đăng ký. Module đăng ký trước, system của nó chạy trước, trong cùng một phase.
  2. Trong một module, trong một phase: theo ràng buộc bạn khai báo bằng njin::sys_desc.

Nếu không khai báo gì, các system chạy theo đúng thứ tự bạn đăng ký. Khi cần kiểm soát:

#include <njin.h>
namespace {
void read_input(njin::context &) {}
void move(njin::context &) {}
void clamp_to_screen(njin::context &) {}
void debug_overlay(njin::context &) {}
void setup(njin::context &ctx) {
// Không ràng buộc: chạy theo thứ tự đăng ký (order mặc định là 100).
// after: move phải chạy sau read_input.
njin::sys_desc{.fnc = move, .after = {read_input}});
// clamp_to_screen phải chạy sau move.
njin::sys_desc{.fnc = clamp_to_screen, .after = {move}});
// order lớn hơn: chạy muộn nhất trong những system còn lại.
njin::sys_desc{.fnc = debug_overlay, .order = 200});
}
} // namespace
njin::mod_desc physics_module() {
return {.name = "physics", .setup = setup};
}
@ phase_update
Mỗi frame, logic chính của game.
Definition _mod.h:35
Mô tả một system kèm ràng buộc thứ tự, dùng với ecs_register().
Definition _mod.h:47
Trường Ý nghĩa
after Các system phải chạy trước system này
before Các system phải chạy sau system này
order Giá trị nhỏ chạy trước, trong số các system đã được after/before cho phép (mặc định 100)
scene Chỉ chạy khi scene này đang chạy. Để trống là chạy ở mọi scene (xem Scene)

Khi hai system cùng sẵn sàng và có cùng order, system đăng ký trước chạy trước.

Lưu ý
after/before chỉ tham chiếu được các system cùng module và cùng phase. Tham chiếu tới system ở nơi khác bị bỏ qua và ghi cảnh báo. Nếu các ràng buộc tạo thành vòng (A sau B, B sau A), engine ghi lỗi và để các system còn lại chạy theo thứ tự đăng ký.

Ví dụ hoàn chỉnh: chuyển động

Một component velocity do game tự định nghĩa, và một system cộng vận tốc vào vị trí:

#include <njin.h>
namespace {
// Component của riêng game: vận tốc, đơn vị thế giới trên giây.
struct velocity {
njin::vec2 value{};
};
void spawn(njin::context &ctx) {
entt::registry &registry = njin::world(ctx);
const entt::entity entity = registry.create();
registry.emplace<njin::transform>(entity);
registry.emplace<velocity>(entity, velocity{.value = {50.0f, 0.0f}});
}
void move(njin::context &ctx) {
const njin::f32 dt = njin::delta(ctx);
auto view = njin::world(ctx).view<njin::transform, const velocity>();
for (auto [entity, tr, vel] : view.each()) {
tr.pos.x += vel.value.x * dt;
tr.pos.y += vel.value.y * dt;
}
}
void setup(njin::context &ctx) {
}
} // namespace
njin::mod_desc movement_module() {
return {.name = "movement", .setup = setup};
}
const char * tr(const context &ctx, const char *key)
Chuỗi đã dịch của key trong ngôn ngữ đang dùng.
f32 delta(const context &ctx)
Thời gian của frame trước, tính bằng giây, đã nhân với tốc độ thời gian.
float f32
Số thực 32 bit.
Definition _types.h:28
Vector 2 chiều. Dùng cho vị trí, kích thước, độ dời.
Definition _types.h:39

Lưu ý dòng njin::delta(ctx): nhân vận tốc với thời gian của frame để tốc độ không phụ thuộc FPS.