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
Cửa sổ, file và lưu game

#include <njin.h>
#include <string>
namespace {
int high_score = 0;
std::string save_file;
void load(njin::context &ctx) {
// %APPDATA%/<app_name>/highscore.txt trên Windows. Thư mục tự được tạo.
save_file = njin::save_path(ctx, "highscore.txt");
std::string text;
if (njin::file_read(save_file.c_str(), text))
high_score = std::stoi(text);
}
void update(njin::context &ctx) {
const njin::vec2 size = njin::screen_size(ctx);
NJIN_INFO("cửa sổ mới: %.0f x %.0f", size.x, size.y);
}
njin::quit(ctx); // phase_shutdown vẫn chạy
}
void save(njin::context &) {
// Ghi vào file tạm rồi đổi tên: tắt ngang cũng không hỏng file cũ.
njin::file_write(save_file.c_str(), std::to_string(high_score));
}
void setup(njin::context &ctx) {
}
} // namespace
int main() {
const njin::config cfg{.title = "My Game",
.width = 1280,
.height = 720,
.target_fps = 60,
.exit_key = njin::key_none, // Esc dành cho menu
.resizable = true,
.app_name = "my_game"};
njin::context *ctx = njin::create(cfg);
njin::mod_register(*ctx, {.name = "save_window", .setup = setup});
njin::run(*ctx);
}
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ổ.
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.
bool file_read(const char *path, std::string &out)
Đọc toàn bộ một file.
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 file_write(const char *path, std::string_view data)
Ghi toàn bộ một file, tạo thư mục cha nếu chưa có.
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_q
Phím Q.
Definition _types.h:360
@ key_f11
Phím F11.
Definition _types.h:394
@ key_none
Không có phím. Không hợp lệ khi bind hay truy vấn.
Definition _types.h:341
#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.
@ phase_shutdown
Một lần, sau khi cửa sổ đóng.
Definition _mod.h:40
@ 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 window_set_fullscreen(context &ctx, bool fullscreen)
Bật hoặc tắt toàn màn hình.
bool window_fullscreen(const context &ctx)
Cửa sổ có đang toàn màn hình không.
bool window_resized(const context &ctx)
Cửa sổ có vừa đổi kích thước ở frame này không (người dùng kéo cửa sổ, hoặc bật tắt toàn màn hình).
void quit(context &ctx)
Yêu cầu thoát game.
Cấu hình cửa sổ và vòng lặp, truyền vào create().
Definition njin_cfg.h:11
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

Cấu hình khi tạo

Ngoài tiêu đề, kích thước, FPS và màu nền, njin::config có:

Trường Mặc định Ý nghĩa
fixed_hz 60 Số nhịp phase_fixed_update mỗi giây
exit_key key_escape Phím đóng game ngay. Đặt key_none khi cần Esc cho menu
resizable false Cho phép kéo đổi kích thước cửa sổ
app_name tiêu đề Tên thư mục lưu game
vsync false Đồng bộ dọc: mỗi frame chờ màn hình làm tươi, không xé hình, CPU và GPU nghỉ giữa các frame. target_fps vẫn là mức trần thêm

Cửa sổ

Hàm Việc làm
njin::screen_size() Kích thước hiện tại của cửa sổ
njin::window_resized() Cửa sổ vừa đổi kích thước ở frame này
njin::window_set_fullscreen() / njin::window_fullscreen() Toàn màn hình dạng cửa sổ không viền
njin::window_set_vsync() / njin::window_vsync() Bật tắt đồng bộ dọc lúc đang chạy; được lưu trong cài đặt
njin::window_set_size(), njin::window_set_title() Đổi kích thước, tiêu đề
njin::cursor_set_visible() Ẩn hiện con trỏ chuột
njin::cursor_set_locked() Khóa con trỏ trong cửa sổ; đọc njin::mouse_delta()
njin::quit() Thoát ở cuối frame. phase_shutdown vẫn chạy
njin::window_set_close_intercept(), njin::window_close_requested() Nút [x] không thoát ngay mà báo cho game; game tự dọn rồi gọi njin::quit()

