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 top-down

Nhân vật đi 8 hướng, quái đuổi theo qua các lối đi, cây che nhân vật đúng chỗ. Game mẫu njin_topdown (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 đi 8 hướng trong 50 dòng : một nhân vật đi được trong 50 dòng.

Game mẫu njin_topdown: nhân vật đi và lướt, con slime hồng đuổi theo, hiện nhắc phím tương tác khi lại gần biển báo
#include <njin.h>
#include <vector>
namespace {
constexpr njin::u32 layer_world = njin::layer_bit(0);
constexpr njin::i32 draw_things = 5;
struct chaser {
float repath = 0.0f;
};
struct game {
entt::entity player = entt::null;
} g;
void startup(njin::context &ctx) {
// Mọi thứ đứng trên bản đồ vẽ cùng một lớp, sắp theo y: đi vòng ra sau cây.
njin::draw_set_y_sort(ctx, draw_things, true);
g.level = njin::level_load(ctx, "assets/map.tmx");
// Lưới tìm đường dựng từ các collider (tường, cây, ao) đang có trong thế giới.
g.nav = njin::nav_grid_from_world(ctx, njin::level_bounds(ctx, g.level), {16.0f, 16.0f}, layer_world);
}
// Quái đuổi người chơi: A* mỗi 0.4 giây, rồi đi theo đường.
void chase(njin::context &ctx) {
entt::registry &reg = njin::world(ctx);
const njin::vec2 target = reg.get<njin::transform>(g.player).pos;
for (auto [e, tr, c, body] : reg.view<njin::transform, chaser, njin::topdown_body>().each()) {
c.repath -= njin::delta(ctx);
// Chỉ đuổi khi nhìn thấy người chơi: tia không bị tường chắn.
if (!njin::collision_line_of_sight(ctx, tr.pos, target, layer_world, e))
continue;
if (c.repath <= 0.0f) {
c.repath = 0.4f;
std::vector<njin::vec2> path;
njin::nav_find_path(g.nav, tr.pos, target, path);
c.agent.set(std::move(path));
}
// nav_steer trả hướng đi (độ dài 1); topdown_body lo tốc độ và va chạm.
body.input.move = njin::nav_steer(c.agent, tr.pos);
}
}
void setup(njin::context &ctx) {
}
} // namespace
int main() {
njin::context *ctx = njin::create({.title = "Top-down", .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.
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
bool collision_line_of_sight(const context &ctx, vec2 from, vec2 to, u32 mask=layer_all, entt::entity ignore=entt::null)
Có nhìn thấy nhau không: tia từ from đến to không trúng vật cản nào.
Definition njin_collision.h:256
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_set_y_sort(context &ctx, i32 layer, bool on)
Bật sắp xếp theo Y cho một lớp vẽ: trong lớp đó, sprite và particle có y lớn hơn (thấp hơn trên màn h...
const char * tr(const context &ctx, const char *key)
Chuỗi đã dịch của key trong ngôn ngữ đang dùng.
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::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_fixed_update
Theo nhịp cố định, 0 hoặc nhiều lần mỗi frame. Cho vật lý.
Definition _mod.h:34
bool nav_find_path(const nav_grid &grid, vec2 from, vec2 to, std::vector< vec2 > &out, const nav_path_opts &opts={})
Tìm đường ngắn nhất (A*) từ from đến to.
nav_grid nav_grid_from_world(const context &ctx, rect area, vec2 cell_size, u32 mask=0xFFFFFFFFu)
Dựng lưới phủ area từ vật cản đang có trong thế giới: mọi ô chồng lên một collider không phải trigger...
vec2 nav_steer(nav_agent &agent, vec2 pos)
Hướng cần đi để theo đường, độ dài 1, hoặc {0, 0} khi đã tới cuối.
f32 delta(const context &ctx)
Thời gian của frame trước, tính bằng giây, đã nhân với tốc độ thời gian.
std::int32_t i32
Số nguyên có dấu 32 bit.
Definition _types.h:14
std::uint32_t u32
Số nguyên không dấu 32 bit.
Definition _types.h:22
Định danh của một level đã nạp, trả về từ level_load().
Definition njin_level.h:20
Một con đường đang được đi theo, dùng với nav_steer().
Definition njin_nav.h:132
Lưới tìm đường: mỗi ô có một chi phí để đi vào, 0 là không đi được.
Definition njin_nav.h:18
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
Vị trí, góc xoay và tỉ lệ của một entity trong thế giới.
Definition _comps.h:14
Vector 2 chiều. Dùng cho vị trí, kích thước, độ dời.
Definition _types.h:39

Nhân vật: njin::topdown_body

Gắn njin::topdown_body cạnh transform và một collider (hộp hoặc tròn). Engine di chuyển nó trong phase_fixed_update bằng collision_move(), nên nó trượt dọc tường thay vì dính vào.

  • Ghi hướng đi vào body.input.move. Độ dài lớn hơn 1 được đưa về 1: đi chéo không nhanh hơn đi thẳng, kể cả khi bạn cộng hai axis.
  • speed, accel, decel cho cảm giác trượt hay bám.
  • Lướt: đặt dash_speed lớn hơn 0 rồi ghi input.dash = true (một yêu cầu, tự xóa). dash_time, dash_cooldown chỉnh độ dài và hồi. njin::body_dashed cho tiếng và bụi.
  • facing là hướng nhìn gần nhất khác 0; dùng để chọn hình và để vung kiếm về đúng phía.

njin::topdown_input_map đọc hai axis và một action giúp bạn.

Sắp xếp theo Y

Một cái cây phải che nhân vật đứng sau nó, và bị nhân vật che khi đứng trước. Bật cho một lớp vẽ:

njin::draw_set_y_sort(ctx, 5, true); // mọi sprite lớp 5: y lớn hơn vẽ sau

y là transform.pos.y + sprite::sort_offset. Đặt sprite::origin ở chân ({0.5, 1}) thì không cần sort_offset. Tilemap trong lớp đó vẫn vẽ trước mọi sprite, làm nền. Đặt cây, nhà, nhân vật, quái, rương cùng một lớp, và đặt collider ở gốc thân để va chạm khớp với chỗ đứng.

Tìm đường

njin::nav_grid là một lưới ô với chi phí đi vào mỗi ô (0 là vật cản). Dựng từ những gì đang có:

g.nav = njin::nav_grid_from_world(ctx, njin::level_bounds(ctx, level), {16, 16}, layer_world);

Mọi collider không phải trigger thuộc layer_world (kể cả ô của tilemap khác tile_none) thành vật cản. Sửa từng ô khi cửa mở, tường vỡ: nav_set_cost(), nav_set_area().

Việc Hàm
Đường ngắn nhất (A*) nav_find_path(): đi chéo, không cạ góc tường, rút gọn đường
Có nhìn thấy nhau không collision_line_of_sight()
Đi theo đường njin::nav_agent, nav_steer(): hướng đi, gán vào topdown_body::input.move

Lưu ý hai điều:

  • Quái đuổi theo tọa độ tâm collider, cùng chỗ lưới được dựng, không theo chân. Đi theo chân thì quái đi sát mép tường và bị kẹt.
  • A* tốn CPU, nên tìm lại mỗi 0.3 đến 0.5 giây và chỉ khi quái đang đuổi; nav_path_opts::max_nodes chặn một đích không tới được trên bản đồ lớn.

Quái chỉ nên đuổi khi nhìn thấy người chơi (tia không bị tường chắn), rồi bỏ cuộc khi người chơi ở quá xa. Đó là tất cả "trí tuệ" mà một game nhỏ cần.

Ô có animation

Nước, đuốc, cỏ lay: khai báo animation cho ô trong Tiled (animation editor của tileset) và engine chạy chúng, cả khi bản đồ đã bake vào ảnh chunk. Hoặc tự đặt bằng tilemap_animate(). Xem Tilemap.