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
Làm game platformer

Trang này ghép các phần có sẵn của engine thành một nhân vật platformer: chạy, nhảy có cảm giác tốt, dốc, bục một chiều, bục di chuyển, và camera bám theo. Game mẫu njin_platformer (Game mẫu và đóng gói) dùng đúng những thứ này. Chưa làm gì bao giờ? Bắt đầu với Nhân vật nhảy được trong 50 dòng : một nhân vật nhảy được trong 50 dòng.

Game mẫu njin_platformer: chạy, nhảy trên dốc, nhắc "E" khi lại gần con cú, bụi khi tiếp đất
#include <njin.h>
namespace {
// Layer để phân biệt người chơi với tường và vật thể trong game.
constexpr njin::u32 layer_player = njin::layer_bit(1);
struct game {
njin::action_handle jump, down;
entt::entity player = entt::null;
} g;
void startup(njin::context &ctx) {
g.down = njin::action_define(ctx, "down", {njin::key_down});
// Màn Tiled: ô có thuộc tính collision = one_way / slope_r / ... (xem trang này).
g.level = njin::level_load(ctx, "assets/level1.tmx");
entt::registry &reg = njin::world(ctx);
g.player = reg.create();
reg.emplace<njin::transform>(g.player, njin::transform{.pos = {64.0f, 100.0f}});
reg.emplace<njin::collider>(g.player, njin::collider{.size = {10.0f, 14.0f},
.offset = {0.0f, -7.0f},
.layer = layer_player});
// Cảm giác nhảy chỉnh ngay trên component.
body.jump_speed = 350.0f;
body.air_jumps = 1; // nhảy đôi
body.wall_slide_speed = 55.0f; // trượt tường
body.wall_jump = {150.0f, 300.0f};
reg.emplace<njin::platformer_body>(g.player, body);
// Nối action với body: engine tự ghi input mỗi frame.
reg.emplace<njin::platformer_input_map>(g.player, njin::platformer_input_map{g.move, g.jump, g.down});
// Camera bám nhân vật, có vùng chết, nhìn trước, và không lộ ra ngoài màn.
const entt::entity camera = njin::camera_spawn(ctx, 2.0f);
reg.emplace<njin::camera_follow>(camera, njin::camera_follow{.target = g.player,
.deadzone = {24.0f, 40.0f},
.lookahead = {36.0f, 0.0f},
.bounds = njin::level_bounds(ctx, g.level),
.pixel_snap = true});
}
// Rơi mạnh thì rung màn hình: nghe sự kiện body_landed.
void on_landed(const njin::body_landed &e) { (void)e; }
void setup(njin::context &ctx) {
njin::events(ctx).sink<njin::body_landed>().connect<&on_landed>();
}
} // namespace
int main() {
njin::context *ctx = njin::create({.title = "Platformer", .width = 1280, .height = 720, .target_fps = 60});
njin::mod_register(*ctx, {.name = "game", .setup = setup});
njin::run(*ctx);
}
rect level_bounds(const context &ctx, level_handle level)
Khung của một level trong thế giới, dùng cho camera_follow::bounds.
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,...
constexpr u32 layer_bit(i32 n)
Bit của lớp va chạm thứ n (0..31), để đặt tên lớp cho dễ đọc: constexpr u32 layer_enemy = njin::layer...
Definition njin_collision.h:21
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ổ.
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
@ pad_axis_left_x
Cần trái, trục ngang (-1 là trái, 1 là phải).
Definition _types.h:513
@ key_space
Phím cách.
Definition _types.h:411
@ key_right
Mũi tên phải.
Definition _types.h:401
@ key_down
Mũi tên xuống.
Definition _types.h:399
@ key_left
Mũi tên trái.
Definition _types.h:400
level_handle level_load(context &ctx, const char *path, const level_desc &desc={})
Nạp một bản đồ và tạo entity cho nó.
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.
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
std::uint32_t u32
Số nguyên không dấu 32 bit.
Definition _types.h:22
Đị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
Event: một njin::platformer_body vừa chạm đất.
Definition njin_body.h:159
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
Định danh của một level đã nạp, trả về từ level_load().
Definition njin_level.h:20
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
f32 jump_speed
Vận tốc lên lúc bắt đầu nhảy.
Definition njin_body.h:54
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
Vị trí, góc xoay và tỉ lệ của một entity trong thế giới.
Definition _comps.h:14

Nhân vật: njin::platformer_body

