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
Level từ Tiled và LDtk

Vẽ màn chơi trong editor, không viết cứng trong code. njin đọc được file của hai editor 2D phổ biến, bằng cùng một hàm. Không dùng editor? Viết bản đồ bằng chữ, mỗi ký tự một ô: xem Tilemap.

Editor File Ghi chú
Tiled .tmx (mặc định, XML), .tmj / .json Tileset tách file (.tsx, .tsj) được đọc theo
LDtk .ldtk Cả khi bật "Save levels to separate files" (.ldtkl)
#include <njin.h>
#include <string>
namespace {
constexpr njin::u32 layer_wall = njin::layer_bit(1);
constexpr njin::u32 layer_player = njin::layer_bit(0);
struct coin {
njin::i32 value = 1;
};
struct door {
std::string target;
};
// Tên prefab trùng với lớp của object trong Tiled / tên entity trong LDtk.
// level_object đã được gắn trước khi hàm dựng chạy, nên đọc được thuộc tính.
void build_coin(njin::context &ctx, entt::entity e) {
entt::registry &reg = njin::world(ctx);
const auto &obj = reg.get<njin::level_object>(e);
reg.emplace<coin>(e, coin{obj.props["value"].int_or(1)});
reg.emplace<njin::collider>(e, njin::collider{.shape = njin::collider_circle, .radius = 6,
.mask = layer_player, .trigger = true});
}
void build_door(njin::context &ctx, entt::entity e) {
entt::registry &reg = njin::world(ctx);
const auto &obj = reg.get<njin::level_object>(e);
reg.emplace<door>(e, door{obj.props["target"].string_or("start")});
reg.emplace<njin::collider>(e, njin::collider{.size = obj.size, .mask = layer_player,
.trigger = true});
}
void enter_play(njin::context &ctx) {
// Tiled (.tmx / .tmj) hay LDtk (.ldtk): cùng một lời gọi.
level = njin::level_load(ctx, "assets/levels/forest.tmx",
{.solid = njin::collider{.layer = layer_wall}});
// Người chơi xuất hiện tại object tên "spawn".
entt::registry &reg = njin::world(ctx);
const entt::entity spawn = njin::level_find(ctx, level, "spawn");
const njin::vec2 start = spawn != entt::null ? reg.get<njin::transform>(spawn).pos
: njin::vec2{32, 32};
const entt::entity hero = reg.create();
reg.emplace<njin::transform>(hero, njin::transform{.pos = start});
reg.emplace<njin::collider>(hero, njin::collider{.size = {12, 16}, .layer = layer_player});
reg.emplace<njin::scene_owned>(hero, njin::scene_owned{play});
// Thuộc tính đặt cho cả bản đồ trong editor.
const njin::json_value &props = njin::level_properties(ctx, level);
NJIN_INFO("nhạc nền: %s", props["music"].string_or("không có"));
}
void startup(njin::context &ctx) {
njin::prefab_register(ctx, {.name = "coin", .build = build_coin});
njin::prefab_register(ctx, {.name = "door", .build = build_door});
// Rời scene thì entity của level bị hủy và ảnh của nó được giải phóng.
play = njin::scene_register(ctx, {.name = "play", .on_enter = enter_play});
njin::scene_set(ctx, play);
}
void setup(njin::context &ctx) { njin::ecs_register(ctx, njin::phase_startup, startup); }
} // namespace
njin::mod_desc level_module() { return {.name = "level", .setup = setup}; }
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
@ collider_circle
Hình tròn bán kính radius.
Definition njin_collision.h:26
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ó.
entt::entity level_find(context &ctx, level_handle level, const char *name)
Tìm object đầu tiên của level có tên (hoặc, nếu không có tên nào khớp, lớp) là name.
const json_value & level_properties(const context &ctx, level_handle level)
Thuộc tính tùy chỉnh của cả bản đồ (Tiled) hoặc của level (LDtk).
#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.
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
prefab_handle prefab_register(context &ctx, const prefab_desc &desc)
Đăng ký một prefab.
scene_handle scene_register(context &ctx, const scene_desc &desc)
Đăng ký một scene.
void scene_set(context &ctx, scene_handle scene)
Chuyển sang một scene khác.
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
Hình va chạm của một entity.
Definition njin_collision.h:46
Một giá trị JSON: null, bool, số, chuỗi, mảng hoặc object.
Definition njin_json.h:33
Định danh của một level đã nạp, trả về từ level_load().
Definition njin_level.h:20
Một object đặt trong editor: điểm xuất hiện, cửa, vùng, quái...
Definition njin_level.h:62
Mô tả một module: một nhóm system có tên.
Definition _mod.h:67
Định danh của một scene, tạo bởi scene_register().
Definition _types.h:139
Gắn entity vào một scene.
Definition _comps.h:122
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ững gì được tạo ra

