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
Nhập liệu

njin hỗ trợ bàn phím, chuột, tay cầm và nhập văn bản. Có ba cách dùng, từ trực tiếp đến linh hoạt:

Cách Khi nào dùng
Hỏi thẳng thiết bị (key_*, mouse_*, pad_*) Việc nhanh, cố định
Action: một tên logic, đúng hoặc sai "Nhảy", "bắn": nút bấm
Axis: một tên logic, giá trị -1 đến 1 "Đi ngang", "đi dọc": chuyển động

Action và axis cho phép đổi phím sau này mà không sửa logic.

Trạng thái nút

Trạng thái mọi thiết bị được đọc một lần đầu mỗi frame và giữ nguyên trong suốt frame đó, nên mọi system trong một frame thấy cùng một kết quả. Mọi nút (phím, chuột, tay cầm) dùng chung một quy tắc, ba hàm cho ba câu hỏi:

Hàm Đúng khi
*_pressed Nút vừa được nhấn ở frame này (chỉ một frame)
*_held Nút đã được giữ từ frame trước và vẫn đang giữ
*_released Nút vừa được thả ở frame này (chỉ một frame)

Ví dụ một lần nhấn giữ 3 frame rồi thả:

Frame Người dùng pressed held released
1 chưa ấn – – –
2 nhấn xuống ✔ – –
3 vẫn giữ – ✔ –
4 vẫn giữ – ✔ –
5 thả ra – – ✔
Ghi chú
Một phím được nhấn rồi nhả giữa hai frame (gõ cực nhanh) vẫn được tính: *_pressed đúng ở frame đó và *_released đúng ở frame sau.
Lưu ý
Ở frame nhấn đầu tiên (frame 2), *_held trả về false. Muốn biết "nút đang được ấn", dùng *_pressed(...) || *_held(...).

Bàn phím

njin::key_pressed(), njin::key_held(), njin::key_released() nhận một njin::key_code: chữ cái, số, F1 đến F12, mũi tên, phím điều hướng, ký hiệu, phím điều khiển (Shift, Ctrl, Alt, Windows) và bàn phím số.

Chuột

Hàm Trả về
njin::mouse_pos() Vị trí con trỏ, pixel màn hình
njin::mouse_delta() Độ dời so với frame trước
njin::mouse_wheel() Số nấc cuộn, dương là cuộn ra xa người dùng. Đa số frame là 0
njin::mouse_pressed() / njin::mouse_held() / njin::mouse_released() Nút chuột (njin::mouse_button), cùng quy tắc như phím
Ghi chú
njin::mouse_pos() là pixel màn hình, không qua camera. Đổi sang vị trí trong thế giới bằng njin::scr2w(): xem Camera.

Tay cầm

Tay cầm được đánh số từ 0 đến njin::gamepad_max - 1. Tay cầm chưa cắm không gây lỗi: mọi hàm đọc như không nhấn và trả về 0.

Hàm Việc làm
njin::pad_available() Tay cầm có đang cắm không
njin::pad_pressed() / njin::pad_held() / njin::pad_released() Nút tay cầm (njin::gamepad_button)
njin::pad_axis() Trục analog (njin::gamepad_axis), từ -1 đến 1
njin::pad_set_deadzone() Vùng chết của cần, mặc định 0.15
  • Tên nút mặt theo vị trí: pad_face_down là A trên Xbox và Cross trên PlayStation.
  • Giá trị trục đã qua vùng chết và được co giãn lại: vừa qua vùng chết thì đọc gần 0 và đạt 1 ở mép, nên không bị nhảy bậc.
  • Cò (pad_axis_left_trigger, pad_axis_right_trigger) nghỉ ở -1 và đọc 1 khi nhấn hết, theo quy ước của raylib.

Nhập văn bản

Gõ chữ và bấm phím tắt là hai câu hỏi khác nhau. njin::key_pressed() cho biết phím nào được nhấn; njin::text_count() và njin::text_char() cho biết ký tự nào được gõ, đã qua bố cục bàn phím và phím chết. Ô nhập văn bản cần loại thứ hai:

