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ân vật nhảy được trong 50 dòng

Trang này dựng một nhân vật platformer chạy và nhảy được trên một bản đồ ô vuông, với dưới 50 dòng code (45 dòng, không tính dòng trống và dòng chú thích). Nó dùng ba thứ: njin::tilemap làm mặt đất, njin::platformer_body làm nhân vật, và njin::platformer_input_map nối phím vào nhân vật.

#include <njin.h>
namespace {
using namespace njin;
void startup(context &ctx) {
// 1. Phím: một axis cho trái/phải, một action cho nhảy.
const axis_handle move = axis_define(ctx, "move", {{key_left, key_right}});
const action_handle jump = action_define(ctx, "jump", {key_space, pad_face_down});
entt::registry &reg = world(ctx);
// 2. Bản đồ: một dải đất và một bục. Đặt ô ở đâu, bản đồ mở rộng tới đó.
tilemap map;
map.tileset = texture_load(ctx, "assets/tiles.png");
texture_set_filter(ctx, map.tileset, filter_nearest); // pixel art: không làm mờ
for (i32 x = -10; x < 40; x++) {
tilemap_set(map, x, 10, 0); // mặt đất có cỏ
tilemap_set(map, x, 11, 1); // đất bên dưới
}
for (i32 x = 8; x < 12; x++)
tilemap_set(map, x, 8, 3); // bục
tilemap_set_shape(map, 3, tile_one_way); // nhảy xuyên từ dưới lên được
const entt::entity level = reg.create();
reg.emplace<transform>(level);
reg.emplace<tilemap>(level, std::move(map));
reg.emplace<collider>(level, collider{.shape = collider_tiles}); // ô thành vật cản
// 3. Nhân vật: transform + collider hộp + platformer_body.
const entt::entity player = reg.create();
reg.emplace<transform>(player, transform{.pos = {40.0f, 100.0f}}); // pos là chân
reg.emplace<collider>(player, collider{.size = {10.0f, 14.0f}, .offset = {0.0f, -7.0f}});
reg.emplace<platformer_body>(player);
reg.emplace<platformer_input_map>(player, platformer_input_map{.move = move, .jump = jump});
// 4. Camera bám theo nhân vật.
reg.emplace<camera_follow>(camera_spawn(ctx, 3.0f), camera_follow{.target = player});
}
// Chưa có sprite: vẽ nhân vật bằng một hình chữ nhật.
void draw_player(context &ctx) {
world(ctx).view<transform, collider, platformer_body>().each(
[&](const transform &tr, const collider &col, const platformer_body &) {
});
}
void setup(context &ctx) {
ecs_register(ctx, phase_startup, startup);
ecs_register(ctx, phase_render, draw_player);
}
} // namespace
int main() {
context *ctx = create({.title = "Nhảy", .width = 960, .height = 540, .target_fps = 60});
mod_register(*ctx, {.name = "game", .setup = setup});
run(*ctx);
destroy(ctx);
}
entt::entity camera_spawn(context &ctx, f32 zoom=1.0f, vec2 pos={})
Tạo một entity camera đang dùng (transform, njin::camera_2d, njin::camera_on) nhìn vào pos,...
@ collider_tiles
Mọi ô không trống của njin::tilemap trên cùng entity là vật cản.
Definition njin_collision.h:29
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ổ.
void draw_rect(const context &ctx, rect r, rgba color)
Vẽ hình chữ nhật đặc.
const char * tr(const context &ctx, const char *key)
Chuỗi đã dịch của key trong ngôn ngữ đang dùng.
action_handle action_define(context &ctx, const char *name, std::initializer_list< binding > sources)
Đăng ký một action và gắn mọi nguồn của nó trong một lời gọi.
Definition njin_bindings.h:102
axis_handle axis_define(context &ctx, const char *name, std::initializer_list< axis_keys > keys, std::initializer_list< gamepad_axis > pads={})
Đăng ký một axis và gắn mọi cặp phím và trục tay cầm của nó trong một lời gọi.
Definition njin_bindings.h:129
@ pad_face_down
Nút mặt dưới (A trên Xbox, Chéo trên PlayStation).
Definition _types.h:497
@ key_space
Phím cách.
Definition _types.h:411
@ key_right
Mũi tên phải.
Definition _types.h:401
@ key_left
Mũi tên trái.
Definition _types.h:400
constexpr rect rect_from_center(vec2 center, vec2 size)
Tạo hình chữ nhật từ tâm và kích thước.
Definition _math.h:228
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
@ phase_render
Mỗi frame, vẽ trong không gian thế giới.
Definition _mod.h:38
void texture_set_filter(context &ctx, texture_handle handle, texture_filter filter)
Đổi cách lấy mẫu của texture.
texture_handle texture_load(context &ctx, const char *path)
Nạp một texture từ file ảnh.
@ filter_nearest
Giữ nguyên từng pixel, sắc cạnh. Dùng cho pixel art.
Definition njin_draw.h:198
void tilemap_set(tilemap &map, i32 x, i32 y, i32 id)
Đặt ô tại cột x, hàng y.
Definition _tilemap.h:265
void tilemap_set_shape(tilemap &map, i32 id, tile_shape shape)
Đặt hình va chạm cho mọi ô có số thứ tự id trong tilemap.
Definition _tilemap.h:150
@ tile_one_way
Bục một chiều: chỉ đỡ từ trên xuống, nhảy xuyên từ dưới lên được.
Definition _tilemap.h:58
std::int32_t i32
Số nguyên có dấu 32 bit.
Definition _types.h:14
constexpr rgba yellow
Vàng.
Definition _math.h:239
Đị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
Cho camera bám theo một entity: mượt, có vùng chết, nhìn trước theo hướng chạy, và không bao giờ lộ r...
Definition njin_camera.h:31
Hình va chạm của một entity.
Definition njin_collision.h:46
vec2 offset
Độ lệch của tâm so với transform.pos.
Definition njin_collision.h:50
vec2 size
Kích thước hình hộp.
Definition njin_collision.h:48
Nhân vật platformer: chạy, nhảy, rơi, trèo dốc, đứng trên bục di chuyển.
Definition njin_body.h:44
Gắn action và axis vào một njin::platformer_body: module body đọc chúng trong phase_pre_update và ghi...
Definition njin_body.h:119
Lưới ô vuông không giới hạn kích thước, vẽ từ một tileset, chia thành chunk.
Definition _tilemap.h:128
texture_handle tileset
Ảnh tileset.
Definition _tilemap.h:129
Vị trí, góc xoay và tỉ lệ của một entity trong thế giới.
Definition _comps.h:14