njin::level_load() đọc file và tạo entity ngay:

Trong editor Thành entity
Layer ô (Tiled tile layer, LDtk Tiles / AutoLayer) njin::tilemap. Một layer dùng nhiều tileset thì mỗi tileset một tilemap
Layer IntGrid của LDtk Một tilemap ẩn, giá trị ô là giá trị IntGrid (1, 2, ...). Đọc bằng njin::tilemap_get(), ví dụ ô nước là 2
Object (Tiled) / entity (LDtk) Entity có njin::transform ở tâm object, và njin::level_object
Tile object (Tiled), entity có ô hiển thị (LDtk) Có thêm njin::sprite
Image layer (Tiled) Một sprite

Thứ tự vẽ theo editor: layer dưới cùng ở njin::sprite::layer bằng level_desc::layer_base, mỗi layer phía trên cao hơn một.

Tileset có lề (margin, padding) và khoảng cách giữa ô (spacing), ô lật ngang và dọc, độ trong suốt và vị trí lệch (offset) của layer đều được giữ đúng. Ảnh tileset mặc định lấy mẫu filter_nearest cho pixel art sắc nét (đổi bằng level_desc::filter).

Object và prefab

Mỗi object mang njin::level_object:

Trường Tiled LDtk
name Name iid
type Class (hoặc Type ở bản cũ) Tên entity
size Width, Height Width, Height
props Custom properties Fields
points Polygon, polyline (so với vị trí object)

Có prefab trùng tên với type thì entity được dựng bằng prefab đó (xem Prefab và transform cha–con). njin::level_object được gắn trước khi hàm dựng chạy, nên hàm dựng đọc được thuộc tính: đặt value = 5 cho đồng xu trong editor, hàm dựng đọc obj.props["value"].int_or(1). Thuộc tính là njin::json_value (xem JSON và lưu game): chuỗi, số, bool, và cả object lồng nhau như field Point của LDtk (props["target"]["cx"]).

Không có prefab thì object vẫn là entity có transform và njin::level_object. Tìm nó bằng njin::level_find(), theo tên rồi theo lớp: tiện cho điểm xuất hiện, điểm camera, vùng kích hoạt.

Vật cản

Layer là vật cản nếu tên của nó nằm trong level_desc::solid_layers. Để trống danh sách thì dùng quy tắc mặc định: layer có thuộc tính solid = true (Tiled), hoặc tên chứa "collision", "collide", "solid" hay "wall".

Loại layer Thành
Layer ô, IntGrid njin::collider collider_tiles: mọi ô không trống chặn njin::collision_move() và raycast
Layer object Object không có prefab: hình chữ nhật thành collider hộp, elip thành collider tròn

Collider tạo ra lấy layer và mask từ level_desc::solid. Với LDtk, ô auto-layer vẽ đè lên IntGrid chỉ để trang trí; va chạm là giá trị IntGrid.

Nhiều level và unload

  • njin::level_load_ldtk() chọn level theo tên; njin::level_list_ldtk() liệt kê các tên. level_desc::use_world_position đặt level đúng vị trí trong world của LDtk để nạp nhiều level liền nhau.
  • Mặc định mọi entity của level thuộc scene đang chạy: rời scene thì chúng bị hủy và ảnh của level được giải phóng. Gọi njin::level_unload() để bỏ một level sớm hơn.
  • njin::level_size(), njin::level_origin() cho biết kích thước và vị trí, để giới hạn camera.
  • njin::level_properties() trả về thuộc tính của cả bản đồ (Tiled) hay của level (LDtk).

Chỉ lưới vuông

Bộ nạp level của njin chỉ nhận bản đồ lưới vuông (Tiled: Orientation "Orthogonal"; LDtk luôn là lưới vuông). Bản đồ isometric, staggered hay lục giác của Tiled bị từ chối: njin::level_load() trả về handle id 0 và ghi lý do vào log, thay vì vẽ sai.

Chưa hỗ trợ

Những thứ sau có cảnh báo trong log thay vì lỗi im lặng: nén zstd (hãy lưu bằng CSV, zlib hoặc gzip), ô xoay chéo (vẽ không xoay), tileset gồm nhiều ảnh rời, ô animation, object template của Tiled, nhiều world trong một project LDtk.

Hình va chạm và animation của ô

  • Tiled: đặt thuộc tính chuỗi collision (hoặc shape, hoặc lớp của ô) trên ô trong tileset: solid, none, one_way, slope_r, slope_l, slope_r_low, slope_r_high, slope_l_low, slope_l_high. Animation vẽ bằng trình chỉnh animation của tileset và được chạy đúng nhịp.
  • LDtk: đặt tên giá trị IntGrid theo các tên trên, hoặc gắn enum tag hay custom data lên ô của tileset.

Xem Làm game platformer.