Dinamik oyun nesnelerini depolamak için en verimli kap nedir? [kapalı]


20

Ben birinci şahıs nişancı yapıyorum ve birçok farklı konteyner türü hakkında biliyorum ama oyun sık sık eklenecek ve silinecek dinamik nesneleri depolamak için en verimli konteyner bulmak istiyorum. EX-Kurşunlar.

Ben bu durumda bir liste olacağını düşünüyorum, böylece bellek bitişik değildir ve devam eden herhangi bir yeniden boyutlandırma yoktur. Ama sonra bir harita ya da set kullanmayı da düşünüyorum. Herhangi bir yararlı bilgi varsa ben bunu takdir ediyorum.

Bu arada c ++ ile yazıyorum.

Ayrıca işe yarayacağını düşündüğüm bir çözüm buldum.

Başlamak için ben büyük bir boyutta bir vektör tahsis edeceğim .. 1000 nesneler diyelim. Bu vektörde son eklenen indeksi takip edeceğim, böylece nesnelerin sonunun nerede olduğunu biliyorum. Sonra da vektörden "silinmiş" tüm nesnelerin indekslerini tutacak bir kuyruk oluşturacağım. (Hiçbir gerçek silme yapılmayacak, ben sadece o slot ücretsiz olduğunu biliyorum). Yani kuyruk boşsa, vektör + 1'deki son eklenen dizine ekleyeceğim, aksi takdirde kuyruğun önündeki vektörün dizinine ekleyeceğim.


Hedeflediğiniz herhangi bir dil var mı?
Phill.Zitt

Bu soru, donanım platformu, dil / çerçeveler
vb.Dahil

1
Pro ipucu, ücretsiz listeyi silinen öğelerin belleğinde saklayabilirsiniz (böylece ekstra kuyruğa ihtiyacınız yoktur).
Jeff Gates

2
Bu soruda bir soru var mı?
Trevor Powell

En büyük dizini takip etmeniz veya bir dizi öğeyi önceden konumlandırmanız gerekmediğini unutmayın. std :: vector sizin için her şeyi halleder.
API-Beast

Yanıtlar:


33

Cevap her zaman bir dizi veya std :: vector kullanmaktır. Bağlantılı bir liste veya std :: map gibi türler genellikle oyunlarda kesinlikle korkunçtur ve bu kesinlikle oyun nesneleri koleksiyonu gibi vakaları içerir.

Nesneleri dizide / vektörde (işaretçiler değil) kendileri saklamalısınız.

Sen istediğiniz bitişik bellek. Gerçekten istiyorsun. Bitişik olmayan bellekte herhangi bir veri üzerinde yineleme yapmak, genel olarak çok sayıda önbellek özlüsü getirir ve derleyici ve CPU'nun etkili önbellek önceden getirme yeteneğini ortadan kaldırır. Bu tek başına performansı öldürebilir.

Ayrıca bellek ayırmalarından ve yeniden konumlandırmalardan kaçınmak istersiniz. Hızlı bir bellek ayırıcıyla bile çok yavaştırlar. Oyunların her karede birkaç yüz bellek ayırmasını kaldırarak 10x FPS yumru aldığını gördüm. Bu kadar kötü olmalı gibi görünmüyor, ama olabilir.

Son olarak, oyun nesnelerini yönetmek için önem verdiğiniz çoğu veri yapısı, bir dizi veya vektör üzerinde bir ağaç veya liste ile olduğundan çok daha verimli bir şekilde uygulanabilir.

Örneğin, oyun nesnelerini kaldırmak için takas ve pop kullanabilirsiniz. Gibi bir şey ile kolayca uygulanır:

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

Ayrıca, nesneleri silindi olarak işaretleyebilir ve bir dahaki sefere yeni bir nesne oluşturmanız gerektiğinde dizinlerini ücretsiz bir listeye koyabilirsiniz, ancak takas ve pop'u yapmak daha iyidir. Döngünün kendisinden dallanma olmadan tüm canlı nesneler üzerinde basit bir döngü yapmanızı sağlar. Mermi fiziği entegrasyonu ve benzerleri için bu önemli bir performans artışı olabilir.

Daha da önemlisi, sen-ebilmek bulmak nesneleri ile basit bir tablo aramalar istikrarlı bir benzersiz benzersiz yuvası harita yapısı kullanmaktır.

Oyun nesnelerinizin ana dizilerinde bir dizin vardır. Sadece bu indeksle çok verimli bir şekilde aranabilirler (bir haritadan veya bir karma tablodan çok daha hızlı). Ancak, nesne kaldırılırken takas ve pop nedeniyle dizin sabit değildir.

