Container hiệu quả nhất để lưu trữ các đối tượng trò chơi động là gì? [đóng cửa]


20

Tôi đang thực hiện một game bắn súng góc nhìn người thứ nhất và tôi biết về rất nhiều loại container khác nhau nhưng tôi muốn tìm thấy container hiệu quả nhất để lưu trữ các đối tượng động sẽ được thêm và xóa khỏi trò chơi khá thường xuyên. Đạn EX.

Tôi nghĩ rằng trong trường hợp đó, nó sẽ là một danh sách để bộ nhớ không tiếp giáp và không bao giờ có bất kỳ kích thước nào đang diễn ra. Nhưng sau đó tôi cũng đang suy nghĩ bằng cách sử dụng bản đồ hoặc bộ. Nếu bất cứ ai có bất kỳ thông tin hữu ích, tôi sẽ đánh giá cao nó.

Bằng cách này, tôi đang viết điều này trong c ++.

Ngoài ra tôi đã đưa ra một giải pháp mà tôi nghĩ sẽ làm việc.

Để bắt đầu, tôi sẽ phân bổ một vectơ có kích thước lớn .. giả sử 1000 đối tượng. Tôi sẽ theo dõi chỉ số được thêm vào cuối cùng trong vectơ này để tôi biết nơi kết thúc của các đối tượng. Sau đó, tôi cũng sẽ tạo một hàng đợi giữ các chỉ số của tất cả các đối tượng được "xóa" khỏi vectơ. (Không có xóa thực tế sẽ được thực hiện, tôi sẽ chỉ biết rằng khe đó miễn phí). Vì vậy, nếu hàng đợi trống, tôi sẽ thêm vào chỉ mục được thêm vào cuối cùng trong vectơ + 1, nếu không tôi sẽ thêm vào chỉ mục của vectơ ở phía trước hàng đợi.


Bất kỳ ngôn ngữ cụ thể bạn đang nhắm mục tiêu?
Phill.Zitt

Câu hỏi này quá khó để trả lời nếu không có nhiều thông tin cụ thể tốt hơn, bao gồm nền tảng phần cứng, ngôn ngữ / khung, v.v.
PlayDeezGames

1
Mẹo nhỏ, bạn có thể lưu trữ danh sách miễn phí trong bộ nhớ của các phần tử đã xóa (vì vậy bạn không cần thêm hàng đợi).
Jeff Gates

2
Có một câu hỏi trong câu hỏi này?
Trevor Powell

Lưu ý rằng bạn không phải theo dõi chỉ số lớn nhất cũng như không phải phân bổ một số yếu tố. std :: vector chăm sóc tất cả những thứ đó cho bạn.
API-Beast

Câu trả lời:


33

Câu trả lời là luôn luôn sử dụng một mảng hoặc std :: vector. Các loại như danh sách được liên kết hoặc std :: map thường hoàn toàn khủng khiếp trong các trò chơi và điều đó chắc chắn bao gồm các trường hợp như bộ sưu tập các đối tượng trò chơi.

Bạn nên lưu trữ các đối tượng (không phải con trỏ tới chúng) trong mảng / vector.

Bạn muốn bộ nhớ tiếp giáp. Bạn thực sự thực sự muốn nó. Việc lặp lại bất kỳ dữ liệu nào trong bộ nhớ không liền kề sẽ tạo ra rất nhiều lỗi nhớ cache nói chung và loại bỏ khả năng trình biên dịch và CPU thực hiện tìm nạp trước bộ đệm hiệu quả. Điều này một mình có thể giết chết hiệu suất.

Bạn cũng muốn tránh phân bổ bộ nhớ và thỏa thuận. Chúng rất chậm, ngay cả với bộ cấp phát bộ nhớ nhanh. Tôi đã thấy các trò chơi tăng gấp 10 lần FPS chỉ bằng cách loại bỏ vài trăm phân bổ bộ nhớ cho mỗi khung hình. Có vẻ như nó không tệ đến thế, nhưng nó có thể.

Cuối cùng, hầu hết các cấu trúc dữ liệu mà bạn quan tâm để quản lý các đối tượng trò chơi có thể được gắn hiệu quả hơn nhiều trên một mảng hoặc một vectơ so với chúng có thể với một cây hoặc một danh sách.

Ví dụ, để xóa các đối tượng trò chơi, bạn có thể sử dụng trao đổi và pop. Dễ dàng thực hiện với một cái gì đó như:

std::swap(objects[index], objects.back());
objects.pop_back();