Bấm mũi tên trái/phải để chạy, Space để nhảy. Nhân vật rơi xuống đất, chạy được, nhảy được, và nhảy xuyên từ dưới lên bục rồi đứng lên trên nó.

Chạy sang phải, giữ Space để nhảy xuyên bục rồi đứng lên trên nó (hình chữ nhật vàng là nhân vật)

Giữ Space thì nhảy cao, nhả sớm thì nhảy thấp: đây là jump_cut của njin::platformer_body. Vì vậy chỉ gõ nhẹ Space thì nhân vật không lên nổi bục cao 32 pixel.

Chuẩn bị

Cần một ảnh tileset gồm các ô 16 x 16 tại assets/tiles.png. Dùng luôn ảnh của game mẫu: src/games/platformer/assets/tiles.png (ô 0 là đất có cỏ, ô 1 là đất, ô 3 là bục). Thư mục assets phải nằm cạnh file exe: gọi njin_add_assets(my_game assets) trong CMake để nó được copy mỗi lần build (xem Bắt đầu và Cửa sổ, file và lưu game).

Từng bước

1. Phím: axis và action

const njin::axis_handle move = njin::axis_define(ctx, "move", {{njin::key_left, njin::key_right}});
const njin::action_handle jump = njin::action_define(ctx, "jump", {njin::key_space, njin::pad_face_down});