Bir yuva haritası iki dolaylı katman gerektirir, ancak her ikisi de sabit indekslere sahip basit dizi aramalarıdır. Onlar hızlı . Çok hızlı.

Temel fikir, üç dizinizin olması: ana nesne listeniz, dolaylı listeniz ve dolaylı liste için ücretsiz bir liste. Ana nesne listeniz, her nesnenin kendi benzersiz kimliğini bildiği gerçek nesnelerinizi içerir. Benzersiz kimlik bir dizin ve sürüm etiketinden oluşur. Dolaylı liste basitçe ana nesne listesine bir indeks dizisidir. Ücretsiz liste, dolaylı listeye bir indeks yığınıdır.

Ana listede bir nesne oluşturduğunuzda, dolaylı listede kullanılmayan bir giriş bulursunuz (ücretsiz listeyi kullanarak). Dolaylı listedeki giriş, ana listede kullanılmayan bir girişi gösterir. Nesnenizi bu konumda başlatırsınız ve benzersiz kimliğini seçtiğiniz dolaylı liste girişinin dizinine ve ana liste öğesindeki mevcut sürüm etiketine ve bir tanesine ayarlarsınız.

Bir nesneyi yok ettiğinizde, takas ve pop'u normal şekilde yaparsınız, ancak sürüm numarasını da artırırsınız. Daha sonra ücretsiz listeye dolaylı liste dizinini (nesnenin benzersiz kimliğinin bir parçası) da eklersiniz. Bir nesneyi takas ve pop'un bir parçası olarak taşırken, aynı zamanda dolaylı listedeki girişini yeni konumuna da güncellersiniz.

Örnek sözde kod:

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)

Dolaylı katman, sıkıştırma sırasında hareket edebilen bir kaynak (ana nesne listesi) için kararlı bir tanımlayıcıya (girdilerin hareket etmediği dolaylı katmana indeks) sahip olmanızı sağlar.

Sürüm etiketi, silinebilecek bir nesneye bir kimlik depolamanıza olanak tanır. Örneğin, (10,1) kimliğine sahipsiniz. Dizin 10'a sahip nesne silinir (örneğin, merminiz bir nesneye çarpar ve yok edilir). Bu durumda, ana nesne listesindeki belleğin konumundaki nesne, sürüm numarasına çarparak verir (10,2). Eski bir kimlikten tekrar (10,1) aramaya çalışırsanız, arama bu nesneyi dizin 10 aracılığıyla döndürür, ancak sürüm numarasının değiştiğini görebilir, böylece kimlik artık geçerli olmaz.

Bu, nesnelerin bellekte hareket etmesini sağlayan sabit bir kimlikle sahip olabileceğiniz en hızlı veri yapısıdır ve bu da veri konumu ve önbellek tutarlılığı için önemlidir. Bu, mümkün olan bir karma tablonun uygulanmasından daha hızlıdır; bir hash tablosunun en azından bir karma değerini hesaplaması gerekir (tablo aramasından daha fazla talimat) ve sonra karma zincirini (std :: unordered_map'nin korkunç durumunda bağlı bir liste veya açık adresli bir liste) izlemesi gerekir. bir karma tablonun aptal olmayan bir şekilde uygulanması) ve ardından her anahtarda bir değer karşılaştırması yapması gerekir (sürüm etiketi kontrolünden daha pahalı değil, ancak daha az pahalı olabilir). Çok iyi bir karma tablosu (STL'nin herhangi bir uygulamasındaki tablo değil, STL bir oyun nesnesi listesi için oyundan farklı kullanım durumlarını optimize eden bir karma tabloyu zorunlu kıldığı için) bir dolaylı kayıttan tasarruf edebilir,

Temel algoritmada yapabileceğiniz çeşitli iyileştirmeler vardır. Örneğin ana nesne listesi için std :: deque gibi bir şey kullanmak; fazladan bir dolaylı katman içerir, ancak nesnelerin alan haritasından aldığınız geçici işaretçileri geçersiz kılmadan tam listeye eklenmesine olanak tanır.

Ayrıca, dizin nesnenin bellek adresinden (this - nesneler) hesaplanabileceğinden ve daha iyisi yalnızca nesneyi kaldırırken daha iyi bir şeydir (bu durumda zaten nesnenin kimliğine sahip olmanız gerekir (ve dolayısıyla endeksi) parametresini kullanın.

Yazma özürleri; Bunun olabileceği en açık açıklama olduğunu düşünmüyorum. Geç oldu ve kod örnekleri üzerinde olduğundan daha fazla zaman harcamadan açıklamak zor.


1
'Kompakt' depolama için her erişimi ekstra bir deref ve yüksek bir tahsis / ücretsiz maliyet (takas) üzerinden alıyorsunuz. Video oyunları ile ilgili tecrübelerime göre, bu kötü bir ticaret :) YMMV elbette.
Jeff Gates,