Bạn cũng có thể đánh dấu các đối tượng là đã xóa và đưa chỉ mục của chúng vào danh sách miễn phí cho lần tiếp theo bạn cần tạo một đối tượng mới, nhưng thực hiện trao đổi và pop là tốt hơn. Nó cho phép bạn thực hiện một vòng lặp đơn giản trên tất cả các đối tượng sống mà không phân nhánh ngoài vòng lặp. Đối với tích hợp vật lý đạn và tương tự, đây có thể là một hiệu suất tăng đáng kể.

Quan trọng hơn, bạn có thể tìm thấy các đối tượng với một cặp tra cứu bảng đơn giản từ một duy nhất ổn định đang sử dụng cấu trúc bản đồ vị trí.

Các đối tượng trò chơi của bạn có một chỉ mục trong mảng chính của chúng. Chúng có thể được tra cứu rất hiệu quả chỉ với chỉ số này (nhanh hơn nhiều so với bản đồ hoặc thậm chí là bảng băm). Tuy nhiên, chỉ mục không ổn định do hoán đổi và bật khi xóa đối tượng.

Một bản đồ vị trí yêu cầu hai lớp gián tiếp, nhưng cả hai đều là các tra cứu mảng đơn giản với các chỉ số không đổi. Họ rất nhanh . Rất nhanh.

Ý tưởng cơ bản là bạn có ba mảng: danh sách đối tượng chính, danh sách chỉ định của bạn và danh sách miễn phí cho danh sách gián tiếp. Danh sách đối tượng chính của bạn chứa các đối tượng thực tế của bạn, trong đó mỗi đối tượng biết ID duy nhất của riêng nó. ID duy nhất bao gồm một chỉ mục và thẻ phiên bản. Danh sách chỉ định đơn giản là một mảng các chỉ mục cho danh sách đối tượng chính. Danh sách miễn phí là một chồng các chỉ số vào danh sách gián tiếp.

Khi bạn tạo một đối tượng trong danh sách chính, bạn sẽ tìm thấy một mục không được sử dụng trong danh sách không xác định (sử dụng danh sách miễn phí). Mục trong danh sách chỉ định chỉ đến mục không được sử dụng trong danh sách chính. Bạn khởi tạo đối tượng của mình ở vị trí đó và đặt ID duy nhất của nó thành chỉ mục của mục nhập danh sách không xác định mà bạn đã chọn và thẻ phiên bản hiện có trong thành phần danh sách chính, cộng với một.

Khi bạn phá hủy một đối tượng, bạn thực hiện trao đổi và bật như bình thường, nhưng bạn cũng tăng số phiên bản. Sau đó, bạn cũng thêm chỉ mục danh sách không xác định (một phần của ID duy nhất của đối tượng) vào danh sách miễn phí. Khi di chuyển một đối tượng như là một phần của trao đổi và pop, bạn cũng cập nhật mục nhập của nó trong danh sách chỉ định đến vị trí mới của nó.

Ví dụ mã giả:

Object:
  int index
  int version
  other data

SlotMap:
  Object objects[]
  int slots[]
  int freelist[]
  int count

  Get(id):
    index = indirection[id.index]
    if objects[index].version = id.version:
      return &objects[index]
    else:
      return null

  CreateObject():
    index = freelist.pop()

    objects[count].index = id
    objects[count].version += 1

    indirection[index] = count

    Object* object = &objects[count].object
    object.initialize()

    count += 1

    return object

  Remove(id):
    index = indirection[id.index]
    if objects[index].version = id.version:
      objects[index].version += 1
      objects[count - 1].version += 1

      swap(objects[index].data, objects[count - 1].data)

Lớp cảm ứng cho phép bạn có một mã định danh ổn định (chỉ mục vào lớp cảm ứng, trong đó các mục không di chuyển) cho một tài nguyên có thể di chuyển trong quá trình nén (danh sách đối tượng chính).

Thẻ phiên bản cho phép bạn lưu trữ ID vào một đối tượng có thể bị xóa. Ví dụ: bạn có id (10,1). Đối tượng có chỉ số 10 bị xóa (giả sử, viên đạn của bạn chạm vào một đối tượng và bị phá hủy). Đối tượng ở vị trí đó của bộ nhớ trong danh sách đối tượng chính sau đó có số phiên bản bị lỗi, cho nó (10,2). Nếu bạn cố gắng tra cứu lại (10,1) từ ID cũ, việc tra cứu sẽ trả về đối tượng đó thông qua chỉ số 10, nhưng có thể thấy rằng số phiên bản đã thay đổi, do đó ID không còn hợp lệ.

