Trang này cho nhân vật do máy điều khiển tự đi trong màn 3D: tìm đường quanh tường, lên dốc, lên bục, nhảy qua khe; và đi thành đám đông mà không đâm vào nhau. Mọi thứ khai báo trong njin_nav3d.h, dựng trên Recast và Detour (thư viện tìm đường của rất nhiều game 3D), nằm trong engine.
Cần biết trước: Đồ họa 3D (begin_3d(), vật lý 3D). Game 2D theo lưới ô thì dùng nav_grid của Làm game top-down, đơn giản hơn nhiều.
Navmesh là gì
Navmesh là các đa giác phủ lên chỗ đi được của màn chơi: mặt sàn, mặt dốc, mặt bục, đã chừa ra một khoảng bằng bán kính nhân vật quanh tường và vật cản. Tìm đường trên đó nhanh hơn trên lưới ô rất nhiều, và đường đi ra là những đoạn thẳng gấp khúc ở góc vật cản, không đi zíc zắc theo ô.
Engine dựng navmesh từ hình học tĩnh mà game đưa vào, theo cỡ của nhân vật:
| Trường của njin::navmesh3d_desc | Nghĩa |
| agent_radius | Chỗ đi được cách tường bấy nhiêu mét |
| agent_height | Chỉ chui qua chỗ trần cao hơn số này |
| agent_climb | Bậc cao nhất bước lên được |
| max_slope | Dốc nhất đi được, độ |
| cell_size, cell_height | Độ mịn khi dựng: nhỏ thì sát tường hơn, dựng lâu hơn |
| tile_size | Cạnh mỗi ô vuông của navmesh, để dựng lại từng vùng |
Dựng navmesh
Tạo navmesh, thêm hình học, rồi dựng một lần lúc nạp màn:
void navmesh3d_add_model(context &ctx, navmesh3d_handle handle, model_handle model, vec3 position, vec3 rotation={0.0f, 0.0f, 0.0f}, vec3 scale={1.0f, 1.0f, 1.0f}, u8 area=0)
Thêm các tam giác của một model, đặt ở vị trí, góc xoay (độ) và tỉ lệ như draw_model().
void navmesh3d_add_box(context &ctx, navmesh3d_handle handle, vec3 center, vec3 size, vec3 rotation={0.0f, 0.0f, 0.0f}, u8 area=0)
Thêm một hình hộp: bục, bàn, vật cản, tường.
bool navmesh3d_build(context &ctx, navmesh3d_handle handle)
Dựng toàn bộ navmesh từ hình học đã thêm.
navmesh3d_handle navmesh3d_create(context &ctx, const navmesh3d_desc &desc={})
Tạo một navmesh trống.
void navmesh3d_add_terrain(context &ctx, navmesh3d_handle handle, terrain3d_handle terrain, u8 area=0)
Thêm một địa hình (njin_world3d.h).
Định danh của một navmesh 3D, tạo bởi navmesh3d_create().
Definition _types.h:278
| Hàm | Thêm gì |
| navmesh3d_add_box() | Một hình hộp, xoay được: sàn, tường, bục, bàn |
| navmesh3d_add_mesh() | Một lưới tam giác trong thế giới (được chép lại) |
| navmesh3d_add_model() | Các tam giác của một model, đặt như draw_model() |
| navmesh3d_add_terrain() | Một địa hình; độ cao đọc lại mỗi lần dựng |
| navmesh3d_add_link() | Lối tắt không liền mặt đất: nhảy qua khe, leo thang, nhảy xuống bục |
Tam giác cần quay mặt trước lên trên (ngược chiều kim đồng hồ khi nhìn từ trên xuống, như mọi lưới của njin). Dựng mất từ vài chục mili giây (một căn phòng) đến vài giây (bản đồ lớn với cell_size nhỏ).
Khi màn chơi đổi (sửa địa hình, mở cửa, đặt thêm vật cản bằng navmesh3d_add_box()), navmesh3d_rebuild() dựng lại các ô vuông trong một vùng, không phải cả bản đồ. Tác tử đang đi tự tìm lại đường.
Hỏi navmesh
| Hàm | Trả lời |
| navmesh3d_path() | Đường từ A đến B: các điểm gấp khúc. Không đến được B thì dừng ở chỗ gần B nhất |
| navmesh3d_nearest() | Điểm gần nhất trên navmesh |
| navmesh3d_raycast() | Đi thẳng trên mặt đất từ A về B, dừng ở mép đầu tiên: có thấy nhau không, lao tới được bao xa |
| navmesh3d_random_point() | Một điểm ngẫu nhiên trên cả navmesh |
| navmesh3d_random_point_near() | Một điểm ngẫu nhiên đi tới được, cách một tâm không quá bán kính: quái đi lang thang quanh ổ |
| navmesh3d_draw_debug() | Vẽ các đa giác bằng gizmo, để xem engine thấy chỗ nào đi được |
std::vector<njin::vec3> path;
for (size_t i = 0; i + 1 < path.size(); i++)
void gizmo_line3d(context &ctx, vec3 a, vec3 b, rgba color=colors::green, f32 duration=0.0f)
Đoạn thẳng 3D.
bool navmesh3d_path(const context &ctx, navmesh3d_handle handle, vec3 from, vec3 to, std::vector< vec3 > &out)
Tìm đường từ from đến to: các điểm gấp khúc, đầu là điểm gần from nhất trên navmesh,...
constexpr rgba yellow
Vàng.
Definition _math.h:239
Điểm của đường đi và vị trí tác tử nằm trên mặt đất. Trên địa hình đã thêm bằng navmesh3d_add_terrain(), độ cao lấy đúng theo terrain3d_height(), và đoạn nào cắt qua đồi thì có thêm điểm giữa để đường bám mặt đất (cách không quá 5 cm). Trên hình học khác (model, hộp nghiêng) độ cao theo lưới chi tiết của navmesh, có thể sai vài cm; muốn chính xác thì lấy độ cao bằng một tia xuống, hoặc cho tác tử lái một nhân vật vật lý (phần dưới).
Đám đông
Tác tử (nav3d_agent_add()) là một nhân vật tự tìm đường và tự đi: game chỉ đặt đích. Các tác tử trên cùng một navmesh né nhau trên đường đi và tách nhau ra khi đứng sát, nên một nhóm quái đuổi theo người chơi không dồn thành một cục. Engine cập nhật chúng trong phase_post_update theo delta().
attack();
vec3 nav3d_agent_position(const context &ctx, nav3d_agent_handle agent)
Vị trí hiện tại của tác tử (trên mặt navmesh).
bool nav3d_agent_set_target(context &ctx, nav3d_agent_handle agent, vec3 target)
Đặt đích: tác tử tìm đường và đi tới đó (đích được kéo về chỗ gần nhất trên navmesh).
vec3 nav3d_agent_velocity(const context &ctx, nav3d_agent_handle agent)
Vận tốc hiện tại của tác tử: hướng nó đang đi, để quay mặt và chọn animation.
nav3d_agent_handle nav3d_agent_add(context &ctx, navmesh3d_handle navmesh, const nav3d_agent_desc &desc)
Thêm một tác tử.
bool nav3d_agent_arrived(const context &ctx, nav3d_agent_handle agent)
Tác tử đã tới đích chưa (cách đích không quá arrive_distance).
Định danh của một tác tử đi trên navmesh, tạo bởi nav3d_agent_add().
Definition _types.h:286
Vector 3 chiều.
Definition _types.h:47
nav3d_agent_stop() bỏ đích, nav3d_agent_teleport() đặt sang chỗ khác ngay, nav3d_agent_remove() bỏ tác tử.
Lái nhân vật vật lý. Đặt nav3d_agent_desc::character là một njin::character3d_handle: mỗi frame vận tốc của tác tử thành vận tốc ngang của nhân vật, nhân vật rơi theo trọng lực khi không đứng trên sàn, và chỗ nhân vật thật sự đến (bị đẩy, va vào thùng) thành chỗ của tác tử. Game đừng tự đặt vận tốc cho nhân vật đó nữa.
Vùng và chi phí
Mỗi chỗ đi được thuộc một vùng, số từ 0 đến 15 (njin::nav3d_max_areas). Vùng 0 là mặt đất thường; các số khác game tự đặt nghĩa: đường cái, cỏ, đầm lầy, nước nông, ô cửa. Đánh dấu vùng theo hai cách:
- Cả một mảng hình học: tham số area cuối của navmesh3d_add_mesh(), navmesh3d_add_model(), navmesh3d_add_box(), navmesh3d_add_terrain() và navmesh3d_add_link() (lối tắt cũng có vùng: nhảy qua khe thì đắt).
- Một khối đứng trên bản đồ: navmesh3d_add_area() với tâm, cỡ, góc xoay quanh trục đứng và số vùng. Navmesh đã dựng thì các ô vuông khối chạm vào được dựng lại ngay; navmesh3d_remove_area() bỏ khối.
Bộ lọc (njin::nav3d_filter) nói cách tìm đường nhìn các vùng: cost[vùng] là giá mỗi mét (1 là bình thường, 8 là một mét ở đó tốn như tám mét đường thường) và excluded là các vùng cấm hẳn (bit 1 << vùng). Mỗi navmesh giữ 16 bộ lọc, đặt bằng navmesh3d_set_filter(); đổi bộ lọc không cần dựng lại navmesh, và tác tử đang đi theo nó tìm lại đường ngay. Bộ lọc 0 là bộ lọc mặc định: navmesh3d_path() không chọn bộ lọc dùng nó. Tác tử chọn bộ lọc bằng nav3d_agent_desc::filter, đổi bằng nav3d_agent_set_filter().
f.excluded = 1u << door;
std::vector<njin::vec3> path;
void navmesh3d_set_filter(context &ctx, navmesh3d_handle handle, i32 index, const nav3d_filter &filter)
Đặt bộ lọc số index (0..15) của navmesh.
i32 navmesh3d_add_area(context &ctx, navmesh3d_handle handle, vec3 center, vec3 size, f32 yaw, u8 area)
Đánh dấu một khối đứng là một vùng (0..15): mọi chỗ đi được bên trong hộp xoay quanh trục đứng (đường...
std::uint8_t u8
Số nguyên không dấu 8 bit.
Definition _types.h:20
Cách tìm đường đánh giá các vùng: chi phí đi qua mỗi vùng và vùng nào cấm.
Definition njin_nav3d.h:55
f32 cost[nav3d_max_areas]
Chi phí mỗi mét của từng vùng, chỉ số là số vùng. Phải lớn hơn 0.
Definition njin_nav3d.h:57
Khi hai mặt nằm sát nhau (đường cái phủ lên sàn), vùng số lớn hơn thắng. Khối của navmesh3d_add_area() luôn thắng vùng của hình học bên dưới.
Nhiều cỡ tác tử
Khoảng chừa quanh tường (agent_radius) được tính lúc dựng, nên mỗi cỡ tác tử cần một navmesh. navmesh3d_clone() chép hình học, vùng, lối tắt, vật cản và bộ lọc của một navmesh sang navmesh mới dựng theo cỡ khác; dựng cả hai, rồi thêm tác tử to vào navmesh to. Hình học, vùng hay vật cản thêm về sau thì thêm cho từng navmesh. Tác tử trên hai navmesh khác nhau không né nhau.
Vật cản di động
navmesh3d_add_obstacle() đặt một hộp (hay hình trụ, khi radius lớn hơn 0) lên navmesh: chỗ nó đứng không còn đi được, cách nó một khoảng agent_radius như tường. navmesh3d_move_obstacle() dời nó, navmesh3d_remove_obstacle() bỏ nó. Không cần thêm hình học hay gọi navmesh3d_rebuild(): engine dựng lại các ô vuông nó chạm vào ở các frame sau, tối đa navmesh3d_desc::obstacle_tiles_per_frame ô mỗi frame (mặc định 4), và tác tử tìm đường vòng qua nó. navmesh3d_pending_tiles() cho biết còn bao nhiêu ô chờ. Mỗi ô vuông 8 m dựng lại mất vài mili giây, nên dời vật cản khi nó thật sự đổi chỗ (thùng dừng lại, cửa đóng xong), không phải mỗi frame khi nó đang trượt.
Ví dụ đầy đủ
Một căn phòng có tường và bục có dốc; tám tác tử đi tới chỗ chuột trái bấm; đường từ con đầu tới chỗ chuột được vẽ thử.
#include <njin.h>
#include <vector>
namespace {
using namespace njin;
const camera3d camera{.position = {0.0f, 22.0f, 18.0f}, .target = {0.0f, 0.0f, 0.0f}, .fovy = 50.0f};
struct box {
vec3 center, size, rotation;
};
const box level[] = {
{{0.0f, -0.5f, 0.0f}, {30.0f, 1.0f, 30.0f}, {}},
{{-4.0f, 1.0f, 0.0f}, {1.0f, 2.0f, 12.0f}, {}},
{{5.0f, 1.0f, -4.0f}, {8.0f, 2.0f, 1.0f}, {}},
{{8.0f, 1.0f, 8.0f}, {6.0f, 2.0f, 6.0f}, {}},
{{2.2f, 0.9f, 8.0f}, {6.0f, 0.2f, 3.0f}, {0.0f, 0.0f, 20.0f}},
};
std::vector<nav3d_agent_handle> crowd;
std::vector<vec3> preview;
void load(context &ctx) {
nav =
navmesh3d_create(ctx, {.agent_radius = 0.4f, .agent_height = 1.8f, .agent_climb = 0.4f});
for (const box &b : level) {
}
for (
i32 i = 0; i < 8; i++)
}
void update(context &ctx) {
return;
}
void render(context &ctx) {
light3d_set(ctx, {.direction = {-0.4f, -1.0f, -0.3f}, .shadows =
true, .shadow_range = 40.0f});
for (const box &b : level)
rgba{0.7f, 0.68f, 0.62f, 1.0f});
}
for (
usize i = 0; i + 1 < preview.size(); i++)
}
void setup(context &ctx) {
}
}
mod_desc crowd_module() {
return {.name =
"crowd", .setup = setup}; }
ray3d camera3d_ray(const context &ctx, const camera3d &camera, vec2 screen)
Tia từ camera đi qua một điểm trên màn hình, để chọn vật dưới chuột: camera3d_ray(ctx,...
void light3d_set(context &ctx, const light3d &light)
Đặt ánh sáng chung.
void begin_3d(context &ctx, const camera3d &camera)
Bắt đầu vẽ 3D qua camera.
void draw_shape3d(const context &ctx, const shape3d &shape, rgba color)
Vẽ một hình SDF mịn.
ray3d_hit ray3d_plane(const ray3d &ray, vec3 point, vec3 normal)
Tia với mặt phẳng vô hạn qua point, pháp tuyến normal.
void end_3d(context &ctx)
Kết thúc vẽ 3D và quay về vẽ 2D trong không gian thế giới.
void draw_capsule3d(const context &ctx, vec3 from, vec3 to, f32 radius, rgba color)
Vẽ viên nang (capsule) đặc bằng lưới tam giác: hình trụ nối hai điểm, hai đầu tròn.
@ shape3d_box
Hộp: size, bo cạnh theo rounding.
Definition njin_3d.h:250
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.
@ 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
void navmesh3d_draw_debug(context &ctx, navmesh3d_handle handle, rgba color={0.2f, 0.8f, 1.0f, 1.0f})
Vẽ các đa giác của navmesh bằng gizmo (njin_gizmo.h) trong frame này: mép đa giác,...
body3d_handle body3d_create(context &ctx, const body3d_desc &desc)
Tạo một body vật lý 3D.
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
float f32
Số thực 32 bit.
Definition _types.h:28
constexpr rgba blue
Xanh dương.
Definition _math.h:238
Camera phối cảnh cho begin_3d().
Definition njin_3d.h:15
Mô tả một module: một nhóm system có tên.
Definition _mod.h:67
Kết quả khi tia chạm một vật.
Definition njin_3d.h:847
vec3 point
Điểm chạm.
Definition njin_3d.h:850
bool hit
Tia có chạm không.
Definition njin_3d.h:848
Một tia trong thế giới 3D, dùng để chọn vật bằng chuột, bắn đạn, kiểm tra tầm nhìn.
Definition njin_3d.h:841
Màu gồm đỏ, xanh lá, xanh dương và alpha.
Definition _types.h:64
Vùng, bộ lọc, hai cỡ tác tử và một vật cản di động: lính gác đi qua cửa, tránh đầm lầy, đi đường cái; con quái to có navmesh riêng; phím K lấy chìa khóa của lính gác.
#include <njin.h>
namespace {
using namespace njin;
constexpr u8 area_road = 1;
constexpr u8 area_swamp = 2;
constexpr u8 area_door = 3;
constexpr i32 villager = 0;
void load(context &ctx) {
navmesh3d_add_area(ctx, small_nav, {-4.0f, 0.0f, 0.0f}, {10.0f, 4.0f, 14.0f}, 0.0f, area_swamp);
navmesh3d_add_area(ctx, small_nav, {-4.0f, 0.0f, 9.0f}, {30.0f, 4.0f, 3.0f}, 0.0f, area_road);
navmesh3d_add_area(ctx, small_nav, {6.0f, 0.0f, -0.5f}, {1.0f, 4.0f, 1.0f}, 0.0f, area_door);
f.
cost[area_swamp] = 8.0f;
f.cost[area_road] = 0.6f;
f.excluded = 1u << area_door;
f.excluded = 0;
big_nav =
navmesh3d_clone(ctx, small_nav, {.agent_radius = 1.2f, .agent_height = 3.0f});
walker =
nav3d_agent_add(ctx, small_nav, {.position = {-14.0f, 0.0f, 0.0f}, .filter = guard});
troll =
nav3d_agent_add(ctx, big_nav, {.position = {-14.0f, 0.0f, -4.0f}, .radius = 1.2f, .max_speed = 2.5f});
}
void update(context &ctx) {
if (now != side) {
side = now;
}
}
void render(context &ctx) {
begin_3d(ctx,
camera3d{.position = {0.0f, 30.0f, 22.0f}, .target = {0.0f, 0.0f, 0.0f}, .fovy = 50.0f});
}
void setup(context &ctx) {
}
}
mod_desc areas_module() {
return {.name =
"areas", .setup = setup}; }
void draw_sphere3d(const context &ctx, vec3 center, f32 radius, rgba color)
Vẽ hình cầu đặc.
i32 navmesh3d_add_obstacle(context &ctx, navmesh3d_handle handle, const nav3d_obstacle_desc &desc)
Thêm một vật cản.
void nav3d_agent_set_filter(context &ctx, nav3d_agent_handle agent, i32 filter)
Đổi bộ lọc tác tử tìm đường theo (người lính có chìa khóa đi được qua cửa khóa).
navmesh3d_handle navmesh3d_clone(context &ctx, navmesh3d_handle source, const navmesh3d_desc &desc)
Một navmesh mới cùng hình học, vùng đánh dấu, lối tắt, vật cản và bộ lọc với source,...
bool navmesh3d_move_obstacle(context &ctx, navmesh3d_handle handle, i32 id, vec3 position, f32 yaw=0.0f)
Dời một vật cản: dựng lại các ô vuông chỗ cũ và chỗ mới.
f32 elapsed(const context &ctx)
Thời gian đã trôi qua kể từ lúc mở cửa sổ, tính bằng giây.
constexpr rgba green
Xanh lá.
Definition _math.h:237
Từ Lua
Script (Script Lua) gọi được njin.nav3d_path(navmesh, from, to) (bảng các điểm, hay nil), njin.nav3d_set_target(agent, target), njin.nav3d_stop, njin.nav3d_position, njin.nav3d_velocity và njin.nav3d_arrived, với navmesh và agent là số id của handle mà phía C++ đưa sang (ví dụ bằng script_set_global()). Vùng và vật cản:
- njin.nav3d_path(navmesh, from, to, filter): tìm đường theo bộ lọc số filter.
- njin.nav3d_set_filter(navmesh, index, {cost = {[2] = 8}, exclude = {3}}), njin.nav3d_agent_filter(agent, index).
- njin.nav3d_add_area(navmesh, center, size, yaw, area) trả về số của khối; njin.nav3d_remove_area(navmesh, id).
- njin.nav3d_add_obstacle(navmesh, {position = ..., size = ..., yaw = 0, radius = 0}) trả về số của vật cản; njin.nav3d_move_obstacle(navmesh, id, position, yaw), njin.nav3d_remove_obstacle(navmesh, id).
Giới hạn
- Vật cản cắt navmesh bằng cách dựng lại cả ô vuông nó chạm vào (vài mili giây mỗi ô 8 m), không phải bằng tile cache của Detour: hợp với thùng, xe đậu, cửa; không hợp với hàng chục vật cản cùng chạy mỗi frame.
- Các tác tử chỉ né nhau trên cùng một navmesh, nên tác tử to (navmesh to) và tác tử nhỏ không né nhau.
- Vùng chỉ có 16 số (0 đến 15).