1
Aslında gerçek dünya senaryolarında sık sık yapılan dereference'i yapmazsınız. Bunu yaptığınızda, özellikle deque değişkenini kullanıyorsanız veya işaretçiniz varken yeni nesneler oluşturmayacağınızı biliyorsanız, döndürülen işaretçiyi yerel olarak depolayabilirsiniz. Koleksiyonlar üzerinde yineleme çok pahalı ve sık yapılan bir işlemdir, kararlı kimliğe ihtiyacınız vardır, uçucu nesneler (madde işaretleri, parçacıklar vb.) İçin bellek sıkıştırması istiyorsunuz ve dolaylı olarak modem donanımında çok etkilidir. Bu teknik, çok yüksek performanslı birkaç ticari motorda kullanılmaktadır. :)
Sean Middleditch

1
Deneyimlerime göre: (1) Video oyunları, ortalama vaka performansına göre değil, en kötü durum performansına göre değerlendirilir. (2) Normalde kare başına bir koleksiyon üzerinde 1 yinelemeye sahip olursunuz, böylece sıkıştırma 'en kötü durumunuzu daha az sıklıkta yapar'. (3) Genellikle tek bir çerçevede çok sayıda alloc / freesiniz vardır, yüksek maliyet bu yeteneği sınırladığınız anlamına gelir. (4) Çerçeve başına sınırsız derefiniz var (Diablo 3 dahil olmak üzere üzerinde çalıştığım oyunlarda, genellikle deref, ılımlı optimizasyondan sonra, sunucu yükünün>% 5'inden sonra en yüksek maliyettir). Sadece deneyimlerime ve akıl yürütmeye dikkat çekerek diğer çözümleri reddetmek istemiyorum!
Jeff Gates

3
Bu veri yapısını seviyorum. Daha iyi bilinmemesine şaşırdım. Çok basit ve aylardır kafamı patlatmamı sağlayan tüm sorunları çözüyor. Paylaşım için teşekkürler.
Jo Bates

2
Bunu okuyan herhangi bir acemi, bu tavsiyeye çok dikkat etmelidir. Bu çok yanıltıcı bir cevap. "Yanıt her zaman bir dizi veya std :: vector kullanmaktır. Bağlantılı liste veya std :: map gibi türler oyunlarda genellikle kesinlikle korkunçtur ve bu kesinlikle oyun nesnelerinin koleksiyonları gibi vakaları içerir." çok abartılı. "HER ZAMAN" yanıtı yoktur, aksi takdirde bu diğer kaplar oluşturulmazdı. Haritalar / listeler "korkunç" demek de abartılı olduğunu. Bunları kullanan bir sürü video oyunu var. "En Verimli", "En Pratik" değildir ve öznel "En İyi" olarak yanlış okunabilir.
user50286

12


dahili serbest liste ile sabit boyutlu dizi (doğrusal bellek) (O (1) tahsis / boş, kararlı göstergeler)
zayıf referans anahtarları (yuvanın yeniden kullanılması geçersiz kılar)
sıfır genel giderler (bilinen-geçerli olduğunda)

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
};

Mermilerden canavarlara, dokulara, parçacıklara vb. Kadar her şeyi idare eder. Bu, video oyunları için en iyi veri yapısıdır. Sanırım Bungie'den geldi (maraton / efsane günlerde), Blizzard'da öğrendim ve sanırım o gün içinde bir programlama programlama oyunuydu. Muhtemelen bu noktada oyun endüstrisinin her yerinde.

S: "Neden dinamik bir dizi kullanmıyorsunuz?" C: Dinamik diziler çökmelere neden olur. Basit bir örnek:

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

Daha fazla komplikasyona sahip vakaları hayal edebilirsiniz (derin çağrılar gibi). Bu, kapsayıcı benzeri tüm diziler için geçerlidir. Oyun yaparken, performans karşılığında her şeye boyut ve bütçeyi zorlamak için sorunumuzu yeterince anlıyoruz.

Ve yeterince söyleyemem: Gerçekten, bu şimdiye kadarki en iyi şey. (Kabul etmiyorsanız, daha iyi çözümünüzü gönderin! Dikkat - bu yazının en üstünde listelenen sorunları ele almalısınız: doğrusal bellek / yineleme, O (1) ayırma / serbest, kararlı endeksler, zayıf referanslar, sıfır havai kayıtlar veya bunlardan birine ihtiyacınız olmadığında inanılmaz bir nedeniniz var;)


Dinamik dizi ile ne demek istiyorsun ? Ben de soruyorum çünkü DataArrayaynı zamanda dinamik olarak ctor içinde bir dizi tahsis görünüyor. Bu yüzden benim anlayışımla farklı bir anlamı olabilir.
Eonil