Đây là cấu trúc dữ liệu nhanh nhất tuyệt đối mà bạn có thể có với ID ổn định cho phép các đối tượng di chuyển trong bộ nhớ, điều này rất quan trọng đối với vị trí dữ liệu và sự gắn kết bộ đệm. Điều này nhanh hơn bất kỳ việc thực hiện bảng băm nào có thể; một bảng băm ít nhất cần phải tính toán một hàm băm (nhiều hướng dẫn hơn tra cứu bảng) và sau đó phải tuân theo chuỗi băm (một danh sách được liên kết trong trường hợp khủng khiếp của std :: unordered_map hoặc danh sách địa chỉ mở trong bất kỳ triển khai không ngu ngốc nào của bảng băm), và sau đó phải thực hiện so sánh giá trị trên mỗi khóa (không đắt hơn, nhưng có thể rẻ hơn, so với kiểm tra thẻ phiên bản). Một bảng băm rất tốt (không phải là bảng trong bất kỳ triển khai STL nào, vì STL bắt buộc một bảng băm tối ưu hóa cho các trường hợp sử dụng khác nhau so với trò chơi của bạn cho danh sách đối tượng trò chơi) có thể tiết kiệm được một lần,

Có nhiều cải tiến khác nhau mà bạn có thể thực hiện đối với thuật toán cơ sở. Ví dụ, sử dụng một cái gì đó như std :: deque cho danh sách đối tượng chính; thêm một lớp gián tiếp, nhưng cho phép các đối tượng được chèn vào danh sách đầy đủ mà không làm mất hiệu lực bất kỳ con trỏ tạm thời nào bạn có được từ slotmap.

Bạn cũng có thể tránh lưu trữ chỉ mục bên trong đối tượng, vì chỉ mục có thể được tính từ địa chỉ bộ nhớ của đối tượng (đối tượng này) và thậm chí tốt hơn là chỉ cần thiết khi xóa đối tượng trong trường hợp bạn đã có id của đối tượng (và do đó chỉ số) như là một tham số.

Xin lỗi vì đã viết lên; Tôi không cảm thấy đó là mô tả rõ ràng nhất có thể. Nó muộn và thật khó để giải thích mà không mất nhiều thời gian hơn tôi có trên các mẫu mã.


1
Bạn đang giao dịch với một deref bổ sung và phân bổ cao / chi phí miễn phí (trao đổi) mỗi lần truy cập cho bộ lưu trữ 'compact'. Theo kinh nghiệm của tôi với các trò chơi video, đó là một giao dịch tồi :) YMMV tất nhiên.
Jeff Gates

1
Bạn không thực sự làm điều bình thường trong các tình huống thực tế. Khi bạn làm như vậy, bạn có thể lưu trữ con trỏ được trả về cục bộ, đặc biệt nếu bạn sử dụng biến thể deque hoặc biết rằng bạn sẽ không tạo đối tượng mới trong khi bạn có con trỏ. Lặp lại các bộ sưu tập là một hoạt động rất tốn kém và thường xuyên, bạn cần id ổn định, bạn muốn nén bộ nhớ cho các đối tượng dễ bay hơi (như đạn, hạt, v.v.) và cảm ứng rất hiệu quả trên phần cứng modem. Kỹ thuật này được sử dụng trong hơn một vài động cơ thương mại hiệu suất rất cao. :)
Sean Middleditch

1
Theo kinh nghiệm của tôi: (1) Trò chơi điện tử được đánh giá dựa trên hiệu suất trường hợp xấu nhất, không phải hiệu suất trường hợp trung bình. (2) Thông thường bạn có 1 lần lặp trên một bộ sưu tập trên mỗi khung, do đó, việc nén đơn giản là 'làm cho trường hợp xấu nhất của bạn ít xảy ra hơn'. (3) Bạn thường có nhiều allocs / giải phóng trong một khung hình duy nhất, chi phí cao có nghĩa là bạn giới hạn khả năng đó. (4) Bạn có các derefs không giới hạn trên mỗi khung hình (trong các trò chơi tôi đã làm việc, bao gồm cả Diablo 3, thường thì deref là chi phí hoàn hảo cao nhất sau khi tối ưu hóa vừa phải,> 5% tải máy chủ). Tôi không có ý gạt bỏ các giải pháp khác, chỉ nêu ra những kinh nghiệm và lý luận của tôi!
Jeff Gates

3
Tôi thích cấu trúc dữ liệu này. Tôi ngạc nhiên nó không được biết đến nhiều hơn. Nó đơn giản và giải quyết tất cả các vấn đề đã khiến tôi đập đầu mình trong nhiều tháng. Cám ơn vì đã chia sẻ.
Jo Bates

