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
Hộp thoại và đa ngôn ngữ

#include <njin.h>
// assets/lang/vi.json: { "_name": "Tiếng Việt", "npc": { "hello": "Chào bạn!" } }
// assets/dialog/owl.json: kịch bản hội thoại; chữ bắt đầu bằng @ là khóa dịch.
namespace {
void startup(njin::context &ctx) {
njin::i18n_load(ctx, "vi", "assets/lang/vi.json"); // ngôn ngữ nạp đầu tiên là mặc định
njin::i18n_load(ctx, "en", "assets/lang/en.json");
njin::dialog_load("assets/dialog/owl.json", owl);
// Chân dung gọi bằng tên trong kịch bản: "portrait": "owl".
njin::dialog_portrait(ctx, "owl", njin::texture_load(ctx, "assets/portraits.png"),
njin::rect{{0.0f, 0.0f}, {32.0f, 32.0f}});
// Điều kiện trong kịch bản ("if": "has_key") do game tự quyết định cú pháp.
njin::dialog_set_condition(ctx, [](njin::context &, const std::string &cond) { return cond == "always"; });
}
void talk(njin::context &ctx) {
njin::dialog_start(ctx, owl);
}
// Kịch bản gửi event: "event": "got_sword".
void on_dialog_event(const njin::dialog_event &e) { (void)e; }
void ui(njin::context &ctx) {
njin::ui_begin(ctx, {.id = "menu", .title = njin::tr(ctx, "menu.title")});
// Câu có tham số: "hud.coins": "Vàng: {0}/{1}"
njin::ui_label(ctx, njin::trf(ctx, "hud.coins", {"3", "10"}).c_str());
njin::i32 language = 0;
if (njin::ui_choice(ctx, njin::tr(ctx, "settings.language"), language,
njin::i18n_set_language(ctx, language == 0 ? "vi" : "en");
}
void setup(njin::context &ctx) {
njin::events(ctx).sink<njin::dialog_event>().connect<&on_dialog_event>();
}
} // namespace
int main() {
njin::context *ctx = njin::create({.title = "Dialog", .width = 1280, .height = 720, .target_fps = 60});
njin::mod_register(*ctx, {.name = "game", .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ổ.
bool dialog_load(const char *path, dialog_script &out)
Nạp một đoạn hội thoại từ file JSON.
void dialog_portrait(context &ctx, const char *name, texture_handle texture, rect source={})
Đăng ký một chân dung để các câu thoại gọi tên ("portrait": "oldman").
void dialog_start(context &ctx, const dialog_script &script, const char *start=nullptr)
Bắt đầu một đoạn hội thoại.
void dialog_set_condition(context &ctx, std::function< bool(context &, const std::string &)> fn)
Hàm kiểm tra điều kiện cond của câu thoại và lựa chọn: nhận chuỗi điều kiện, trả về đúng hay sai.
bool dialog_active(const context &ctx)
Hộp thoại có đang mở không.
bool i18n_load(context &ctx, const char *lang, const char *path)
Nạp bảng chuỗi của một ngôn ngữ từ file JSON.
const char * tr(const context &ctx, const char *key)
Chuỗi đã dịch của key trong ngôn ngữ đang dùng.
void i18n_set_language(context &ctx, const char *lang)
Chọn ngôn ngữ đang dùng.
const char * i18n_language_name(const context &ctx, const char *lang)
Tên hiện của một ngôn ngữ: khóa _name trong file của nó, hoặc chính mã.
std::string trf(const context &ctx, const char *key, std::initializer_list< std::string_view > args)
Như tr(), rồi thay {0}, {1}... bằng các tham số theo thứ tự: câu dịch được đổi trật tự từ theo từng n...
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_e
Phím E.
Definition _types.h:348
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::dispatcher & events(context &ctx)
Trả về dispatcher của EnTT để gửi và nhận event giữa các system.
@ 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
texture_handle texture_load(context &ctx, const char *path)
Nạp một texture từ file ảnh.
std::int32_t i32
Số nguyên có dấu 32 bit.
Definition _types.h:14
void ui_begin(context &ctx, const ui_panel_desc &desc={})
Bắt đầu một panel.
void ui_label(context &ctx, const char *text)
Một dòng chữ.
void ui_end(context &ctx)
Kết thúc panel và vẽ nó.
bool ui_choice(context &ctx, const char *label, i32 &index, std::initializer_list< const char * > options)
Chọn một trong nhiều lựa chọn bằng trái/phải hoặc bấm: độ khó, độ phân giải, ngôn ngữ.
Event: một câu thoại hoặc lựa chọn có event vừa được kích hoạt.
Definition njin_dialog.h:141
Một đoạn hội thoại: nhiều nút nối với nhau.
Definition njin_dialog.h:49
Hình chữ nhật thẳng trục: pos là góc trên trái, size là rộng và cao.
Definition _math.h:13

Đa ngôn ngữ

Mỗi ngôn ngữ là một file JSON. Object lồng nhau được nối tên bằng dấu chấm:

{ "_name": "Tiếng Việt",
"menu": { "play": "Chơi", "quit": "Thoát" },
"hud": { "coins": "Vàng: {0}/{1}" } }
Hàm Việc làm
i18n_load() Nạp một ngôn ngữ; nạp thêm file cùng ngôn ngữ thì ghi đè và bổ sung
i18n_set_language() Đổi ngôn ngữ đang dùng (áp dụng ngay)
i18n_languages(), i18n_language_name() Danh sách để hiện trong menu, dùng khóa _name
tr() Chuỗi của một khóa
trf() Như tr(), thay {0}, {1}... bằng tham số: mỗi ngôn ngữ tự đổi trật tự từ

Thiếu một khóa trong ngôn ngữ đang dùng thì lấy ở ngôn ngữ dự phòng (mặc định là ngôn ngữ nạp đầu tiên; đổi bằng i18n_set_fallback()); thiếu nữa thì hiện chính khóa và ghi log một lần, nên chữ thiếu dịch hiện ra rõ ràng thay vì mất hẳn.

Font mặc định của engine (JetBrains Mono) đã có chữ tiếng Việt. Muốn font khác, nạp bằng font_load() và đặt vào njin::ui_style::font và njin::dialog_style::font.

Hộp thoại

Hộp thoại của game mẫu: chân dung, tên người nói (Cú Già) và chữ chạy dần. Ảnh chụp khi câu đã chạy xong

Một kịch bản là một danh sách nút nối với nhau (njin::dialog_script). Viết bằng JSON:

{ "start": "hi", "nodes": [
{ "id": "hi", "speaker": "@owl.name", "portrait": "owl", "text": "@owl.hello", "next": "ask" },
{ "id": "ask", "text": "@owl.ask",
"choices": [ { "text": "@owl.yes", "next": "tip" },
{ "text": "@owl.no", "event": "refused" } ] },
{ "id": "tip", "text": "@owl.tip", "event": "got_hint", "next": null } ] }
  • Chữ bắt đầu bằng @ là khóa dịch (xem tr()); không thì là chữ thường.
  • Một nút không có next (cũng không có lựa chọn) đi tiếp xuống nút dưới nó. "next": null là kết thúc.
  • event gửi njin::dialog_event khi câu hiện ra (hoặc khi chọn); hội thoại xong gửi njin::dialog_ended.
  • "if": "điều_kiện" trên nút hoặc lựa chọn: game quyết định bằng dialog_set_condition().

dialog_start() chạy một kịch bản; dialog_say() nói một câu lẻ (biển báo, đồ vật). Hộp thoại được engine vẽ và điều khiển: chữ chạy từng ký tự (bấm một lần để hiện hết), mũi tên chọn, Enter / Space / nút A / chuột trái để sang câu. Trong lúc mở, các phím đó không tới game; phím di chuyển thì vẫn tới, nên kiểm tra dialog_active() hoặc bật dialog_style::pause_game.

Giao diện là njin::dialog_style: font, cỡ chữ, số dòng, tốc độ chữ, tiếng "blip", chân dung. Hộp, thẻ tên và lựa chọn dùng njin::ui_look như UI, nên dùng được ảnh 9-slice và shader riêng. Chân dung đăng ký bằng dialog_portrait().

Để ngắt một đoạn chữ dài ở chỗ khác, dùng text_wrap() và draw_text_wrapped().