Kullanım sırasında (yapının aksine) yeniden boyutlandırma / hatırlatma dizisi demek. Bir stl vektörü, dinamik dizi dediğim şeyin bir örneğidir.
Jeff Gates

@JeffGates Bu yanıtı gerçekten beğendim. En kötü durumu standart vaka çalışma zamanı maliyeti olarak kabul etmeyi tamamen kabul edin. Mevcut diziyi kullanarak ücretsiz bağlantılı listeyi yedeklemek çok zariftir. Sorular Q1: maxUsed amacı? S2: Tahsis edilen girişler için indeksi düşük dereceli id ​​bitlerinde depolamanın amacı nedir? Neden 0 değil? S3: Bu varlık nesillerini nasıl ele alıyor? Değilse, kısa nesil sayısı için Q2'nin düşük dereceli bitlerini kullanmanızı öneririm. - Teşekkürler.
Mühendis

1
A1: Kullanılan Max, yinelemenizi sınırlamanızı sağlar. Ayrıca herhangi bir inşaat maliyeti amortismana tabi tutulur. A2: 1) Genellikle item -> id'den gidersiniz. 2) Karşılaştırmayı ucuz / açık hale getirir. A3: 'Nesiller'in ne anlama geldiğinden emin değilim. Bunu 'alan 7'de tahsis edilen 5. öğeyi 6. öğeden nasıl ayırt edersiniz?' Olarak yorumlayacağım. burada 5 ve 6 nesillerdir. Önerilen şema tüm slotlar için global olarak bir sayaç kullanmaktadır. (Aslında bu sayacı, kimlikleri daha kolay ayırt etmek için her DataArray örneği için farklı bir numaradan başlatırız.) Eminim, öğe izleme başına bitleri yeniden belirleyebilmeniz önemliydi.
Jeff Gates

1
@JeffGates - Bunun eski bir konu olduğunu biliyorum ama bu fikri gerçekten çok beğendim, void Free (T) 'nin void Free (id) üzerindeki iç çalışmaları hakkında bilgi verebilir misiniz?
TheStatehz

1

Buna doğru bir cevap yok. Her şey algoritmalarınızın uygulanmasına bağlıdır. Sadece en iyi olduğunu düşündüğün biriyle git. Bu erken aşamada optimizasyon yapmaya çalışmayın.

Nesneleri sık sık siliyor ve yeniden oluşturuyorsanız, nesne havuzlarının nasıl uygulandığına bakmanızı öneririm.

Düzenleme: Neden şeyler yuvaları ile karmaşık ve ne değil. Neden sadece bir yığın kullanmıyor ve son öğeyi çıkartıp tekrar kullanmıyorsunuz? Yani bir tane eklediğinizde, son dizininizi takip etmek için yaptığınız ++ 'ı yaparsınız.


Basit bir yığın, öğelerin rastgele sırada silindiği durumu işlemez.
Jeff Gates,

Adil olmak gerekirse, hedefi tam olarak belli değildi. En azından bana değil.
Sidar

1

Bu oyununuza bağlıdır. Kaplar, belirli bir öğeye erişimin ne kadar hızlı olduğu, bir öğenin ne kadar hızlı kaldırıldığı ve bir öğenin ne kadar hızlı eklendiği bakımından farklıdır.


  • std :: vector - Hızlı erişim ve kaldırma ve sonuna ekleme hızlıdır. Baştan ve ortadan kaldırmak yavaştır.
  • std :: list - Liste üzerinde yineleme yapmak bir vektörden çok daha yavaş değildir, ancak listenin belirli bir noktasına erişmek yavaştır (çünkü yineleme temelde bir listeyle yapabileceğiniz tek şeydir). Her yere öğe eklemek ve çıkarmak hızlıdır. Çoğu bellek yükü. Sigara sürekli.
  • std :: deque - Hızlı erişim ve kaldırma ve son ve başlangıç ​​ekleme hızlı ama ortada yavaş.

Genellikle nesne listenizi kronik olarak farklı bir şekilde sıralamak istiyorsanız bir liste kullanmak istersiniz ve bu nedenle eklemek yerine yeni nesneler eklemek zorundasınız, aksi takdirde bir deque. Bir deque, bir vektör üzerinde esnekliği arttırdı, ancak gerçekten dezavantajı yok.

Gerçekten çok fazla varlığınız varsa Space Partitioning'e bir göz atmalısınız.


Doğru değil re: list. Deque tavsiyesi tamamen hız ve uygulama açısından çılgınca değişen deque uygulamalarına bağlıdır.
metamorfoz
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.