Thay vì vẽ từng ô, bạn mô tả cách bản đồ được tạo ra: nhiễu cho hình dáng, vài luật cho vẻ tự nhiên, hoặc Wave Function Collapse (WFC) cho các mảnh ghép khớp nhau. Mỗi lần đổi seed là một bản đồ mới, và cùng seed luôn cho cùng một bản đồ.
Ví dụ top-down chạy thật: mỗi lần bấm R là một hòn đảo mới, đã bo góc bờ và mỏm đá. Camera được kéo ra xa để thấy cả bản đồ
Ví dụ platformer chạy thật: mỗi lần bấm R là một màn mới, có hố, hang, bục, dốc và góc bo tròn
Ba bước
Mọi thứ đều đi qua một lưới tạm, njin::tile_grid, chỉ là dữ liệu nên không cần cửa sổ hay njin::context:
Hai ví dụ đầy đủ dưới đây đi qua cả ba bước: một cho top-down, một cho platformer. Cả hai chạy trong thư mục game mẫu tương ứng (chúng cần assets/terrain.png, xem Bo góc tự nhiên: autotile). R là màn mới.
#include <njin.h>
namespace {
using namespace njin;
constexpr i32 water = 0, grass = 1, rock = 2, flowers = 3;
constexpr i32 land_set = 0, rock_set = 47, water_tile = 94, flower_tile = 98;
entt::entity ground = entt::null, props = entt::null, sea = entt::null, player = entt::null;
return out;
}
void build(context &ctx) {
d.
height_noise = {.seed = seed, .frequency = 0.06f, .octaves = 5, .warp = 6.0f};
d.
biomes = {{water, 0.32f}, {grass, 0.85f}, {rock, 1.0f}};
grid_autotile(land, {{.tiles = {grass}, .base = land_set, .outside = 0}});
grid_autotile(top, {{.tiles = {rock}, .base = rock_set, .outside = 0}});
for (
usize i = 0; i < top.cells.size(); i++)
top.cells[i] = flower_tile;
entt::registry ® =
world(ctx);
for (const auto &[e, grid] : {std::pair{ground, &land}, std::pair{props, &top}}) {
}
}
entt::entity make_layer(context &ctx,
const tilemap &shared,
i32 layer,
bool collide) {
entt::registry ® =
world(ctx);
const entt::entity e = reg.create();
if (collide)
return e;
}
void startup(context &ctx) {
entt::registry ® =
world(ctx);
for (
i32 i = 0; i < 47; i++)
sea = make_layer(ctx, map, 0, false);
ground = make_layer(ctx, map, 1, false);
props = make_layer(ctx, map, 2, true);
player = reg.create();
reg.emplace<
collider>(player,
collider{.size = {10.0f, 8.0f}, .offset = {0.0f, -4.0f}});
camera_follow{.target = player, .bounds = {{0.0f, 0.0f}, {64 * 16.0f, 48 * 16.0f}}});
build(ctx);
}
void update(context &ctx) {
seed++;
build(ctx);
}
}
void draw_player(context &ctx) {
}
void setup(context &ctx) {
}
}
int main() {
context *ctx =
create({.title =
"Sinh bản đồ top-down", .width = 960, .height = 540, .target_fps = 60});
}
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.
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_update
Mỗi frame, logic chính của game.
Definition _mod.h:35
@ 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
topdown_gen_result generate_topdown(const topdown_gen_desc &desc)
Sinh bản đồ top-down từ nhiễu.
void tilemap_from_grid(tilemap &map, const tile_grid &grid, cell origin={})
Đặt lưới vào tilemap, góc trên trái ở ô origin.
i32 grid_scatter(tile_grid &grid, i32 on, i32 place, f32 chance, u32 seed, i32 min_distance=0)
Rải ngẫu nhiên: mỗi ô on có xác suất chance thành place, và hai ô được rải cách nhau ít nhất min_dist...
tile_grid tile_grid_make(i32 width, i32 height, i32 fill=-1)
Tạo một lưới width x height, mọi ô là fill.
i32 grid_autotile(tile_grid &grid, const std::vector< autotile_rule > &rules)
Thay mọi ô thuộc rules[i].tiles bằng ô của bộ tương ứng, chọn theo các ô kề: chỗ nào địa hình hở ra t...
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_animate(tilemap &map, i32 id, const std::vector< i32 > &frames, f32 seconds)
Cho mọi ô số id chạy animation qua các ô frames, mỗi frame seconds giây.
Definition _tilemap.h:188
void tilemap_clear(tilemap &map)
Xóa mọi ô.
Definition _tilemap.h:291
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
rect tilemap_cell_rect(const tilemap &map, vec2 origin, i32 x, i32 y)
Hình chữ nhật của một ô trong thế giới.
Definition _tilemap.h:420
@ 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
std::size_t usize
Số nguyên không dấu cỡ con trỏ, dùng cho kích thước và chỉ số.
Definition _types.h:34
std::uint32_t u32
Số nguyên không dấu 32 bit.
Definition _types.h:22
constexpr rgba yellow
Vàng.
Definition _math.h:239
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
Vị trí một ô trong lưới. Có thể âm.
Definition _tilemap.h:43
i32 x
Cột.
Definition _tilemap.h:44
i32 y
Hàng.
Definition _tilemap.h:45
Hình va chạm của một entity.
Definition njin_collision.h:46
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
Một lưới ô hình chữ nhật, kích thước cố định, để sinh và sửa bản đồ trước khi đưa vào njin::tilemap.
Definition njin_procgen.h:24
i32 width
Số cột.
Definition njin_procgen.h:25
i32 height
Số hàng.
Definition njin_procgen.h:26
std::vector< i32 > cells
Các ô theo hàng, từ trên xuống. cells[y * width + x].
Definition njin_procgen.h:27
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
i32 layer
Lớp vẽ, cùng thang với sprite::layer.
Definition _tilemap.h:133
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
Tham số sinh bản đồ top-down.
Definition njin_procgen.h:256
noise_desc height_noise
Nhiễu độ cao.
Definition njin_procgen.h:259
std::vector< biome > biomes
Các vùng, xét theo thứ tự. Ô không khớp vùng nào dùng vùng cuối.
Definition njin_procgen.h:262
std::vector< i32 > walkable
Các ô đi được.
Definition njin_procgen.h:272
i32 height
Số hàng.
Definition njin_procgen.h:258
i32 blocked_tile
Ô thay cho vùng đi được bị bỏ, khi có walkable.
Definition njin_procgen.h:273
i32 width
Số cột.
Definition njin_procgen.h:257
f32 island
0 là tắt.
Definition njin_procgen.h:263
Kết quả của generate_topdown().
Definition njin_procgen.h:278
tile_grid grid
Bản đồ.
Definition njin_procgen.h:279
cell spawn
Điểm xuất phát: ô đi được gần tâm nhất (tâm bản đồ nếu không có walkable).
Definition njin_procgen.h:281
Vector 2 chiều. Dùng cho vị trí, kích thước, độ dời.
Definition _types.h:39
#include <njin.h>
namespace {
using namespace njin;
constexpr i32 grass = 0, dirt = 1, stone = 2;
constexpr i32 ground_set = 0, stone_set = 47, plank = 94, bush = 95, flower = 96;
entt::entity level = entt::null, player = entt::null;
void build(context &ctx) {
auto on_grass = [](
const tile_grid &g,
i32 x,
i32 y) {
return g.
get(x, y) == -1 && g.
get(x, y + 1) == grass; };
grid_autotile(r.
grid, {{.tiles = {grass, dirt}, .joins = {stone}, .base = ground_set, .outside = outside},
{.tiles = {stone}, .joins = {grass, dirt}, .base = stone_set, .outside = outside}});
entt::registry ® =
world(ctx);
}
void startup(context &ctx) {
entt::registry ® =
world(ctx);
level = reg.create();
reg.emplace<
tilemap>(level, std::move(map));
player = reg.create();
reg.emplace<
collider>(player,
collider{.size = {10.0f, 14.0f}, .offset = {0.0f, -7.0f}});
camera_follow{.target = player, .bounds = {{0.0f, 0.0f}, {160 * 16.0f, 30 * 16.0f}}});
build(ctx);
}
void update(context &ctx) {
seed++;
build(ctx);
}
}
void draw_player(context &ctx) {
}
void setup(context &ctx) {
}
}
int main() {
context *ctx =
create({.title =
"Sinh màn platformer", .width = 960, .height = 540, .target_fps = 60,
.clear_bg_color = {0.37f, 0.80f, 0.89f, 1.0f}});
}
platformer_gen_result generate_platformer(const platformer_gen_desc &desc)
Sinh màn platformer từ nhiễu.
@ side_all
Cả bốn phía.
Definition njin_procgen.h:116
@ side_up
Ô phía trên.
Definition njin_procgen.h:112
@ 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::uint8_t u8
Số nguyên không dấu 8 bit.
Definition _types.h:20
u32 seed
Hạt giống. Đổi số này là có bản đồ khác.
Definition njin_procgen.h:85
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
i32 get(i32 x, i32 y) const
Giá trị một ô.
Definition njin_procgen.h:32
Nhiễu: từ đơn giản đến phức tạp
njin::noise_2d() cho một số từ 0 đến 1 tại mỗi điểm, mượt theo không gian. njin::noise_desc có mọi núm chỉnh, và mỗi núm làm một việc dễ thấy:
| Núm | Tác dụng | Khi nào chỉnh |
| seed | Đổi cả bản đồ | Mỗi màn |
| frequency | Nhỏ: vùng to; lớn: vùng vụn (0.02 đến 0.1) | Đầu tiên: quyết định cỡ vùng |
| octaves | Số lớp chi tiết chồng lên | Muốn mép có chi tiết nhỏ (3 đến 6) |
| gain | Độ mạnh của lớp nhỏ | Muốn mép gồ ghề (0.5 mặc định) |
| lacunarity | Lớp sau nhỏ hơn lớp trước bao nhiêu lần | Hiếm khi |
| fractal | fractal_fbm địa hình, fractal_ridged sống núi và sông, fractal_billow đồi tròn | Đổi dáng |
| warp | Uốn cong toạ độ (tính bằng ô) | Muốn viền cong queo (4 đến 20) |
Đoạn dưới in nhiễu ra chữ (# là chỗ nhiễu cao hơn ngưỡng) với từng núm bật dần lên. Đây là kết quả chạy thật:
#include <cstdio>
#include <njin.h>
using namespace njin;
std::printf("%s\n", title);
for (
i32 y = 0; y < 14; y++) {
for (
i32 x = 0; x < 60; x++)
std::putchar('\n');
}
}
int main() {
show(
"1. một lớp, tần số 0.06", {.seed = 4, .frequency = 0.06f, .fractal =
fractal_none}, 0.5f);
show("2. fBm, 5 lớp", {.seed = 4, .frequency = 0.06f, .octaves = 5}, 0.5f);
show("3. fBm, 5 lớp, gain 0.7", {.seed = 4, .frequency = 0.06f, .octaves = 5, .gain = 0.7f}, 0.5f);
show("4. fBm, 5 lớp, warp 8", {.seed = 4, .frequency = 0.06f, .octaves = 5, .warp = 8.0f}, 0.5f);
show(
"5. ridged, 4 lớp", {.seed = 4, .frequency = 0.05f, .fractal =
fractal_ridged, .octaves = 4}, 0.72f);
}
f32 noise_2d(const noise_desc &desc, f32 x, f32 y)
Giá trị nhiễu tại một điểm, từ 0 đến 1.
@ fractal_none
Một lớp. Hình to, trơn.
Definition njin_procgen.h:73
@ fractal_ridged
Gờ nhọn: dãy núi, sống đá, dòng sông khi lấy vùng thấp.
Definition njin_procgen.h:75
float f32
Số thực 32 bit.
Definition _types.h:28
Tham số của một nguồn nhiễu.
Definition njin_procgen.h:84
1. một lớp, tần số 0.06
.################.................################..........
..###############...............##############..............
....#############.............###############...............
......###########............################...............
........#########............################.........######
.........########............#################....##########
.........########.............#################.############
........#########..............##############...############
........#########................##########.......##########
##....###########..................#####............########
##################...................................#######
##....#############...................................######
........#############.................................######
..........#############..............................#######
2. fBm, 5 lớp
...############..####.............#...########...#..####...#
#.##############..##.............###...#######..............
.###############.#####..........##############..............
......################........#################......####...
......#########..####.........###############.......########
...........####................##############....###########
...............................########...##...#############
..######..#.##.####...........#########......###############
..########....#####..........#######..####...####.##########
...########...........................####..........########
##.########..######....................#................####
####.......#########.....................................###
###......##############...............................######
.........###############...........................#########
3. fBm, 5 lớp, gain 0.7
...###########...####..........#..#...########...#..####...#
#.##############..###............###...#######............#.
.######.########.######.........##############...........##.
......###################.#..##################......#####..
.....######.###..######.......###############.......########
............###....#............#######.####.....###########
................................######....##...#############
.#######..#.##.####..........###..####.......######.....####
.#########....##.###.......#########...###...####.##########
...#######........#...................#####.#.....####..####
#..#######...##.###...................##................##..
####.......#########.##...............###................###
####........########.##...............................######
........##########..####..............##...........###....##
4. fBm, 5 lớp, warp 8
..##########..####.............#..################...#######
#################................####....########.......###.
###################.............#################...........
....################.........####################...........
....########.####............####################.......####
.....######....................################......#######
........##....................################....##########
.............#...............############..###..############
.#####.#.#######............#############.....#####.########
.#######.................................###..#....#..######
.#######...#.####........................##..............###
###.....###########..#.................................#####
##.....################..............................#######
......#################.............#######..........#######
5. ridged, 4 lớp
###................###................#####.......#.########
#####..............###..............#####.............####.#
....##............###..............###................###...
.....###.........####..............##.................##....
......###........####.................................##....
........###....#.#.###...........##..................#######
.........###........###.............................########
.........##.........####...........#.................######.
........####........###.............###..............#####.#
#.......####.......###................###...........###.####
###.....####.......###.................###......######.....#
######.###..........#...................###########........#
#########.........#.#........................####...........
.########.............#.....................................
Đọc từ trên xuống: (1) một lớp cho những mảng trơn; (2) thêm lớp thì mép có chi tiết; (3) gain cao làm mép gồ ghề hơn; (4) warp uốn các mảng cho tự nhiên; (5) fractal_ridged cho các sống hẹp, hợp với núi hay sông.
- Ghi chú
- Nhiễu fBm dồn quanh 0.5 và hiếm khi ra ngoài 0.2 đến 0.8, nên "ngưỡng 0.3" không có nghĩa là 30% bản đồ. njin::generate_topdown() tự kéo giãn cho đủ 0 đến 1, nên ngưỡng của các vùng đất là phần trăm của bản đồ. Khi tự dùng njin::noise_2d(), hãy đặt ngưỡng bằng mắt như ở đoạn trên, hoặc xếp hạng các giá trị rồi lấy phần trăm.
Luật: làm bản đồ trông tự nhiên
Nhiễu thô thường vụn: chấm cỏ lẻ giữa nước, vũng một ô, mép răng cưa. Các luật sửa đúng những thứ đó, và chạy trên bất kỳ njin::tile_grid nào, dù nó đến từ nhiễu, WFC hay một bản đồ chữ (njin::tile_grid_from_text()).
#include <cstdio>
#include <njin.h>
using namespace njin;
constexpr i32 water = 0, sand = 1, grass = 2;
void print(
const char *title,
const tile_grid &g) {
std::printf("%s\n", title);
std::putchar(
".,#"[g.
get(x, y)]);
std::putchar('\n');
}
}
int main() {
const noise_desc noise{.seed = 11, .frequency = 0.16f, .octaves = 3};
g.
set(x, y, v < 0.47f ? water : (v < 0.52f ? sand : grass));
}
print("Trước khi áp luật", g);
print("Sau khi áp luật", g);
}
i32 grid_border(tile_grid &grid, i32 tile, i32 touching, i32 replace, u8 sides=side_all, i32 distance=1)
Viền: ô tile nằm cách touching không quá distance ô (theo các phía trong sides) thì thành replace.
i32 grid_merge_small(tile_grid &grid, i32 min_size)
Như grid_remove_small() cho mọi loại ô: vùng nhỏ hơn min_size hoà vào loại ô bao quanh nó nhiều nhất.
void grid_majority(tile_grid &grid, i32 iterations=1, i32 threshold=5)
Làm mượt: mỗi ô thành loại ô xuất hiện nhiều nhất trong ô 3 x 3 quanh nó, nếu loại đó có ít nhất thre...
void set(i32 x, i32 y, i32 tile)
Đặt một ô; ngoài lưới thì bỏ qua.
Definition njin_procgen.h:34
Bản đồ trước và sau khi áp ba luật (. là nước, , là cát, # là cỏ):
Trước khi áp luật
,##,.,,.#,,.,,#...,,,#,..,.,...,######......,###.,,,#####,..
#####......####..,,..,,######....#######...,,#,......,,#####
###,......####....#,,###,.......#,######....,##......,,,#,.,
,,###.,#####,#.....,#####,.....,,####,......##,,,,,##.,,,.##
..####,,,#####,......####,..,.#####,#......,.,...,##,..,####
....,,...,##......#..,,####,,,####,......,##,,.....#,,,..,#,
,###,,,..,#,,,..###,...##,...###......,#####.......##...,###
#############,,,###...,#,,.,,##,.....#######,.....,##,...,#,
.###########,,,...,,###..,#####....,#########....,,,#,,....,
#######,,###.......###,.,,.,###,.....,########....,,,##,#,,,
.,###,....#####...,###,.,########......####,,,...#######,,##
,,.......,############,.....,##,.,...,,#######,.....,,##,,.,
......,,.,#,,#####.,.,..#.......,..,########,##,...........#
##########,..,,,.,,#,,,#######,#,......,,,,,....###,,#####,,
Sau khi áp luật
####,......,##,...,######,......,####,......,.......,#######
####,......,##,...,######,......,#####,.....,........,,#####
####,.....,##,....,######,......,#####,.....,........,,,####
####,.,,,,###,.....,,####,.....,#####,............,,,,,,####
#####,######,........,###,....,###,,,.......,.....,,,,,,,###
############,.........,##,...,##,,.......,,,.......,,,...,##
###########,...........,#,..,##,......,,,##,.......,,....,##
###########,..........,#,,,,##,......,#####,.......,,....,##
###########,........,,#,,,,###,......,######,.............,#
#####,,,,,##,......,##,,,,#####,.....,#######,......,,,,,,##
,,,,,.....,##,,...,###,,.,,,###,......,######,.....,########
.........,#####,,,####,.....,,,.......,######,......,,######
.........,,#########,,................,#####,,........,#####
...........,##########,................,###,.........,######
Mảng cỏ vụn thành vài khối liền, chấm lẻ biến mất, và chỗ cỏ chạm nước có viền cát. Thứ tự các luật quan trọng: làm mượt và bỏ vùng nhỏ trước, viền sau cùng.
Bộ sinh top-down
njin::generate_topdown() nhận một bảng vùng đất (njin::biome) theo độ cao và, nếu muốn, độ ẩm:
- biomes: xét theo thứ tự, vùng đầu tiên thoả max_height và max_moisture được chọn. Ví dụ {{water, 0.3}, {sand, 0.35}, {grass, 0.8}, {rock, 1.0}}: 30% thấp nhất là nước, rồi cát, cỏ, và đỉnh là đá.
- island: kéo độ cao xuống về mép, để bản đồ là hòn đảo giữa biển.
- walkable, blocked_tile: chỉ giữ chỗ đi được lớn nhất, phần còn lại thành vật cản. Điểm xuất phát (result.spawn) luôn nằm trong đó, nên người chơi không bao giờ bị nhốt.
- border_tile: vòng tường quanh bản đồ.
- smooth, min_region: số lần làm mượt và cỡ vùng nhỏ nhất được giữ.
- Độ ẩm (moisture_noise, max_moisture) cho sa mạc và rừng ở cùng độ cao.
result.heights trả lại độ cao từng ô, để luật của riêng bạn dùng (ví dụ đặt cây ở chỗ cao vừa).
Bộ sinh platformer
njin::generate_platformer() dựng đường mặt đất bằng nhiễu một chiều, rồi đục hang, đào hố, đặt bục. Khác với top-down, nó có luật chơi được, vì màn nhìn ngang mà nhảy không tới thì hỏng:
| Luật | Tham số | Mặc định | Vì sao |
| Hai cột cạnh nhau chênh không quá max_step ô | max_step | 2 | Nhân vật mặc định nhảy cao gần 3 ô |
| Hố không rộng quá pit_max ô | pit_min, pit_max | 1 đến 2 | Nhảy xa khoảng 59 px, hố 2 ô là 32 px |
| Hai cột trước hố và cột sau hố bằng nhau | | | Tránh bước xuống ngay mép hố, không còn chỗ đạp |
| Hai đầu màn bằng phẳng, không hố | safe_columns | 6 | Chỗ xuất phát và đích |
| Hang không đục lên sát mặt đất | cave_margin | 4 | Không thủng sàn, không sụp xuống hang giữa đường |
| Bục lơ lửng thấp hơn tầm nhảy | platform_height | 3 | Nhảy lên được |
| Chỗ đất lên hoặc xuống đúng 1 ô có thể là dốc | slope_r_tile, slope_l_tile | không dùng | Đi lên êm hơn |
caves là tỉ lệ diện tích ngầm được đục (0.2 là chừng 20%): hang được chọn theo thứ hạng của nhiễu, rồi bo tròn bằng automat tế bào, và hốc dưới 8 ô bị lấp.
- Ghi chú
- Những con số trên dành cho njin::platformer_body mặc định (chạy 110 px/s, nhảy cao chừng 45 px, xa chừng 59 px). Đổi run_speed hay jump_speed của nhân vật thì đổi pit_max, max_step, platform_height theo.
Đã kiểm chứng thế nào: một bot chạy sang phải và nhảy khi gặp hố hay tường, điều khiển njin::platformer_body thật trên chính màn do ví dụ trên sinh ra (160 x 30, caves 0.2, pit_chance 0.06, platform_chance 0.05, không dùng dốc), tới được đích ở cả 20 seed liền nhau (1 đến 20). Đó là bằng chứng cho các tham số ở trên, không phải lời hứa cho mọi tham số: nới pit_max hay max_step quá tầm nhảy thì màn có thể không đi được.
Bo góc tự nhiên: autotile
Một lưới ô vuông trông "răng cưa" vì mỗi ô là hình vuông. Autotile sửa việc đó: đất được vẽ bằng một bộ ô, và mỗi ô của lưới chọn tấm nào trong bộ tuỳ theo các ô xung quanh nó. Chỗ đất hở ra thì có mép, hai mép gặp nhau thì góc tròn, chỗ đất lõm vào thì có góc lõm. Bạn viết luật: "những ô nào là đất, bộ ô nào vẽ chúng".
njin::grid_autotile() làm việc đó trên njin::tile_grid, bằng njin::autotile_rule:
| Trường | Ý nghĩa |
| tiles | Các ô của lưới mà luật này thay bằng ô của bộ. Cỏ và đất có thể cùng là một địa hình |
| joins | Ô khác vẫn được coi là nối liền, nhưng không bị thay (đá dưới đất, cửa trên tường) |
| base | Số thứ tự ô đầu tiên của bộ trong tileset; bộ là các ô liền nhau |
| layout | njin::autotile_blob (47 ô, có cả góc lồi và góc lõm) hoặc njin::autotile_edges (16 ô, chỉ nhìn bốn phía) |
| outside | Những phía mà ngoài lưới coi như đất chạy tiếp (các bit njin::grid_side). Mặc định cả bốn |
Một ô nhìn tám ô kề, cộng thành mặt nạ (njin::autotile_neighbor). Ô chéo chỉ tính khi cả hai ô kề thẳng cạnh nó cũng nối liền (nếu không, mép đã che góc rồi), nên còn đúng 47 kiểu. njin::autotile_index() đổi mặt nạ thành số thứ tự ô trong bộ:
#include <cstdio>
#include <njin.h>
using namespace njin;
constexpr i32 ground = 1;
int main() {
"..........\n"
"..####....\n"
".######...\n"
".######...\n"
"..#####...\n"
"....####..\n"
"....####..\n"
"..........\n",
{{'#', ground}});
std::printf("%d ô đất được thay bằng ô của bộ\n\n", n);
if (before.get(x, y) == -1)
std::printf(" . ");
else
std::printf(
"%3d ", g.
get(x, y) - 100);
}
std::putchar('\n');
}
std::printf(
"\nô (2, 1): mặt nạ %d -> ô số %d của bộ (khớp: %s)\n", mask,
autotile_index(mask),
}
tile_grid tile_grid_from_text(std::string_view text, std::initializer_list< tile_key > legend)
Dựng lưới từ bản đồ chữ, cùng quy tắc với tilemap_from_text(): mỗi ký tự một ô, legend cho biết ký tự...
i32 autotile_index(u8 mask, autotile_layout layout=autotile_blob)
Số thứ tự ô (trong bộ, từ 0) ứng với một mặt nạ.
@ neighbor_right
Ô bên phải.
Definition njin_procgen.h:192
@ neighbor_down
Ô phía dưới.
Definition njin_procgen.h:194
@ neighbor_down_right
Ô chéo phải dưới.
Definition njin_procgen.h:193
29 ô đất được thay bằng ô của bộ
. . . . . . . . . .
. . 10 31 31 26 . . . .
. 10 33 46 46 45 26 . . .
. 4 41 46 46 46 42 . . .
. . 4 36 41 46 42 . . .
. . . . 12 46 45 26 . .
. . . . 4 36 36 34 . .
. . . . . . . . . .
ô (2, 1): mặt nạ 28 -> ô số 10 của bộ (khớp: có)
Số 46 là ô ở giữa vùng (đủ tám ô kề). Góc trên trái của khối là ô số 10, đúng bằng mặt nạ neighbor_right | neighbor_down | neighbor_down_right tính tay ở dòng cuối.
Game mẫu có sẵn hai bộ 47 ô (đất phủ cỏ và đá) trong terrain.png, xếp đúng thứ tự của njin::autotile_index():
terrain.png của game mẫu platformer, vẽ lên nền trời: bộ đất phủ cỏ (trái) và bộ đá (phải), mỗi bộ 47 ô, đánh số theo thứ tự trong bộ. Ô 46 là ô ở giữa vùng
Cùng một màn, một bên chỉ dùng ô "đầy đủ" (46 và 93) cho mọi ô, một bên qua njin::grid_autotile():
Trái: mọi ô là ô đầy đủ, không autotile. Phải: cùng màn qua njin::grid_autotile(): mép có viền, cỏ chỉ ở mặt trên, góc lồi tròn
Vài điều nên biết:
- Gọi sau cùng. Autotile đổi ô "trên giấy" thành số thứ tự ô của bộ, nên các luật đọc loại ô (njin::grid_border(), njin::grid_scatter() với điều kiện) phải chạy trước. Các luật trong một lần gọi đều đọc từ một bản chụp lưới, nên thứ tự chúng không ảnh hưởng, và số thứ tự ô mới không bị nhầm với ô cũ.
- Va chạm vẫn là ô vuông. Góc tròn chỉ là hình vẽ; hộp va chạm của ô vẫn đầy đủ. Ở top-down đó thường đúng ý; ở platformer, đứng sát mép góc bo sẽ hơi "lơ lửng" vài pixel. Nếu cần hình khớp hẳn, dùng dốc (njin::tile_slope_r).
- Nhiều lớp. Ví dụ top-down ở trên chia thành ba tilemap (nước, đất, đá và hoa) với njin::tilemap::layer: đất bo góc để lộ nước bên dưới, đá bo góc để lộ cỏ bên dưới. Đó là cách có bờ biển và mỏm đá tròn mà không cần ô "cát
chuyển sang nước" riêng cho từng cặp.
- Ngoài lưới. Mặc định ngoài lưới là đất chạy tiếp, nên mép bản đồ không bo. Bỏ side_up khỏi outside cho mặt đất platformer, như ví dụ; đặt outside = 0 cho hòn đảo, như ví dụ top-down.
- Tự vẽ bộ ô. Bộ 47 ô là quy ước phổ biến (còn gọi "blob tileset"). Dùng ảnh của bạn thì xếp các ô theo đúng thứ tự của njin::autotile_index(); script src/games/shared/tools/make_terrain.py là ví dụ cách dựng chúng từ mặt nạ.
Wave Function Collapse
WFC lấp lưới sao cho mọi cặp ô cạnh nhau đều hợp lệ. Bạn không đặt hình dáng, bạn đặt luật kề: "ô nào được đứng bên
phải, bên dưới... ô nào". Có hai cách có luật:
Học từ một mẫu
njin::wfc_learn() đọc một njin::tile_grid mẫu (thường viết bằng njin::tile_grid_from_text()): mọi cặp ô cạnh nhau trong mẫu thành luật kề, số lần xuất hiện thành độ hay gặp. Mẫu nhỏ và đa dạng cho kết quả tốt; cặp nào mẫu không có thì không bao giờ xuất hiện. njin::wfc_desc::allowed cố định những chỗ bạn muốn (trời ở hàng trên, đá ở hàng dưới):
#include <cstdio>
#include <njin.h>
using namespace njin;
constexpr i32 sky = -1, grass = 0, dirt = 1, stone = 2;
std::putchar(t < 0 ? ' ' : "gds"[t]);
}
std::putchar('\n');
}
}
int main() {
" \n"
" ggg \n"
"gggggggg ggggggggddggggggg \n"
"ddddddddgggggddddddddddddddddg\n"
"dddddddddddddddddddddddddddddd\n"
"ssdddddsssdddddddddsssssdddsss\n"
"ssssssssssssssssssssssssssssss\n",
{{'g', grass}, {'d', dirt}, {'s', stone}});
wfc_desc d{.width = 60, .height = 20, .seed = 8};
if (y == 0)
return tile == sky;
return tile == stone;
return true;
};
std::printf("luật quá chặt hoặc ràng buộc mâu thuẫn\n");
return 1;
}
print(out);
}
bool wfc_generate(const wfc_rules &rules, const wfc_desc &desc, tile_grid &out)
Sinh một lưới thoả mọi luật kề, bằng Wave Function Collapse (mô hình ô, không quay lui: gặp mâu thuẫn...
wfc_rules wfc_learn(const tile_grid &sample, bool periodic=false)
Học luật từ một mẫu: mọi cặp ô cạnh nhau trong mẫu thành luật kề, số lần xuất hiện thành độ hay gặp.
Tham số của wfc_generate().
Definition njin_procgen.h:380
std::function< bool(i32 x, i32 y, i32 tile)> allowed
Ràng buộc: ô tile có được đặt ở (x, y) không.
Definition njin_procgen.h:388
i32 height
Số hàng.
Definition njin_procgen.h:382
Luật của Wave Function Collapse: có những ô nào, mỗi ô hay gặp đến đâu, và ô nào được đứng cạnh ô nào...
Definition njin_procgen.h:354
ggggg g gg gg g ggg gggg ggg
gdddddggg gggg g g g g gg gdggg gddgddddgddg
gg ggdddsddddggdddg ggg g gdgg gggddgddgddgggddddddddddd
ddg gdddddsddddddddddgdgdgg ggddggddgdddddgdddddddddddddddd
dddggddddddsdddddddddddddsdg gddddddddsddddddddddddddddddddd
ddddgddddddsdddddddddddddsdgggddddddsdsdddddddddddddsdddddds
dddddddddddsdddddddddddddsdgddddddddsdsdddsdddddddddsdddddds
dddddddddddsdsdddddddddddsddddddddddsdsdddsddddddsddsdddddds
dddddddddddsdsdddddddddddsddddddddddsdsdsdsddddddsddsdddddds
dddddddddddsdsdddddddddddsddddddddddsssdsssdddddssddsdddddds
dddddddddddsdsdsdddddddddsddddddddsdsssdsssdddsdssddsdddddds
ddsdddsddddsdsdsddsddddddsddddddddsdsssdsssdsdsdssddsdddddds
ddsdddsddddsdsdsdssddddddsddddddddsdsssdsssdsdsdssddsdddddds
ddsdsdsddddsdsdsdsssdsdddsddddddsdsssssdsssdsdsdssddsdsdddds
ddsdsdsddddsdsdsdsssdsdsdsdsddsdsdsssssdsssdsdsdssddsdsdddds
sdsdsdsdsddsdsdsdsssdsdsssssddsdsssssssdsssdsssdssddsssdddds
sdsdsdsdssdsdsdsssssdsssssssdssdsssssssdsssdsssdssddsssssdds
sssdsdsdssdsdsssssssdsssssssdssdsssssssdssssssssssdsssssssds
ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss
Mọi cột đều đúng thứ tự trời, cỏ, đất, đá, và không có cỏ nào lơ lửng, vì mẫu chưa từng cho cỏ đứng dưới đất. Nhưng luật là theo cặp ô, không theo mảng ô: WFC này không biết "đồi rộng chừng nào", nên bề mặt có thể lởm chởm và đôi chỗ thành cột. Cho hình dáng tốt hơn thì dùng nhiễu (bộ sinh ở trên) rồi để WFC lo phần chi tiết, hoặc cho mẫu nhiều đoạn phẳng hơn (cặp cỏ với cỏ hay gặp hơn, nên bề mặt có xu hướng phẳng hơn), hoặc dùng luật tự viết như dưới đây.
Luật tự viết: đường nối liền
Với bộ ô có mép khớp nhau (đường, sông, hành lang), luật viết bằng tay dễ hơn mẫu: hai ô cạnh nhau hợp lệ khi cùng có đường hoặc cùng không có ở phía chạm nhau. Mười sáu ô đường đánh số đúng như njin::autotile_edges (bit 1 lên, 2 phải, 4 xuống, 8 trái), nên cùng bộ ô đó dùng để vẽ luôn:
#include <bit>
#include <cstdio>
#include <njin.h>
using namespace njin;
constexpr i32 up = 1, right = 2, down = 4, left = 8;
int main() {
for (
i32 t = 0; t < 16; t++) {
const i32 arms = std::popcount((
unsigned)t);
const f32 weight = arms == 0 ? 6.0f : (arms == 1 ? 0.05f : (arms == 2 ? (t == 5 || t == 10 ? 3.0f : 1.5f) : (arms == 3 ? 0.4f : 0.15f)));
}
for (
i32 a = 0; a < 16; a++)
for (
i32 b = 0; b < 16; b++) {
if (((a & right) != 0) == ((b & left) != 0))
if (((a & down) != 0) == ((b & up) != 0))
}
wfc_desc d{.width = 40, .height = 14, .seed = 6};
return !((x == 0 && (tile & left)) || (x == d.
width - 1 && (tile & right)) || (y == 0 && (tile & up)) ||
(y == d.
height - 1 && (tile & down)));
};
std::printf("không sinh được\n");
return 1;
}
const char *glyph[16] = {" ", "╵", "╶", "└", "╷", "│", "┌", "├", "╴", "┘", "─", "┴", "┐", "┤", "┬", "┼"};
std::printf(
"%s", glyph[out.
get(x, y)]);
std::putchar('\n');
}
}
void wfc_allow(wfc_rules &rules, i32 a, wfc_dir dir, i32 b)
Cho phép b đứng ở hướng dir của a, và (đối xứng) a đứng ở hướng ngược lại của b.
i32 wfc_add_tile(wfc_rules &rules, i32 tile, f32 weight=1.0f)
Thêm một ô vào luật (hoặc đổi độ hay gặp nếu đã có).
i32 width
Số cột.
Definition njin_procgen.h:381
┌┐ ┌┬─┐ ┌─┐┌┐ ┌─┐ ┌─┐
┌───┼┤ ┌┘└─┘ │ │└┘ │ │ ├┐├╴
│ ││ ┌─┼─┐ ┌─┴┐│ └─┤ ┌┐ ├┴┴┐
├───┘│ │╷└┐│ └┐┌┘└┐┌────┤ │└┐ └──┘
┌┘ │ ┌┘└─┘│┌─┴┘ ┌┘│ ┌─┐│ │ │ ┌─┐
└─────┘ │ ┌──┴┘ ┌┐ ├┬┘┌┴┬┘└─┘ │┌┐┌┘ │┌─┐
┌───┐│ │┌────┴┘┌┘└┐└─┤┌─┐ ││││ └┘╶┘
┌┐ │┌──┘└─┼┘ ┌┐ └┐ │ ││ └─┐││└┤
└┤┌┴┘┌──┐┌┘╷ │└┐ └┬┘ ┌┘├┬─┐│└┘┌┘┌┐ ┌─┐
│└┬─┤ ╷└┘ └┐│ └┐ │┌┐└─┘└┬┼┘ └─┘│ └─┘
│╶┘ └─┘ │└──┘┌┐ └┴┼─┐ ││ │┌┐
└┐ ┌──┘ └┴┐ └─┘ └┘ │└┤
└──────┘ ┌┐ └┐ ┌───┐┌┐ ┌┐└┐│
└┴────┘ └───┘└┘ └┘ └┘
Mọi chỗ hai ô chạm nhau, đường đều khớp, và không có đường nào chạy ra ngoài bản đồ (đã kiểm từng cặp ô của kết quả trên: 0 chỗ không khớp). Độ hay gặp (weight) quyết định cảnh: cỏ nhiều, đường thẳng thường gặp, ngã tư hiếm.
Các tham số
| Tham số | Ý nghĩa |
| seed | Cùng seed cùng kết quả |
| attempts | WFC không quay lui: gặp ngõ cụt (một ô không còn lựa chọn) thì làm lại với thứ tự ngẫu nhiên khác, tối đa từng ấy lần. Mặc định 20 |
| periodic | true: mép phải nối mép trái, mép dưới nối mép trên (để lát nền, lặp được) |
| allowed | Ràng buộc cố định: ô tile có được đặt ở (x, y) không |
| njin::wfc_allow(), njin::wfc_add_tile() | Dựng luật và đổi độ hay gặp bằng tay |
njin::wfc_generate() trả false khi luật quá chặt, ràng buộc mâu thuẫn hoặc hết số lần thử, và không đổi lưới đầu ra. Luôn kiểm tra giá trị trả về, và có chỗ dự phòng (một bản đồ vẽ tay, hay thử seed khác).
Kết quả của WFC cũng là njin::tile_grid, nên các luật ở trên (njin::grid_scatter(), njin::grid_autotile()...) chạy tiếp được.
Đưa vào game
njin::tilemap_from_grid() đặt lưới vào njin::tilemap, góc trên trái ở ô origin. Ô -1 của lưới không xoá ô đang có, nên chồng được nhiều lưới; muốn sinh lại từ đầu (như khi bấm R) thì gọi njin::tilemap_clear() trước, như hai ví dụ. Va chạm và animation của ô do njin::tilemap_set_shape() và njin::tilemap_animate() quyết định như mọi tilemap (xem Tilemap). result.spawn (và result.goal của platformer) là ô để đặt nhân vật.
Lưu ý
- Cùng seed, cùng bản đồ, trên mọi máy đã thử: một chuỗi băm của 12 seed cho cả ba bộ sinh ra cùng một giá trị khi biên dịch bằng gcc trên Windows (MinGW) và trên Linux (gcc 15), ở -O0, -O2 và -O3 -march=native. Chưa thử bằng MSVC hay clang. Lưu seed (số nhỏ) thay vì lưu cả bản đồ.
- Sinh chạy một lần lúc nạp màn, không mỗi frame. Đo trên bản -O2: bản đồ top-down 64 x 48 mất dưới 1 ms, màn platformer 160 x 30 dưới 0,3 ms; WFC là phần nặng nhất (một lưới 96 x 64 mất khoảng 0,2 giây), nên sinh trước khi vào màn.
- Sinh xong nên nhìn thử. Nhiễu và luật cho ra bản đồ hợp lệ, không phải bản đồ hay. Đổi seed vài chục lần và xem.
- Xem thêm
- njin::tile_grid, njin::generate_topdown(), njin::generate_platformer(), njin::grid_autotile(), njin::wfc_generate()
-
tilemap, level, platformer, topdown