#include <njin.h>
#include <string>
namespace {
bool menu_open = true;
std::string typed;
// Nhập văn bản: dùng text_count/text_char, không dùng key_pressed. Ký tự đã qua
// bố cục bàn phím, nên gõ dấu tiếng Việt hay chữ hoa vẫn đúng.
void read_text(njin::context &ctx) {
for (njin::i32 i = 0; i < njin::text_count(ctx); i++) {
const njin::i32 codepoint = njin::text_char(ctx, i);
if (codepoint >= 32 && codepoint < 127) // ví dụ chỉ nhận ASCII
typed.push_back((char)codepoint);
}
if (njin::key_pressed(ctx, njin::key_backspace) && !typed.empty())
typed.pop_back();
NJIN_INFO("đã gõ: %s", typed.c_str());
typed.clear();
}
}
// Menu chạy TRƯỚC thế giới và nuốt cú nhấp, để thế giới bên dưới không nhận
// cùng cú nhấp đó.
void menu_click(njin::context &ctx) {
if (menu_open && njin::mouse_pressed(ctx, njin::mouse_left)) {
NJIN_INFO("menu nhận cú nhấp");
}
}
void world_click(njin::context &ctx) {
// Khi menu đã nuốt cú nhấp, hàm này trả về false.
NJIN_INFO("thế giới nhận cú nhấp");
}
void setup(njin::context &ctx) {
}
} // namespace
njin::mod_desc text_consume_module() {
return {.name = "text_consume", .setup = setup};
}
bool mouse_pressed(const context &ctx, mouse_button button)
Đúng ở frame mà nút chuột vừa được nhấn xuống (chỉ một frame).
i32 text_count(const context &ctx)
Số ký tự đã gõ trong frame này.
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).
void mouse_consume(context &ctx, mouse_button button)
Xóa trạng thái của một nút chuột trong phần còn lại của frame.
i32 text_char(const context &ctx, i32 index)
Mã Unicode của ký tự thứ index đã gõ trong frame này, theo thứ tự gõ.
@ mouse_left
Chuột trái.
Definition _types.h:475
@ key_enter
Enter.
Definition _types.h:412
@ key_backspace
Backspace.
Definition _types.h:415
#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.
@ phase_update
Mỗi frame, logic chính của game.
Definition _mod.h:35
@ phase_pre_update
Mỗi frame, trước khi cập nhật.
Definition _mod.h:33
std::int32_t i32
Số nguyên có dấu 32 bit.
Definition _types.h:14
Mô tả một module: một nhóm system có tên.
Definition _mod.h:67

Mã trả về là Unicode. Mỗi frame nhận tối đa 32 ký tự.

Consume: nuốt input

njin::key_consume(), njin::mouse_consume() và njin::mouse_wheel_consume() xóa trạng thái của một nút trong phần còn lại của frame. System chạy sau đó không thấy nút đó nữa. Dùng khi một menu cần nuốt cú nhấp trước khi thế giới bên dưới xử lý cùng cú nhấp đó (xem ví dụ trên).

Hiệu lực không kéo dài quá frame gọi nó, vì trạng thái được đọc lại ở frame sau. Consume cũng không làm sai lịch sử của nút: một phím đang giữ bị consume thì ở frame sau vẫn là "đang giữ", không bị tính là vừa nhấn lại. Muốn system menu chạy trước, đặt nó ở phase sớm hơn (phase_pre_update) hoặc dùng thứ tự after/before: xem Module, system và phase.

Action

Action là một tên logic (ví dụ "fire") gắn với một hoặc nhiều nguồn: phím, nút chuột, nút tay cầm. Nguồn nào thỏa cũng làm action thỏa. Game hỏi về action thay vì phím, nên đổi phím sau này không phải sửa logic.

Hàm Việc làm
njin::action_define() Tạo action và gắn mọi nguồn trong một dòng: action_define(ctx, "jump", {key_space, key_w, pad_face_down}). Cách nên dùng
njin::action_register() Tạo action trống. Nếu tên đã có thì trả về action cũ
njin::action_find() Tìm action theo tên
njin::action_bind_key() Gắn thêm một phím
njin::action_bind_mouse() Gắn thêm một nút chuột
njin::action_bind_pad() Gắn thêm một nút tay cầm (bất kỳ tay cầm nào đang cắm)
njin::action_clear_binds() Xóa mọi nguồn đã gắn. Dùng được ở mọi lúc
njin::action_pressed() / njin::action_held() / njin::action_released() Cùng quy tắc như phím

