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 đi 8 hướng trong 50 dòng

Bản top-down của Nhân vật nhảy được trong 50 dòng : một nhân vật nhìn từ trên xuống, đi 8 hướng, lướt, va vào tường đá, với dưới 50 dòng code (46 dòng, không tính dòng trống và dòng chú thích). Nó dùng njin::tilemap làm bản đồ, njin::topdown_body làm nhân vật, và njin::topdown_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: hai axis (ngang, dọc) và một action lướt.
const axis_handle move_x = axis_define(ctx, "move_x", {{key_left, key_right}, {key_a, key_d}}, {pad_axis_left_x});
const axis_handle move_y = axis_define(ctx, "move_y", {{key_up, key_down}, {key_w, key_s}}, {pad_axis_left_y});
const action_handle dash = action_define(ctx, "dash", {key_space, pad_face_down});
entt::registry &reg = world(ctx);
// 2. Bản đồ 20 x 12 ô: sàn cỏ, tường đá bao quanh và một đoạn tường ở giữa.
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 y = 0; y < 12; y++)
for (i32 x = 0; x < 20; x++)
tilemap_set(map, x, y, (x == 0 || y == 0 || x == 19 || y == 11) ? 7 : 0);
for (i32 x = 6; x < 10; x++)
tilemap_set(map, x, 5, 7);
tilemap_set_shape(map, 0, tile_none); // cỏ chỉ để nhìn, đi qua đượ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 + topdown_body. pos là chân.
const entt::entity player = reg.create();
reg.emplace<transform>(player, transform{.pos = {48.0f, 48.0f}});
reg.emplace<collider>(player, collider{.size = {10.0f, 8.0f}, .offset = {0.0f, -4.0f}});
reg.emplace<topdown_body>(player, topdown_body{.dash_speed = 230.0f}); // 0 là tắt lướt
reg.emplace<topdown_input_map>(player, topdown_input_map{move_x, move_y, dash});
// 4. Camera bám nhân vật và không bao giờ lộ ra ngoài bản đồ (320 x 192 pixel).
const rect bounds{{0.0f, 0.0f}, {320.0f, 192.0f}};
reg.emplace<camera_follow>(camera_spawn(ctx, 3.0f), camera_follow{.target = player, .bounds = bounds});
}
// 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, topdown_body>().each(
[&](const transform &tr, const collider &col, const topdown_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 = "Đi bộ", .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
@ 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
@ key_space
Phím cách.
Definition _types.h:411
@ key_right
Mũi tên phải.
Definition _types.h:401
@ key_up
Mũi tên lên.
Definition _types.h:398
@ 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
@ key_down
Mũi tên xuống.
Definition _types.h:399
@ 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_none
Không va chạm: chỉ để nhìn, dù nằm trong layer vật cản.
Definition _tilemap.h:57
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
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
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
Nhân vật top-down: đi 8 hướng (hoặc mọi hướng với cần analog), trượt dọc tường, lướt.
Definition njin_body.h:97
Gắn action và axis vào một njin::topdown_body, như njin::platformer_input_map.
Definition njin_body.h:126
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 hoặc WASD để đi, Space để lướt. Nhân vật trượt dọc tường đá thay vì dính vào, và camera không bao giờ lộ ra ngoài bản đồ.

Đi chéo, lướt (giữ Space khi đang đi), rồi đi xuống. Hình chữ nhật vàng là nhân vật, camera bám theo

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/topdown/assets/tiles.png (ô 0 là cỏ, ô 7 là tường đá). Thư mục assets phải nằm cạnh file exe: gọi njin_add_assets(my_game assets) trong CMake (xem Bắt đầu).

Từng bước

1. Phím: hai axis và một action

const njin::axis_handle move_x = njin::axis_define(ctx, "move_x", {{njin::key_left, njin::key_right}, {njin::key_a, njin::key_d}});
const njin::axis_handle move_y = njin::axis_define(ctx, "move_y", {{njin::key_up, njin::key_down}, {njin::key_w, njin::key_s}});
const njin::action_handle dash = njin::action_define(ctx, "dash", {njin::key_space, njin::pad_face_down});

Top-down cần hai axis, ngang và dọc, vì engine không có axis hai chiều. Mỗi axis nhận nhiều cặp phím (mũi tên và WASD cùng chạy) và một trục tay cầm. Xem Nhập liệu.

2. Bản đồ: cỏ để đi, đá để chặn

Vòng for lồng nhau đặt cả bản đồ 20 x 12 ô là cỏ (ô 0), trừ đường viền là đá (ô 7). Ba việc để đá chặn đường còn cỏ thì không:

  • Gắn njin::collider với shape = collider_tiles lên entity của bản đồ: mọi ô không trống thành vật cản.
  • njin::tilemap_set_shape() cho ô số 0 hình tile_none: cỏ chỉ để nhìn, đi xuyên qua được.
  • Ô đá giữ hình mặc định (tile_solid), nên chặn mọi phía.

Cùng một bản đồ phục vụ cả hai việc (vẽ và va chạm) nên không cần một layer riêng cho tường. Đọc thêm ở Tilemap.

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

Component Vai trò
njin::transform Vị trí. pos là chân vì offset của collider là {0, -4}
njin::collider Hộp va chạm 10 x 8, nhỏ và thấp để nhân vật đi sát tường mà không kẹt
njin::topdown_body Đi, tăng tốc, giảm tốc, lướt

Engine di chuyển nó trong phase_fixed_update bằng njin::collision_move(). Hai điều đáng biết:

  • Đi chéo không nhanh hơn: độ dài của hướng đi lớn hơn 1 được đưa về 1, kể cả khi bạn cộng hai axis.
  • Lướt mặc định tắt. dash_speed = 230.0f bật nó; dash_time và dash_cooldown chỉnh độ dài và thời gian hồi.

njin::topdown_input_map đọc hai axis và action giúp bạn: không cần system nào để xử lý phím.

4. Camera: giới hạn trong bản đồ

njin::camera_follow bám theo target. bounds là vùng thế giới mà khung nhìn không được vượt ra ngoài; ở đây là cả bản đồ, 320 x 192 pixel. Khi nạp bản đồ từ Tiled hoặc LDtk thì lấy thẳng từ njin::level_bounds() (xem Level từ Tiled và LDtk) thay vì tự tính.

Chỉnh cảm giác

njin::topdown_body body{};
body.speed = 78.0f; // đi chậm hơn
body.accel = 600.0f; // trượt lâu hơn khi bắt đầu đi
body.decel = 600.0f; // trượt lâu hơn khi thả phím
body.dash_speed = 230.0f;
body.dash_time = 0.2f; // lướt dài hơn

accel và decel lớn (mặc định 900 và 1300) là nhân vật dừng và chạy tức thì; nhỏ là cảm giác trơn như đi trên băng.

Bước tiếp theo