Gắn njin::platformer_body cạnh một njin::transform và một njin::collider hộp. Engine di chuyển nó trong phase_fixed_update bằng collision_move(), rồi ghi lại trạng thái (grounded, on_wall, velocity...). Game chỉ cần cho nó input và đọc trạng thái để chọn animation.

Các cảm giác quen thuộc đều có sẵn, chỉnh bằng số ngay trên component:

Cảm giác Trường
Coyote time: rời mép rồi vẫn nhảy được coyote_time
Jump buffer: bấm nhảy sớm trước khi chạm đất jump_buffer
Thả nút nhảy sớm thì nhảy thấp jump_cut
Rơi nhanh hơn lúc lên gravity, fall_gravity, max_fall
Nhảy đôi air_jumps
Trượt tường, nhảy khỏi tường wall_slide_speed, wall_jump
Gia tốc trên đất và trên không ground_accel, air_accel...

Có hai cách đưa input vào:

  • Tự ghi vào body.input mỗi frame (move_x, jump, jump_held, drop). jump là một yêu cầu: engine tự xóa khi đã xử lý, nên bấm ở frame không có nhịp vật lý nào vẫn không bị lỡ.
  • Gắn njin::platformer_input_map với một axis và một action: engine đọc chúng giúp bạn.

Muốn đẩy lùi khi trúng đòn thì ghi thẳng vào velocity.

Nhân vật gửi hai event để game rung màn hình, phát tiếng, tạo bụi: njin::body_jumped và njin::body_landed (kèm tốc độ rơi).

Dốc, bục một chiều, ô không va chạm

Hình va chạm của một ô nằm trong njin::tilemap::shapes, đặt bằng tilemap_set_shape():

njin::tile_shape Việc làm
tile_solid Chặn mọi phía (mặc định)
tile_none Chỉ để nhìn, dù nằm trong layer vật cản
tile_one_way Bục chỉ đỡ từ trên xuống; nhảy xuyên từ dưới lên
tile_slope_r, tile_slope_l Dốc 45 độ, cao bên phải hoặc bên trái
tile_slope_*_low, *_high Dốc 22.5 độ, hai ô liền nhau

Khi nạp từ Tiled, đặt thuộc tính chuỗi collision trên ô trong tileset (one_way, slope_r, slope_l_high, none...). Khi nạp từ LDtk, đặt tên giá trị IntGrid, hoặc gắn enum tag / custom data trên tileset bằng cùng những tên đó. Ô dùng cho animation (nước, đuốc) cũng lấy từ tileset, xem Tilemap.

Nhân vật đi lên dốc mượt, và dính vào mặt dốc khi đi xuống thay vì nảy ra khỏi nó. Nhảy xuống khỏi bục một chiều: giữ phím xuống rồi bấm nhảy (input.drop, hoặc platformer_input_map::down).

collision_move() nhận thêm njin::collision_move_opts nếu bạn tự viết bộ điều khiển: drop_through, snap_down, test_only (dò tường hay mặt đất mà không di chuyển). Kết quả có grounded, ground, on_slope.

Bục di chuyển: njin::path_mover

Một entity có transform, collider hộp (thường one_way = true) và njin::path_mover đi theo một đường gấp khúc lặp lại. Engine dời nó bằng collision_move_platform(), chở theo mọi nhân vật đứng trên nó và đẩy những thứ nó đâm vào. Trong Tiled, vẽ bục bằng một object có polyline; các điểm của polyline là các điểm dừng.

Camera bám theo: njin::camera_follow

const entt::entity cam = njin::camera_spawn(ctx, 2.0f);
reg.emplace<njin::camera_follow>(cam, njin::camera_follow{
.target = player, .deadzone = {24, 40}, .lookahead = {36, 0},
.bounds = njin::level_bounds(ctx, level), .pixel_snap = true});

Camera trễ nhẹ (smoothing), đứng yên khi nhân vật đi trong vùng chết (deadzone), nhìn trước theo hướng chạy (lookahead), và không bao giờ lộ ra ngoài level (bounds; level nhỏ hơn màn hình thì nằm giữa). Với pixel art, bật pixel_snap để các ô không rung một pixel. Sau khi dịch chuyển nhân vật xa, đặt started = false để camera nhảy thẳng tới thay vì trượt qua cả level.

Rung màn hình của njin::camera_shake() cộng thêm lúc vẽ, không ảnh hưởng camera_follow.

Kiểm tra bằng inspector

njin_inspector hiện platformer_body (vận tốc, đang đứng đất, tường, các bộ đếm coyote và buffer) và camera_follow của entity đang chọn. Xem Công cụ debug: njin_inspector.