Đổi phím trong lúc chạy là gọi njin::action_clear_binds() rồi bind lại.

Axis

Axis đọc giá trị từ -1 đến 1. Nó tách khỏi action vì hai thứ trả lời hai câu hỏi khác nhau: action là đúng hoặc sai, axis là mức độ.

Hàm Việc làm
njin::axis_define() Tạo axis và gắn mọi cặp phím, trục tay cầm trong một dòng: axis_define(ctx, "move", {{key_left, key_right}, {key_a, key_d}}, {pad_axis_left_x}). Cách nên dùng
njin::axis_register() / njin::axis_find() Tạo axis trống, và tìm axis theo tên
njin::axis_bind_keys() Gắn một cặp phím: chỉ phím âm thì -1, chỉ phím dương thì 1, cả hai hoặc không phím nào thì 0
njin::axis_bind_pad() Gắn một trục tay cầm
njin::axis_clear_binds() Xóa mọi nguồn đã gắn
njin::axis_value() Giá trị hiện tại

Nguồn lệch xa vị trí nghỉ nhất thì thắng: cần đẩy nửa chừng không bị cặp phím đang đứng yên làm phẳng, và cặp phím đang giữ không bị cần không ai chạm chặn lại.

Không có axis dạng vector 2 chiều. Đọc hai axis rồi ghép lại: chỉ bạn biết có cần chuẩn hóa vector đó hay không (đi chéo có nhanh hơn không).

Ví dụ hoàn chỉnh

Di chuyển bằng phím hoặc cần trái, bắn bằng phím cách, chuột trái hoặc nút A:

#include <njin.h>
namespace {
void bind(njin::context &ctx) {
// Một axis có thể nhận cả cặp phím lẫn cần analog. Nguồn nào lệch xa vị trí
// nghỉ nhất thì thắng.
move_x = njin::axis_register(ctx, "move_x");
move_y = njin::axis_register(ctx, "move_y");
// Một action nhận cả phím, nút chuột và nút tay cầm.
fire = njin::action_register(ctx, "fire");
// Vùng chết của cần analog, mặc định 0.15.
}
void steer(njin::context &ctx) {
// Hai axis đọc cùng nhau là một vector. Có chuẩn hóa hay không do bạn quyết.
const njin::vec2 direction{njin::axis_value(ctx, move_x),
njin::axis_value(ctx, move_y)};
(void)direction;
if (njin::action_pressed(ctx, fire)) {
// Chuột tính bằng pixel màn hình; scr2w đổi sang vị trí trong thế giới.
const njin::vec2 target = njin::scr2w(ctx, njin::mouse_pos(ctx));
NJIN_INFO("bắn tới %.0f, %.0f", target.x, target.y);
}
// Nút và cần của một tay cầm cụ thể.
if (njin::pad_available(ctx, 0)) {
NJIN_INFO("Start");
(void)trigger; // -1 khi nghỉ, 1 khi nhấn hết
}
// Bánh xe chuột: dương là cuộn ra xa người dùng.
const njin::f32 wheel = njin::mouse_wheel(ctx);
if (wheel != 0.0f)
NJIN_INFO("cuộn %.1f nấc", wheel);
}
void setup(njin::context &ctx) {
}
} // namespace
njin::mod_desc input_devices_module() {
return {.name = "input_devices", .setup = setup};
}
vec2 scr2w(const context &ctx, vec2 pos)
Đổi một điểm từ pixel màn hình sang thế giới, qua camera_active().
void axis_bind_keys(context &ctx, axis_handle handle, key_code negative, key_code positive)
Gắn một cặp phím vào axis, đóng vai một cần analog.
bool pad_available(const context &ctx, i32 pad)
Tay cầm số pad có đang được cắm không.
f32 mouse_wheel(const context &ctx)
Số nấc cuộn bánh xe chuột trong frame này.
void action_bind_mouse(context &ctx, action_handle handle, mouse_button button)
Gắn một nút chuột vào action.
void action_bind_pad(context &ctx, action_handle handle, gamepad_button button)
Gắn một nút tay cầm vào action.
axis_handle axis_register(context &ctx, const char *name)
Đăng ký một axis có tên.
f32 pad_axis(const context &ctx, i32 pad, gamepad_axis axis)
Giá trị một trục analog của tay cầm.
bool pad_pressed(const context &ctx, i32 pad, gamepad_button button)
Đúng ở frame mà nút tay cầm vừa được nhấn xuống (chỉ một frame).
void pad_set_deadzone(context &ctx, f32 deadzone)
Đặt vùng chết cho mọi cần trên mọi tay cầm.
void action_bind_key(context &ctx, action_handle handle, key_code key)
Gắn một phím vào action.
f32 axis_value(const context &ctx, axis_handle handle)
Giá trị hiện tại của axis.
bool action_pressed(const context &ctx, action_handle handle)
Đúng ở frame mà một trong các nguồn của action (phím, nút chuột, nút tay cầm) vừa được nhấn.
vec2 mouse_pos(const context &ctx)
Vị trí con trỏ chuột, tính bằng pixel màn hình.
action_handle action_register(context &ctx, const char *name)
Đăng ký một action có tên.
void axis_bind_pad(context &ctx, axis_handle handle, gamepad_axis axis)
Gắn một trục tay cầm vào axis.
@ pad_face_down
Nút mặt dưới (A trên Xbox, Chéo trên PlayStation).
Definition _types.h:497
@ pad_start
Nút Start / Menu.
Definition _types.h:505
@ pad_axis_left_x
Cần trái, trục ngang (-1 là trái, 1 là phải).
Definition _types.h:513
@ pad_axis_left_y
Cần trái, trục dọc (-1 là lên, 1 là xuống).
Definition _types.h:514
@ pad_axis_right_trigger
Cò phải. Nghỉ ở -1, nhấn hết đọc 1.
Definition _types.h:518
@ key_space
Phím cách.
Definition _types.h:411
@ key_s
Phím S.
Definition _types.h:362
@ key_d
Phím D.
Definition _types.h:347
@ key_a
Phím A.
Definition _types.h:344
@ key_w
Phím W.
Definition _types.h:366
@ phase_startup
Một lần, trước frame đầu tiên.
Definition _mod.h:32
float f32
Số thực 32 bit.
Definition _types.h:28
Định danh của một action nhập liệu, tạo bởi action_register().
Definition _types.h:85
Định danh của một axis nhập liệu, tạo bởi axis_register().
Definition _types.h:93
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

Xem thêm ví dụ action đơn giản hơn:

#include <njin.h>
namespace {
void bind_keys(njin::context &ctx) {
// Một action có thể có nhiều phím.
move_left = njin::action_register(ctx, "move_left");
move_right = njin::action_register(ctx, "move_right");
}
// Phím đang được ấn = vừa nhấn xuống (pressed) hoặc đã giữ từ frame trước (held).
bool is_down(const njin::context &ctx, njin::action_handle action) {
return njin::action_pressed(ctx, action) || njin::action_held(ctx, action);
}
void steer(njin::context &ctx) {
njin::f32 direction = 0.0f;
if (is_down(ctx, move_left))
direction -= 1.0f;
if (is_down(ctx, move_right))
direction += 1.0f;
NJIN_INFO("nhảy! hướng hiện tại: %.0f", direction);
}
void setup(njin::context &ctx) {
}
} // namespace
njin::mod_desc input_demo_module() {
return {.name = "input_demo", .setup = setup};
}
bool action_held(const context &ctx, action_handle handle)
Đúng khi một trong các nguồn của action đã được giữ từ frame trước và vẫn đang giữ.
@ key_right
Mũi tên phải.
Definition _types.h:401
@ key_left
Mũi tên trái.
Definition _types.h:400

Danh sách phím và nút

Mọi mã nằm trong njin::key_code, njin::mouse_button, njin::gamepad_button và njin::gamepad_axis.