Toán học, va chạm, số ngẫu nhiên có các hàm hình học để tự kiểm tra hai hình. Trang này là tầng trên: gắn njin::collider vào entity, engine tự tìm các cặp chạm nhau và báo cho game bằng event. Kèm theo là di chuyển có vật cản, truy vấn vùng và bắn tia.
Đây không phải mô phỏng vật lý: không có lực, khối lượng, nảy hay xếp chồng. Nó dành cho những thứ game hành động cần: đạn trúng quái, nhặt đồ, vùng sát thương, tường chắn, quái nhìn thấy người chơi.
#include <njin.h>
namespace {
struct player {};
struct coin {};
struct bullet {
};
struct enemy {
};
entt::entity hero = entt::null;
if (!reg.valid(e.
self) || !reg.valid(e.
other))
return;
if (reg.all_of<coin>(e.
self) && reg.all_of<player>(e.
other)) {
score++;
}
if (reg.all_of<bullet>(e.
self)) {
if (enemy *foe = reg.try_get<enemy>(e.
other); foe !=
nullptr && --foe->hp <= 0)
}
}
void spawn(njin::context &ctx) {
hero = reg.create();
reg.emplace<player>(hero);
const entt::entity wall = reg.create();
const entt::entity c = reg.create();
reg.emplace<coin>(c);
.layer = layer_pickup, .mask = layer_player,
.trigger = true});
const entt::entity foe = reg.create();
reg.emplace<enemy>(foe);
}
void move_hero(njin::context &ctx) {
}
void shoot(njin::context &ctx) {
const entt::entity b = reg.create();
reg.emplace<bullet>(b, bullet{.velocity = {600, 0}});
.mask = layer_enemy | layer_wall,
.trigger = true});
}
tr.pos += b.velocity * dt;
if (sight.hit && sight.entity == hero) {
}
}
}
void setup(njin::context &ctx) {
}
}
njin::mod_desc collision_example_module() {
return {.name =
"collision", .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
void collision_set_debug(context &ctx, bool on)
Vẽ khung mọi collider hộp và tròn trong phase_render, trên sprite: xanh lá cho vật cản,...
collision_move_result collision_move(context &ctx, entt::entity entity, vec2 delta)
Di chuyển một entity có collider, dừng lại khi đụng collider không phải trigger (kể cả ô của collider...
raycast_hit collision_raycast(const context &ctx, vec2 from, vec2 to, u32 mask=layer_all, bool include_triggers=false, entt::entity ignore=entt::null)
Bắn một tia từ from đến to, trả về vật trúng gần nhất: tầm nhìn của quái, đạn tức thời,...
@ collider_circle
Hình tròn bán kính radius.
Definition njin_collision.h:26
const char * tr(const context &ctx, const char *key)
Chuỗi đã dịch của key trong ngôn ngữ đang dùng.
vec2 normalize(vec2 v)
Vector cùng hướng, độ dài 1.
Definition _math.h:74
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::dispatcher & events(context &ctx)
Trả về dispatcher của EnTT để gửi và nhận event giữa các system.
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_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
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
float f32
Số thực 32 bit.
Definition _types.h:28
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
Hai collider bắt đầu chồng nhau.
Definition njin_collision.h:78
entt::entity self
Entity nhận event.
Definition njin_collision.h:79
entt::entity other
Entity kia.
Definition njin_collision.h:80
Mô tả một module: một nhóm system có tên.
Definition _mod.h:67
Kết quả của collision_raycast().
Definition njin_collision.h:223
Vector 2 chiều. Dùng cho vị trí, kích thước, độ dời.
Definition _types.h:39
f32 y
Thành phần dọc (trong không gian màn hình, y tăng khi đi xuống).
Definition _types.h:41
f32 x
Thành phần ngang.
Definition _types.h:40
Collider
| Trường | Ý nghĩa |
| shape | collider_box (hình hộp thẳng trục), collider_circle, hoặc collider_tiles |
| size, radius | Kích thước hộp, bán kính tròn |
| offset | Độ lệch của tâm so với transform.pos |
| layer, mask | Thuộc lớp nào, va chạm với lớp nào |
| trigger | Chỉ báo chồng nhau, không chặn đường |
| enabled | Tắt tạm mà không gỡ component |
offset, size và radius nhân với transform.scale; offset xoay theo transform.rot. Bản thân hình hộp luôn thẳng trục, không xoay.
Collider có thể nằm trên entity con (njin::child_of), ví dụ hitbox ở đầu kiếm: module collision chạy sau module hierarchy nên thấy đúng vị trí của frame này.
Lớp và mặt nạ
Mỗi collider thuộc một hoặc nhiều lớp (layer, tối đa 32, đặt tên bằng njin::layer_bit()) và chọn va chạm với những lớp nào (mask). Hai collider A và B chỉ được xét khi cả hai chiều đều cho phép:
(A.mask & B.layer) != 0 và (B.mask & A.layer) != 0
Ví dụ đạn của người chơi có mask = layer_enemy | layer_wall: nó không bao giờ trúng người chơi, dù người chơi có mask = layer_all. Đây cũng là quy tắc của Box2D.
Event
Mỗi frame, trong phase_post_update, engine tìm mọi cặp collider hộp hoặc tròn đang chồng nhau và xếp hàng event qua njin::events():
- Mỗi cặp gửi hai event, một cho mỗi bên (self là bên nhận). Handler chỉ cần hỏi "`self`
là gì, `other` là gì".
- Event được phát sau phase_post_update, nên hủy entity ngay trong handler là an toàn. Nhưng mọi event của frame đã được xếp hàng trước đó: nếu hai viên đạn cùng trúng một con quái, handler thứ nhất hủy quái thì event thứ hai vẫn tới. Luôn kiểm tra registry.valid() cho cả self và other ở đầu handler.
- exit chỉ gửi cho bên còn sống. other có thể đã bị hủy: kiểm tra registry.valid().
- trigger trong event là true nếu một trong hai là trigger.
Tìm cặp dùng một lưới đều: chỉ những collider chung ô mới được so. Cỡ ô mặc định 64, chỉnh bằng njin::collision_set_cell_size() cho gần cỡ collider phổ biến của game. Trên máy thử, bản Release, 2000 hộp di chuyển mất khoảng 1 ms mỗi frame.
Di chuyển có vật cản
njin::collision_move() dời một entity, dừng trước collider không phải trigger và trước các ô của collider_tiles, rồi ghi vị trí mới vào transform:
if (r.
hit_y && velocity.y > 0)
on_ground = true;
velocity.x = 0;
Kết quả của collision_move().
Definition njin_collision.h:116
bool hit_x
Bị chặn trên trục ngang.
Definition njin_collision.h:118
bool hit_y
Bị chặn trên trục dọc. hit_y && delta.y > 0 là đang đứng trên đất.
Definition njin_collision.h:119
- Đi trục ngang trước rồi trục dọc, nên trượt dọc theo tường.
- Mỗi trục được quét liên tục: bước dài đến đâu cũng không xuyên qua tường mỏng.
- Vật cản đang chồng sẵn lên entity bị bỏ qua, để entity bị kẹt vẫn đi ra được.
- Bỏ qua collider của các entity con trực tiếp, để vũ khí không chặn chủ.
- Hình tròn được di chuyển như hình hộp bao quanh nó.
Tilemap làm vật cản
Gắn một collider collider_tiles lên entity có njin::tilemap: mọi ô không trống thành vật cản cho njin::collision_move(), truy vấn và raycast, với layer/mask như collider thường.
@ 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
Ô tilemap không sinh event va chạm. njin::tilemap_move() vẫn dùng được khi chỉ cần va chạm với một tilemap.
Truy vấn và raycast
Truy vấn chạy ngay khi gọi, trên vị trí hiện tại, và duyệt qua mọi collider: nhanh với vài trăm collider, nhưng đừng gọi hàng nghìn lần mỗi frame. Raycast trả về vật trúng gần nhất, mặc định bỏ qua trigger, và có tham số ignore để bỏ qua người bắn.
Đám đông: hàng xóm gần nhất và đẩy nhau ra
Collider hợp với vài trăm vật. Một bầy vài nghìn con quái hay một đám đông thì cần thứ khác: chúng không cần event, chỉ cần không chồng lên nhau và biết ai ở gần. Việc đó là của njin::spatial_index, trong njin_spatial.h:
#include <njin.h>
#include <vector>
namespace {
struct zombie {
};
entt::entity hero = entt::null;
std::vector<entt::entity> who;
std::vector<njin::spatial_item> items;
std::vector<njin::vec2> push;
entt::entity aimed = entt::null;
void startup(njin::context &ctx) {
hero = reg.create();
for (int i = 0; i < 2000; i++) {
const entt::entity z = reg.create();
reg.emplace<zombie>(z);
}
}
void update(njin::context &ctx) {
who.clear();
items.clear();
tr.pos += z.velocity * dt;
who.push_back(e);
items.push_back({.pos =
tr.pos, .radius = 6.0f});
}
for (std::size_t i = 0; i < who.size(); i++)
aimed =
njin::spatial_nearest(index, {.at = target, .radius = 200.0f}, nearest, 1) == 1 ? who[nearest[0].item]
: entt::null;
}
void draw(njin::context &ctx) {
for (
auto [e, tr, z] : reg.view<
const njin::transform,
const zombie>().each())
if (reg.valid(aimed))
}
void setup(njin::context &ctx) {
}
}
int main() {
njin::context *ctx =
njin::create({.title =
"spatial", .width = 1280, .height = 720});
}
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_circle(const context &ctx, vec2 center, f32 radius, rgba color)
Vẽ hình tròn đặc.
void draw_line(const context &ctx, vec2 a, vec2 b, f32 thickness, rgba color)
Vẽ đoạn thẳng.
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.
@ phase_render
Mỗi frame, vẽ trong không gian thế giới.
Definition _mod.h:38
rng & random(context &ctx)
Bộ sinh số ngẫu nhiên dùng chung của engine.
void spatial_build(spatial_index &index, const spatial_desc &desc, const spatial_item *items, u32 count)
Dựng (lại) index từ count vật.
u32 spatial_separate(const spatial_index &index, std::vector< vec2 > &push, u32 k=6)
Tính độ đẩy để các vật chồng nhau tách ra: va chạm mềm cho đám đông.
u32 spatial_nearest(const spatial_index &index, const spatial_query &q, spatial_hit *out, u32 k)
Tìm tối đa k vật gần q.at nhất thỏa q, gần nhất trước.
@ spatial_grid
Lưới đều.
Definition njin_spatial.h:17
Bộ sinh số ngẫu nhiên PCG32: nhanh, chất lượng tốt, và lặp lại được: cùng hạt giống thì cùng dãy số.
Definition _random.h:13
f32 range(f32 lo, f32 hi)
Số thực trong [lo, hi).
Definition _random.h:50
Một vật tìm thấy.
Definition njin_spatial.h:107
Index không gian cho hàng nghìn hình tròn: tìm k vật gần nhất và đẩy các vật chồng nhau ra,...
Definition njin_spatial.h:58
Vật fixed không bị đẩy, vật khác phải đi vòng qua nó: người đang ngồi, lính đang đứng gác. Vật cùng njin::spatial_item::group không đẩy nhau, để một nhóm đứng sát nhau được. Mỗi vật chỉ cộng lực đẩy của k vật chạm gần nhất, nên giữa một cụm dày nó không bị hất văng; phần chồng còn lại tan dần qua vài frame. Cụm càng dày thì mỗi lần hỏi càng phải xét nhiều vật, nên vẫn chậm hơn khi vật rải đều.
njin::spatial_desc::kind chọn cấu trúc bên dưới. Hai loại trả cùng kết quả, chỉ khác tốc độ, nên đổi qua lại để đo trong game của bạn:
| Loại | Nhanh hơn khi |
| njin::spatial_grid (mặc định) | Vật cùng cỡ, rải khá đều trong một màn: bầy quái, đám đông, đạn. Dựng nhanh gấp 3 đến 4 lần quadtree. |
| njin::spatial_quadtree | Map rất rộng mà vật dồn thành vài cụm, và hỏi với bán kính lớn. |
Đo trên game crowd (bán kính 5, rải đều): với 50 000 người, cả bước mô phỏng kể cả dựng index và đẩy nhau hết 13,9 ms với lưới, 26,3 ms với quadtree. Trên một map 100 000 x 65 000 có 20 cụm, 5000 lần tìm 8 vật gần nhất trong bán kính 180 hết 10,7 ms với lưới, 7,2 ms với quadtree.
Dò lỗi
njin::collision_set_debug() vẽ khung mọi collider hộp và tròn: xanh lá là vật cản, vàng là trigger, xám là đang tắt.