Giữ màn chơi vừa mọi kích thước cửa sổ. Thiết kế game theo một kích thước cố định, rồi mỗi frame đặt zoom của camera cho vừa cửa sổ:

const njin::vec2 screen = njin::screen_size(ctx);
cam.zoom = std::fmin(screen.x / 960.0f, screen.y / 540.0f);
cam.offset = screen * 0.5f;

Chụp màn hình

njin::screenshot() lưu ảnh của frame hiện tại ra file:

njin::screenshot(ctx); // tự đặt tên, trong thư mục lưu game
njin::screenshot(ctx, "captures/level1.png"); // hoặc chỉ định đường dẫn
@ key_f12
Phím F12.
Definition _types.h:395
void screenshot(context &ctx, const char *path=nullptr)
Chụp màn hình và lưu ra file ảnh.
  • Ảnh được chụp ở cuối frame, sau phase_post_render, nên có đủ thế giới lẫn UI dù gọi ở phase nào.
  • Không truyền đường dẫn thì ảnh vào screenshots/ trong thư mục lưu game (xem njin::save_path()), tên theo ngày giờ, không bao giờ trùng nhau.
  • Định dạng theo đuôi file: .png, .bmp, .tga, .qoi. Đuôi khác thì không lưu và ghi cảnh báo vào log.
Ghi chú
raylib mặc định tự chụp màn hình khi bấm F12 trong mọi game, lưu vào thư mục làm việc. njin tắt hành vi ngầm đó (cờ SUPPORT_SCREEN_CAPTURE=0 trong CMakeLists.txt), nên F12 là phím bình thường, game tự quyết định dùng vào việc gì.

Đường dẫn tài nguyên

Mọi hàm nạp (texture, shader, font, sound, music) tìm file theo thứ tự:

  1. đúng đường dẫn đã cho, tính từ thư mục làm việc,
  2. nếu không thấy và là đường dẫn tương đối: tính từ thư mục chứa file exe.

Nhờ bước 2, game chạy bằng cách nhấp đúp file exe (thư mục làm việc khác) vẫn tìm thấy assets/ đặt cạnh exe.

Đưa assets vào cạnh exe

Đặt tài nguyên của game vào một thư mục cạnh CMakeLists.txt của game, rồi gọi njin_add_assets() (khai báo trong cmake/njin.cmake):

add_executable(my_game main.cpp)
target_link_libraries(my_game PRIVATE njin::rt njin_warnings)
njin_add_assets(my_game assets) # src/games/my_game/assets -> build/bin/assets

Mỗi lần build my_game, thư mục được copy cạnh exe, chỉ những file đã đổi, nên sửa một ảnh rồi build lại là đủ. Trong code, luôn dùng đường dẫn tương đối như "assets/player.png". Cả thư mục build/bin khi đó chạy được ở bất cứ đâu, gửi cho người khác cũng được.

File đã xóa khỏi thư mục gốc không bị xóa khỏi bản copy; xóa build/bin/assets để làm sạch. Nhiều game trong cùng build/bin thì đặt tên thư mục khác nhau (ví dụ njin_add_assets(pong pong_assets)) để không đè lên nhau.

Định dạng lưu game

njin::save_path() cho biết nơi lưu. Để lưu nhiều giá trị có cấu trúc (màn chơi, túi đồ, cài đặt), dùng JSON: njin::json_save() và njin::json_load(), xem JSON và lưu game.

File

Hàm Việc làm
njin::file_exists() File có tồn tại không
njin::file_read() Đọc toàn bộ file vào một chuỗi
njin::file_write() Ghi toàn bộ file, tạo thư mục cha nếu cần
njin::save_path() Đường dẫn file lưu game trong thư mục của người dùng

njin::file_write() ghi vào một file tạm rồi đổi tên, nên game tắt giữa chừng cũng không làm hỏng file cũ.

njin::save_path() trả về đường dẫn trong thư mục riêng của người dùng, tạo thư mục nếu chưa có:

Hệ điều hành Thư mục
Windows APPDATA%\<app_name>
macOS ~/Library/Application Support/<app_name>
Linux ~/.local/share/<app_name>

Mọi đường dẫn là UTF-8, nên tên file và thư mục có dấu tiếng Việt vẫn đúng.