Một axis là một trục từ -1 đến 1, ở đây gộp hai phím thành trục ngang. Một action là một tên logic ("jump") gắn với một hoặc nhiều nguồn: phím, nút chuột, nút tay cầm. Game không hỏi "phím Space có đang bấm không" mà hỏi "action `jump` có đang bấm không", nên sau này đổi phím hay thêm tay cầm chỉ là thêm một phần tử vào danh sách. Muốn thêm phím A/D cho axis: {{key_left, key_right}, {key_a, key_d}}, và thêm cần analog: njin::axis_define(ctx, "move", {...}, {njin::pad_axis_left_x}). Xem Nhập liệu.

2. Bản đồ: njin::tilemap

njin::tilemap_set(map, x, y, id) đặt ô (x, y) thành ô số id của tileset. Không cần khai báo kích thước bản đồ: đặt ô ở đâu, bản đồ mở rộng tới đó. Ô có thể ở tọa độ âm.

Bản đồ cần hai thứ nữa để nhân vật đứng lên được:

  • Gắn một njin::collider có shape = collider_tiles lên entity của bản đồ. Khi đó mọi ô không trống thành vật cản.
  • njin::tilemap_set_shape() cho ô số 3 hình tile_one_way: một bục một chiều, chỉ đỡ từ trên xuống, còn nhảy từ dưới lên thì xuyên qua. Hình va chạm đặt theo số thứ tự ô nên áp dụng cho mọi ô loại đó. Ngoài ra còn có dốc, xem Làm game platformer.

transform của entity là góc trên trái của ô (0, 0), nên ở đây ô hàng 10 có mặt trên nằm ở y = 160.

3. Nhân vật: njin::platformer_body

Nhân vật là một entity có ba component:

Component Vai trò
njin::transform Vị trí. Với nhân vật này, pos là chân vì offset của collider là {0, -7}
njin::collider Hộp va chạm 10 x 14
njin::platformer_body Chạy, nhảy, rơi, ghi lại grounded...

platformer_body{} với mọi giá trị mặc định đã cho một cú nhảy tốt: có coyote time (rời mép vẫn nhảy được một chút), jump buffer (bấm nhảy sớm một chút vẫn được tính), và nhảy thấp khi thả nút sớm. Engine di chuyển nó trong phase_fixed_update nên kết quả không phụ thuộc FPS.

njin::platformer_input_map nối axis move và action jump vào body: engine đọc chúng mỗi frame và ghi body.input giúp bạn. Không cần viết một system nào để xử lý phím.

4. Camera: njin::camera_follow

njin::camera_spawn(ctx, 3.0f) tạo camera phóng to 3 lần (hợp với pixel art), và njin::camera_follow cho nó bám theo target. Xem Camera.

5. Vẽ nhân vật

Chưa có sprite, nên nhân vật là một hình chữ nhật vàng vẽ trong phase_render (không gian thế giới, đi qua camera). njin::rect_from_center() đổi tâm và kích thước thành hình chữ nhật. Khi có ảnh nhân vật, đổi thành njin::sprite và njin::sprite_anim, xem Sprite và animation.

Chỉnh cảm giác nhảy

Mọi thông số nằm ngay trên njin::platformer_body. Tạo body, sửa, rồi emplace:

njin::platformer_body body{};
body.jump_speed = 350.0f; // nhảy cao hơn
body.air_jumps = 1; // nhảy đôi
body.wall_slide_speed = 55.0f; // trượt tường
body.wall_jump = {150.0f, 300.0f}; // nhảy khỏi tường
reg.emplace<njin::platformer_body>(player, body);

Độ cao cú nhảy xấp xỉ jump_speed² / (2 * gravity): với giá trị mặc định (300 và 1000) là chừng 45 pixel, gần ba ô (đo thực tế được 42 pixel vì vật lý đi theo nhịp rời rạc). Bục ở hàng 8 cao hơn mặt đất 32 pixel nên nhảy lên được; đặt nó cao hơn 42 pixel thì phải tăng jump_speed.

Bước tiếp theo