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
Scene

Scene là một trạng thái lớn của game: menu, đang chơi, tạm dừng, game over. Mỗi lúc chỉ có một scene chạy.

#include <njin.h>
namespace {
njin::scene_handle menu, play;
// Chạy một lần khi vào scene "play": tạo thế giới của màn chơi.
void enter_play(njin::context &ctx) {
entt::registry &reg = njin::world(ctx);
for (int i = 0; i < 10; i++) {
const entt::entity e = reg.create();
reg.emplace<njin::transform>(e, njin::transform{.pos = {i * 40.0f, 100}});
// Rời scene "play" thì engine tự hủy entity này.
reg.emplace<njin::scene_owned>(e, njin::scene_owned{play});
}
}
void menu_update(njin::context &ctx) {
njin::scene_set(ctx, play); // chuyển ở đầu frame sau
}
void play_update(njin::context &ctx) {
njin::scene_set(ctx, menu);
}
void draw_menu(njin::context &ctx) {
njin::draw_text(ctx, "Press Enter", {20, 20}, 30, njin::colors::white);
}
void setup(njin::context &ctx) {
// Đăng ký scene trước, để dùng handle trong sys_desc.
menu = njin::scene_register(ctx, {.name = "menu"});
play = njin::scene_register(ctx, {.name = "play", .on_enter = enter_play});
// Mỗi system chỉ chạy trong scene của nó.
njin::ecs_register(ctx, njin::phase_update, njin::sys_desc{.fnc = menu_update, .scene = menu});
njin::ecs_register(ctx, njin::phase_update, njin::sys_desc{.fnc = play_update, .scene = play});
njin::ecs_register(ctx, njin::phase_post_render, njin::sys_desc{.fnc = draw_menu, .scene = menu});
[](njin::context &c) { njin::scene_set(c, menu); });
}
} // namespace
njin::mod_desc scenes_module() { return {.name = "scenes", .setup = setup}; }
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_escape
Esc.
Definition _types.h:414
@ key_enter
Enter.
Definition _types.h:412
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
@ phase_post_render
Mỗi frame, vẽ trong không gian màn hình (UI).
Definition _mod.h:39
scene_handle scene_register(context &ctx, const scene_desc &desc)
Đăng ký một scene.
void scene_set(context &ctx, scene_handle scene)
Chuyển sang một scene khác.
void draw_text(const context &ctx, const char *text, vec2 pos, f32 size, rgba color, font_handle font={})
Vẽ chữ với góc trên trái tại pos.
constexpr rgba white
Trắng.
Definition _math.h:234
Mô tả một module: một nhóm system có tên.
Definition _mod.h:67
Định danh của một scene, tạo bởi scene_register().
Definition _types.h:139
Gắn entity vào một scene.
Definition _comps.h:122
Mô tả một system kèm ràng buộc thứ tự, dùng với ecs_register().
Definition _mod.h:47
Vị trí, góc xoay và tỉ lệ của một entity trong thế giới.
Definition _comps.h:14

Ba việc scene làm cho bạn

1. System chỉ chạy trong một scene. Đặt scene trong njin::sys_desc. System không đặt scene (handle id 0) chạy ở mọi scene.

2. Gọi hàm khi vào và rời scene. on_enter và on_exit trong njin::scene_desc, mỗi hàm chạy đúng một lần mỗi lần chuyển. Đây là chỗ tạo thế giới của màn chơi và lưu điểm.

3. Tự hủy entity của scene. Gắn njin::scene_owned vào entity; khi rời scene đó, engine hủy nó. Không phải tự dọn.

Chuyển scene

njin::scene_set() không chuyển ngay mà ở đầu frame sau, trước mọi system của frame đó. Frame hiện tại vì thế chạy trọn vẹn trong một scene.

Khi chuyển, engine lần lượt:

  1. gọi on_exit của scene cũ,
  2. hủy mọi entity có njin::scene_owned trỏ tới scene cũ,
  3. gọi on_enter của scene mới.

Scene đầu tiên cũng được đặt bằng njin::scene_set(), thường trong phase_startup. Trước đó chưa có scene nào: chỉ system không gắn scene chạy.

Hàm Việc làm
njin::scene_register() Tạo scene. Tên đã có thì trả về scene cũ
njin::scene_find() Tìm scene theo tên
njin::scene_set() Chuyển scene ở đầu frame sau
njin::scene_current() Scene đang chạy

Chuyển cảnh mờ dần