2
Bất kỳ người mới đọc điều này nên rất cảnh giác với lời khuyên này. Đây là một câu trả lời rất sai lệch. "Câu trả lời là luôn luôn sử dụng một mảng hoặc std :: vector. Các loại như danh sách được liên kết hoặc bản đồ std :: thường hoàn toàn khủng khiếp trong các trò chơi và điều đó chắc chắn bao gồm các trường hợp như bộ sưu tập các đối tượng trò chơi." được phóng đại rất nhiều. Không có câu trả lời "LUÔN", nếu không những container khác sẽ không được tạo. Để nói bản đồ / danh sách là "khủng khiếp" cũng là cường điệu. Có rất nhiều trò chơi video sử dụng chúng. "Hiệu quả nhất" không phải là "Thực tế nhất" và có thể bị đọc sai thành "Tốt nhất" chủ quan.
user50286

12

mảng kích thước cố định (bộ nhớ tuyến tính)
với danh sách miễn phí nội bộ (O (1) cấp phát / miễn phí, chỉ báo ổn định)
với các khóa tham chiếu yếu (sử dụng lại khóa
không hợp lệ của khe cắm) không tham gia hội nghị trên không (khi biết là hợp lệ)

struct DataArray<T>
{
  void Init(int count); // allocs items (max 64k), then Clear()
  void Dispose();       // frees items
  void Clear();         // resets data members, (runs destructors* on outstanding items, *optional)

  T &Alloc();           // alloc (memclear* and/or construct*, *optional) an item from freeList or items[maxUsed++], sets id to (nextKey++ << 16) | index
  void Free(T &);       // puts entry on free list (uses id to store next)

  int GetID(T &);       // accessor to the id part if Item

  T &Get(id)            // return item[id & 0xFFFF]; 
  T *TryToGet(id);      // validates id, then returns item, returns null if invalid.  for cases like AI references and others where 'the thing might have been deleted out from under me'

  bool Next(T *&);      // return next item where id & 0xFFFF0000 != 0 (ie items not on free list)

  struct Item {
    T item;
    int id;             // (key << 16 | index) for alloced entries, (0 | nextFreeIndex) for free list entries
  };

  Item *items;
  int maxSize;          // total size
  int maxUsed;          // highest index ever alloced
  int count;            // num alloced items
  int nextKey;          // [1..2^16] (don't let == 0)
  int freeHead;         // index of first free entry
};

Xử lý tất cả mọi thứ từ đạn đến quái vật cho đến kết cấu cho đến các hạt, v.v ... Đây là cấu trúc dữ liệu tốt nhất cho các trò chơi video. Tôi nghĩ rằng nó đến từ Bungie (trở lại trong những ngày chạy marathon / huyền thoại), tôi đã tìm hiểu về nó tại Blizzard, và tôi nghĩ rằng đó là trong một viên ngọc lập trình trò chơi ngày trước. Có lẽ tất cả các ngành công nghiệp trò chơi tại thời điểm này.

Q: "Tại sao không sử dụng một mảng động?" A: Mảng động gây ra sự cố. Ví dụ đơn giản:

foreach(Foo *foo in array)
  if (ShouldSpawnBaby(*foo))
    Foo &baby = array.Alloc();
    foo->numBabies++; // crash!

Bạn có thể tưởng tượng các trường hợp có nhiều phức tạp hơn (như các cửa sổ sâu). Điều này đúng cho tất cả các mảng như container. Khi làm trò chơi, chúng tôi có đủ hiểu biết về vấn đề của mình để buộc kích thước và ngân sách cho mọi thứ để đổi lấy hiệu suất.

Và tôi không thể nói nó đủ: Thực sự, đây là điều tốt nhất từng có. . có một lý do tuyệt vời tại sao bạn không cần một trong số đó;)


Bạn có ý nghĩa gì với mảng động ? Tôi đang hỏi điều này bởi vì DataArraydường như cũng phân bổ một mảng động trong ctor. Vì vậy, nó có thể có một số ý nghĩa khác nhau với sự hiểu biết của tôi.
Eonil

Tôi có nghĩa là một mảng thay đổi kích thước / memmove trong quá trình sử dụng của nó (trái ngược với cấu trúc của nó). Vectơ stl là một ví dụ về cái mà tôi gọi là mảng động.
Jeff Gates

