Nếu đó chỉ là một trò chơi lát gạch đơn giản trên lưới như trò chơi chiến lược theo lượt, thì một cái gì đó như thế này:
struct Tile
{
// Stores the first entity (enemy, NPC, item, etc) on the tile.
int first_entity;
...
};
struct Entity
{
// Stores the next entity on the same tile or
// the next free entity index to reclaim if
// this entity has been freed/removed.
int next;
...
};
struct Row
{
// Stores all the tiles on the row.
vector<Tile> tiles;
// Stores all the entities on the row.
vector<Entity> entities;
// Points to the first free entity index
// to reclaim on insertion.
int first_free;
};
struct Map
{
// Stores all the rows in the map.
vector<Row> rows;
};
Một số người có thể tự hỏi tại sao tôi chọn lưu trữ các vectơ riêng cho từng hàng bản đồ. Đó là để cải thiện địa phương không gian khi chúng ta đi qua các thực thể đứng trên một ô nhất định. Khi chúng tôi lưu trữ một vectơ riêng cho mỗi hàng, thì tất cả các thực thể cho hàng đó có thể vừa với L1 hoặc L2, trong khi chúng thậm chí có thể không vừa với L3 nếu chúng tôi lưu trữ một thùng chứa thực thể cho tất cả các thực thể trong toàn bản đồ. Điều đó vẫn có xu hướng khá rẻ so với, lưu trữ một vectơ riêng trên mỗi ô.
Để có được, giả sử, gạch tại (102, 72)
, chúng tôi làm điều này:
Row& row = map.rows[72];
Tile& tile = row.tiles[102];
Để duyệt qua các thực thể trên ô, chúng tôi thực hiện:
int entity = tile.first_entity;
while (entity != -1)
{
// Do something with the entity on the tile.
...
// Advance to the next entity on the tile.
entity = row.entities[entity].next;
}
Đương nhiên, việc triển khai loại "container riêng trên mỗi hàng" có lợi nhất, các mẫu truy cập ô của bạn nên cố gắng xử lý tất cả các cột quan tâm cho một hàng trước khi bạn chuyển sang hàng tiếp theo, không quá ngoằn ngoèo qua lại từ một hàng tiếp theo và trở lại một lần nữa.
Chèn một thực thể vào ô sẽ như thế này:
int Map::insert_entity(Entity ent, int col_idx, int row_idx)
{
Row& row = rows[row_idx];
int ent_idx = row.first_free;
if (ent_idx != -1)
{
row.first_free = row.entities[ent_idx].next;
row.entities[ent_idx] = ent;
}
else
{
ent_idx = static_cast<int>(row.entities.size());
row.entities.push_back(ent);
}
Tile& tile = row.tiles[col_idx];
row.entities[ent_idx].next = tile.first_entity;
tile.first_entity = ent_idx;
return ent_idx;
}
... và xóa:
void Map::remove_entity(int ent_idx, int col_idx, int row_idx)
{
Row& row = rows[row_idx];
Tile& tile = row.tiles[col_idx];
if (tile.first_entity = ent_idx)
tile.first_entity = row.entities[ent_idx].next;
row.entities[ent_idx].next = row.first_free;
row.first_free = ent_idx;
}
Lý do chính tôi thích giải pháp này là chúng tôi tránh lưu trữ quá nhiều vectơ (ví dụ: một vectơ trên mỗi ô: quá nhiều cho bản đồ lớn), nhưng không quá ít khi lặp qua các thực thể trên một ô cụ thể dẫn đến những bước tiến tuyệt vời trên địa chỉ bộ nhớ không gian và rất nhiều bộ nhớ cache bỏ lỡ. Một vector thực thể trên mỗi hàng tạo ra sự cân bằng tốt đẹp ở đó.
Điều này giả định rằng bạn có những thứ như tòa nhà, kẻ thù và vật phẩm và rương kho báu và người chơi đứng trên gạch và rất nhiều thời gian trong logic trò chơi đang truy cập vào các thực thể đứng trên các ô đó cũng như kiểm tra xem các thực thể đó đang ở đâu một gạch nhất định. Mặt khác, tôi sử dụng cách tiếp cận mảng 1D với một vectơ duy nhất cho tất cả các ô vì đó sẽ là cách hiệu quả nhất cho việc truy cập các ô. Sau đó, bạn có thể nhận được một ô bằng cách sử dụng: tiles[row*num_cols+col]
Sử dụng mảng một chiều nếu nghi ngờ vì nó sẽ cho phép bạn duyệt mọi thứ theo thứ tự tuần tự đơn giản mà không cần các vòng lặp lồng nhau và chỉ yêu cầu một phân bổ heap để phân bổ toàn bộ.
Nói chung, mảng động riêng biệt trên mỗi hàng là thứ tôi đã tìm thấy để giảm bộ nhớ cache rất nhiều trong trường hợp lưới của bạn đang lưu trữ các phần tử bên trong nó. Tất nhiên, nếu nó không và lưới của bạn giống như một hình ảnh chứa pixel, thì việc sử dụng một mảng động riêng biệt trên mỗi hàng là vô nghĩa. Như một điểm chuẩn gần đây nơi tôi đã tối ưu hóa một cái gì đó giống như lưới theo cách này (trước khi nó chỉ sử dụng một mảng khổng lồ cho mọi thứ; tôi đã tối ưu hóa nó để lưu trữ một mảng động riêng biệt trên mỗi hàng sau khi thấy nhiều lỗi bộ nhớ cache trong vtune):
Trước:
--------------------------------------------
- test_grid
--------------------------------------------
time passed for 'insert': {1.799000 secs}
mem use after 'insert': 479,508,224 bytes
8560 cells, 1000000 rects
finished test_grid: {1.919000 secs}
Sau:
--------------------------------------------
- test_grid
--------------------------------------------
time passed for 'insert': {0.310000 secs}
mem use after 'insert': 410,546,720 bytes
8560 cells, 1000000 rects
finished test_grid: {0.361000 secs}
Và tôi đã sử dụng cùng một loại chiến lược được mô tả ở trên. Như một phần thưởng, bạn cũng có thể thấy nó giảm mức sử dụng bộ nhớ vì các vectơ lưu trữ các thực thể có xu hướng phù hợp chặt chẽ hơn nếu bạn lưu trữ một hàng trên mỗi hàng thay vì một cho toàn bộ bản đồ.
Lưu ý rằng thử nghiệm trên để chèn một triệu thực thể vào lưới có vẻ như mất nhiều thời gian và bộ nhớ rất nhiều ngay cả sau khi tối ưu hóa. Đó là bởi vì mỗi thực thể tôi đang chèn có nhiều ô, trung bình khoảng 100 ô cho mỗi thực thể (kích thước trung bình 10 x 10). Vì vậy, tôi đang chèn mỗi triệu triệu thực thể vào trung bình 100 ô lưới, điều này giống như chèn 100 triệu thực thể hơn 1 triệu thực thể. Đó là căng thẳng thử nghiệm một trường hợp bệnh lý. Nếu tôi chỉ chèn một triệu thực thể chiếm 1 ô, tôi có thể thực hiện việc đó trong một phần nghìn giây và chỉ cần sử dụng khoảng 16 megabyte bộ nhớ.
Trong trường hợp của tôi, tôi thậm chí thường phải làm cho các trường hợp bệnh lý có hiệu quả kể từ khi tôi làm việc trong VFX thay vì chơi game. Tôi không thể nói với các nghệ sĩ, "Làm cho nội dung của bạn theo cách này cho công cụ này,"vì toàn bộ quan điểm của VFX là để cho các nghệ sĩ tạo ra nội dung theo ý họ muốn. Sau đó, họ tối ưu hóa nó trước khi xuất sang công cụ yêu thích của họ, nhưng tôi phải xử lý những thứ không được tối ưu hóa, điều đó có nghĩa là tôi thường phải xử lý hiệu quả các trường hợp bệnh lý, như một quãng tám phải xử lý hiệu quả các hình tam giác lớn bao trùm toàn bộ khung cảnh kể từ đó các nghệ sĩ tạo ra nội dung như vậy và thường xuyên (thường xuyên hơn nhiều so với người ta có thể mong đợi). Vì vậy, dù sao đi nữa, bài kiểm tra trên đang thử nghiệm điều gì đó không bao giờ xảy ra và đó là lý do tại sao phải mất gần một phần ba giây để chèn một triệu thực thể, nhưng trong trường hợp của tôi, những điều "không bao giờ nên xảy ra" luôn luôn xảy ra. Vì vậy, trường hợp bệnh lý không phải là trường hợp hiếm gặp đối với tôi,
Là một phần thưởng phụ, điều này cũng cho phép bạn chèn và loại bỏ các thực thể cho nhiều hàng song song bằng cách sử dụng đa luồng mà không bị khóa, vì giờ đây bạn có thể làm điều đó một cách an toàn khi mỗi hàng có một thùng chứa thực thể riêng biệt với điều kiện hai luồng không cố gắng chèn / xóa công cụ đến / từ cùng một hàng.