njin::scene_fade() đổi scene sau một hiệu ứng mờ dần, và có thể hiện màn hình loading:

#include <njin.h>
namespace {
njin::scene_handle menu{}, level{};
void draw_loading(njin::context &ctx) {
const njin::vec2 screen = njin::screen_size(ctx);
njin::draw_text(ctx, "Đang tải...", {screen.x - 220.0f, screen.y - 60.0f}, 32.0f,
}
void enter_level(njin::context &ctx) {
// Nạp nặng ở đây: người chơi đang thấy màn hình loading, không phải cửa sổ đứng hình.
level_art = njin::texture_load(ctx, "assets/level1.png");
}
void exit_level(njin::context &ctx) { njin::texture_unload(ctx, level_art); }
void startup(njin::context &ctx) {
menu = njin::scene_register(ctx, {.name = "menu"});
level = njin::scene_register(ctx, {.name = "level", .on_enter = enter_level,
.on_exit = exit_level});
njin::scene_set(ctx, menu);
}
void menu_input(njin::context &ctx) {
// Bỏ qua phím trong lúc đang chuyển, để không bấm hai lần.
njin::scene_fade(ctx, level, {.fade_out = 0.4f, .hold = 0.3f, .fade_in = 0.6f,
.draw_loading = draw_loading});
}
void level_input(njin::context &ctx) {
njin::scene_fade(ctx, menu, {.color = {1.0f, 1.0f, 1.0f, 1.0f}}); // mờ sang trắng
}
void setup(njin::context &ctx) {
njin::ecs_register(ctx, njin::phase_update, {.fnc = menu_input, .scene = menu});
njin::ecs_register(ctx, njin::phase_update, {.fnc = level_input, .scene = level});
}
} // namespace
njin::mod_desc scene_fade_module() { return {.name = "scene_fade", .setup = setup}; }
vec2 screen_size(const context &ctx)
Trả về kích thước màn hình mà game vẽ lên, tính bằng pixel.
void scene_fade(context &ctx, scene_handle scene, const scene_transition &transition={})
Chuyển sang scene khác với hiệu ứng mờ dần (fade).
bool scene_transitioning(const context &ctx)
Có đang chuyển scene bằng scene_fade() không.
void texture_unload(context &ctx, texture_handle handle)
Giải phóng texture.
texture_handle texture_load(context &ctx, const char *path)
Nạp một texture từ file ảnh.
Định danh của một texture, tạo bởi texture_load().
Definition _types.h:146
Vector 2 chiều. Dùng cho vị trí, kích thước, độ dời.
Definition _types.h:39
f32 y
Thành phần dọc (trong không gian màn hình, y tăng khi đi xuống).
Definition _types.h:41
f32 x
Thành phần ngang.
Definition _types.h:40
flowchart LR
  A["Phủ dần<br/>fade_out giây"]:::engine --> B["Phủ kín:<br/>vẽ màn loading<br/>ít nhất một frame"]:::engine --> C["on_exit cũ,<br/>on_enter mới<br/>(nạp tài nguyên)"]:::update --> D["Giữ kín<br/>ít nhất hold giây"]:::engine --> E["Mở dần<br/>fade_in giây"]:::engine
  • Màn hình loading (draw_loading) được vẽ trước khi on_enter của scene mới chạy, nên on_enter nạp tài nguyên nặng thì người chơi vẫn thấy nó, không thấy cửa sổ đứng hình.
  • Thời gian tính theo giờ thật: pause và time_set_scale() không ảnh hưởng.
  • Game vẫn chạy trong lúc chuyển. Dùng njin::scene_transitioning() để bỏ qua nhập liệu nếu cần.
  • Gọi lại njin::scene_fade() khi đang chuyển thì chỉ đổi scene đích. njin::scene_set() khi đang chuyển thì hủy hiệu ứng và đổi ngay frame sau.
  • Lớp phủ nằm trên mọi thứ, kể cả UI.
Hàm Việc làm
njin::scene_fade() Chuyển scene với hiệu ứng mờ dần
njin::scene_transitioning() Đang chuyển không
njin::scene_transition_cover() Độ phủ hiện tại, 0..1

Scene hay tạm dừng?

Menu tạm dừng thường không nên là một scene riêng: chuyển scene sẽ hủy thế giới đang chơi. Hãy giữ scene "play" và dùng njin::time_set_paused() (xem Thời gian, fixed update, timer và tween), rồi vẽ menu khi đang dừng. Game Pong trong src/games/pong làm theo cách này.