@JeffGates Thực sự thích câu trả lời này. Hoàn toàn đồng ý chấp nhận trường hợp xấu nhất là chi phí thời gian chạy trường hợp tiêu chuẩn. Sao lưu danh sách liên kết miễn phí bằng cách sử dụng mảng hiện có là rất thanh lịch. Câu hỏi Q1: Mục đích của maxUsed? Câu 2: Mục đích của việc lưu trữ chỉ mục trong các bit thứ tự thấp của id cho các mục được phân bổ là gì? Tại sao không 0? Câu 3: Làm thế nào điều này xử lý các thế hệ thực thể? Nếu không, tôi khuyên bạn nên sử dụng các bit thứ tự thấp của Q2 để đếm số lần tạo. - Cảm ơn.
Kỹ sư

1
A1: Max được sử dụng cho phép bạn giới hạn số lần lặp của mình. Ngoài ra, bạn khấu hao bất kỳ chi phí xây dựng. A2: 1) Bạn thường đi từ mục -> id. 2) Nó làm cho sự so sánh rẻ / rõ ràng. A3: Tôi không chắc 'thế hệ' nghĩa là gì. Tôi sẽ giải thích điều này là 'làm thế nào để bạn phân biệt mục thứ 5 được phân bổ trong vị trí 7 với mục thứ 6?' trong đó 5 và 6 là các thế hệ. Đề án đề xuất sử dụng một bộ đếm trên toàn cầu cho tất cả các vị trí. (Chúng tôi thực sự bắt đầu bộ đếm này ở một số khác nhau cho mỗi phiên bản DataArray để dễ dàng phân biệt id hơn.) Tôi chắc chắn rằng bạn có thể điều chỉnh lại các bit trên mỗi mục theo dõi là rất quan trọng.
Jeff Gates

1
@JeffGates - Tôi biết đây là một chủ đề cũ nhưng tôi thực sự thích ý tưởng này, bạn có thể cho tôi một số thông tin về hoạt động bên trong của void Free (T &) trên void Free (id) không?
TheStatehz

1

Không có câu trả lời đúng cho điều này. Tất cả phụ thuộc vào việc thực hiện các thuật toán của bạn. Chỉ cần đi với một trong những bạn nghĩ là tốt nhất. Đừng cố gắng tối ưu hóa ở giai đoạn đầu này.

Nếu bạn thường xuyên xóa các đối tượng và tạo lại chúng, tôi khuyên bạn nên xem cách các nhóm đối tượng được triển khai.

Chỉnh sửa: Tại sao làm phức tạp mọi thứ với các vị trí và những gì không. Tại sao không sử dụng một ngăn xếp và bật mục cuối cùng ra và sử dụng lại? Vì vậy, khi bạn thêm một, bạn sẽ làm ++, khi bạn bật một cái bạn làm - để theo dõi chỉ số kết thúc của bạn.


Một ngăn xếp đơn giản không xử lý trường hợp các mục bị xóa theo thứ tự tùy ý.
Jeff Gates

Công bằng mà nói, mục tiêu của anh không chính xác rõ ràng. Ít nhất là không phải với tôi.
Sidar

1

Nó phụ thuộc vào trò chơi của bạn. Các thùng chứa khác nhau về tốc độ truy cập vào một yếu tố cụ thể, tốc độ loại bỏ một yếu tố và tốc độ của một yếu tố được thêm vào.


  • std :: vector - Truy cập nhanh và loại bỏ và thêm vào cuối là nhanh chóng. Loại bỏ từ đầu và giữa là chậm.
  • std :: list - Lặp lại danh sách không chậm hơn vectơ nhưng truy cập vào một điểm cụ thể của danh sách thì chậm (vì về cơ bản, việc lặp lại là điều duy nhất bạn có thể làm với danh sách). Thêm và xóa các mục ở bất cứ đâu là nhanh chóng. Hầu hết bộ nhớ trên đầu. Không liên tục.
  • std :: deque - Truy cập nhanh và loại bỏ / thêm vào cuối và bắt đầu nhanh nhưng chậm ở giữa.

Thông thường bạn muốn sử dụng một danh sách nếu bạn muốn danh sách đối tượng của bạn được sắp xếp theo một cách khác với thời gian và do đó phải chèn các đối tượng mới thay vì nối thêm, một cách khác. Một deque đã tăng tính linh hoạt trên một vectơ nhưng không thực sự có nhiều nhược điểm.

Nếu bạn thực sự có rất nhiều thực thể, bạn nên xem Phân vùng không gian.


Không đúng sự thật: danh sách. Lời khuyên Deque phụ thuộc hoàn toàn vào việc triển khai deque, khác nhau rất nhiều về tốc độ và thực hiện.
biến thái
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.