Ist das Speichern aller Spielobjekte in einer einzigen Liste ein akzeptables Design?


24

Für jedes Spiel, das ich gemacht habe, lege ich einfach alle meine Spielobjekte (Kugeln, Autos, Spieler) in einer einzigen Array-Liste ab, die ich durchlaufe, um zu zeichnen und zu aktualisieren. Der Aktualisierungscode für jede Entität wird in ihrer Klasse gespeichert.

Ich habe mich gefragt, ob dies der richtige oder der effizientere Weg ist.


4
Ich stelle fest, dass Sie "Array-Liste" gesagt haben, vorausgesetzt, dies ist .Net, sollten Sie stattdessen auf eine Liste <T> aktualisieren. Es ist fast identisch, mit der Ausnahme, dass eine Liste <T> typstark ist und Sie daher nicht jedes Mal Cast eingeben müssen, wenn Sie auf ein Element zugreifen. Hier ist das Dokument: msdn.microsoft.com/en-us/library/6sh2ey19.aspx
John McDonald

Antworten:


26

Es gibt keinen richtigen Weg. Was Sie beschreiben, funktioniert und ist vermutlich schnell genug, sodass es keine Rolle spielt, da Sie anscheinend gefragt werden, weil Sie sich Gedanken über das Design machen und nicht, weil es zu Leistungseinbußen führt. Also ist es sicher ein richtiger Weg.

Sie können dies sicherlich auch anders machen, indem Sie beispielsweise für jeden Objekttyp eine homogene Liste erstellen oder sicherstellen, dass alle Elemente in Ihrer heterogenen Liste so gruppiert sind, dass Sie die Kohärenz für den im Cache befindlichen Code verbessern oder parallele Aktualisierungen zulassen. Ob Sie auf diese Weise eine nennenswerte Leistungsverbesserung erzielen, ist schwer zu sagen, ohne den Umfang und die Größe Ihrer Spiele zu kennen.

Sie können auch die Verantwortlichkeiten Ihrer Entitäten weiter herausarbeiten - es klingt so, als würden sich Entitäten im Moment selbst aktualisieren und rendern -, um das Prinzip der einheitlichen Verantwortung besser zu befolgen. Dies kann die Wartbarkeit und Flexibilität Ihrer einzelnen Schnittstellen erhöhen, indem diese voneinander entkoppelt werden. Aber auch hier ist es für mich schwierig zu sagen, ob Sie von der zusätzlichen Arbeit profitieren oder nicht, wenn ich weiß, was ich über Ihre Spiele weiß (was im Grunde genommen nichts ist).

Ich würde empfehlen, nicht zu viel darüber zu streichen und im Hinterkopf zu behalten, dass dies ein potenzieller Bereich für Verbesserungen sein könnte, wenn Sie jemals Leistungs- oder Wartbarkeitsprobleme bemerken.


16

Wie andere gesagt haben, wenn es schnell genug ist, dann ist es schnell genug. Wenn Sie sich fragen, ob es einen besseren Weg gibt, müssen Sie sich folgende Frage stellen:

Wiederhole ich alle Objekte, aber arbeite ich nur mit einer Teilmenge davon?

Wenn Sie dies tun, ist dies ein Hinweis darauf, dass Sie möglicherweise mehrere Listen möchten, die nur relevante Referenzen enthalten. Wenn Sie beispielsweise jedes Spielobjekt durchlaufen, um es zu rendern, aber nur eine Teilmenge von Objekten gerendert werden muss, empfiehlt es sich möglicherweise, eine separate renderbare Liste nur für die Objekte zu führen, die gerendert werden müssen.

Dies würde die Anzahl der Objekte verringern, die Sie durchlaufen, und die unnötigen Überprüfungen überspringen, da Sie bereits wissen, dass alle Objekte in der Liste verarbeitet werden sollen.

Sie müssen es profilieren, um zu sehen, ob Sie einen erheblichen Leistungszuwachs erzielen.


Ich bin damit einverstanden, ich habe im Allgemeinen Teilmengen meiner Liste für Objekte, die in jedem Frame aktualisiert werden müssen, und ich habe darüber nachgedacht, dasselbe für Objekte zu tun, die gerendert werden. Wenn sich diese Eigenschaften jedoch im Handumdrehen ändern können, kann die Verwaltung aller Listen komplex sein. Wenn und Objekt nicht mehr rendernd oder aktualisierbar, sondern nicht mehr aktualisierbar sind, können Sie sie jetzt gleichzeitig mehreren Listen hinzufügen oder daraus entfernen.
Nic Foster

8

Es hängt fast ausschließlich von Ihren spezifischen Spielanforderungen ab. Es kann perfekt für ein einfaches Spiel funktionieren, aber auf einem komplexeren System versagen. Wenn es für Ihr Spiel funktioniert, sorgen Sie sich nicht darum und versuchen Sie nicht, es zu überarbeiten, bis die Notwendigkeit entsteht.

Was den Grund für das Scheitern des einfachen Ansatzes in manchen Situationen angeht, lässt sich dies nur schwer in einem einzigen Beitrag zusammenfassen, da die Möglichkeiten endlos sind und ausschließlich von den Spielen selbst abhängen. In anderen Antworten wurde beispielsweise erwähnt, dass Sie Objekte, die Verantwortlichkeiten teilen, in einer separaten Liste zusammenfassen möchten.

Dies ist eine der häufigsten Änderungen, die Sie je nach Spieldesign vornehmen können. Ich werde die Gelegenheit nutzen, einige andere (komplexere) Beispiele zu beschreiben, aber denken Sie daran, dass es noch viele andere mögliche Gründe und Lösungen gibt.

Für den Anfang weise ich darauf hin, dass das Aktualisieren und Rendern Ihrer Spielobjekte in einigen Spielen unterschiedliche Anforderungen haben kann und daher separat gehandhabt werden muss. Einige mögliche Probleme beim Aktualisieren und Rendern von Spielobjekten:

Aktualisieren von Spielobjekten

Hier ist ein Auszug aus der Game Engine Architecture, den ich empfehlen würde:

Bei Vorhandensein von Abhängigkeiten zwischen Objekten muss die oben beschriebene Technik der schrittweisen Aktualisierung ein wenig angepasst werden. Dies liegt daran, dass Abhängigkeiten zwischen Objekten zu Regelkonflikten bei der Reihenfolge der Aktualisierung führen können.

Mit anderen Worten, in einigen Spielen ist es möglich, dass Objekte voneinander abhängig sind und eine bestimmte Aktualisierungsreihenfolge erfordern. In diesem Szenario müssen Sie möglicherweise eine komplexere Struktur als eine Liste erstellen, um Objekte und ihre gegenseitigen Abhängigkeiten zu speichern.

Bildbeschreibung hier eingeben

Spielobjekte rendern

In einigen Spielen bilden Szenen und Objekte eine Hierarchie von Eltern-Kind-Knoten und sollten in der richtigen Reihenfolge und mit Transformationen relativ zu ihren Eltern gerendert werden. In diesen Fällen benötigen Sie möglicherweise eine komplexere Struktur wie ein Szenendiagramm (eine Baumstruktur) anstelle einer einzelnen Liste:

Bildbeschreibung hier eingeben

Andere Gründe können beispielsweise darin bestehen, dass Sie Ihre Objekte mithilfe einer räumlichen Partitionierungsdatenstruktur so organisieren, dass die Effizienz beim Durchführen von View-Frustum-Culling verbessert wird.


4

Die Antwort lautet "Ja, das ist in Ordnung." Als ich an großen Eisensimulationen arbeitete, bei denen die Leistung nicht verhandelbar war, war ich überrascht, alles in einem großen, fetten (BIG und FAT) globalen Array zu sehen. Später arbeitete ich an einem OO-System und musste die beiden vergleichen. Obwohl es super hässlich war, war die "Ein Array, um alle zu regieren" -Version in einfachem Single-Thread-C, das beim Debug kompiliert wurde, um ein Vielfaches schneller als das "hübsche" Multithread-OO-System, das in C ++ -O2 kompiliert wurde . Ich denke, ein bisschen davon hat mit der exzellenten Cache-Lokalität (sowohl räumlich als auch zeitlich) in der Big-Fat-Array-Version zu tun.

Wenn Sie Leistung wünschen, wird ein großes, fettes globales C-Array die Obergrenze sein (aus Erfahrung).


2

Grundsätzlich möchten Sie Listen so erstellen, wie Sie sie benötigen. Wenn Sie Geländeobjekte auf einmal neu zeichnen, kann eine Liste mit nur diesen nützlich sein. Aber damit sich das lohnt, braucht man Zehntausende von ihnen und viele Zehntausende von Dingen, die kein Gelände sind. Andernfalls ist es schnell genug, die gesamte Liste zu durchsuchen und ein "Wenn" zum Herausziehen von Geländeobjekten zu verwenden.

Ich habe immer eine Hauptliste benötigt, unabhängig davon, wie viele andere Listen ich hatte. Normalerweise mache ich daraus eine Karte, eine Schlüsseltabelle oder ein Wörterbuch, damit ich willkürlich auf einzelne Elemente zugreifen kann. Aber eine einfache Liste ist viel einfacher. Wenn Sie nicht zufällig auf Spielobjekte zugreifen, benötigen Sie die Karte nicht. Und das gelegentliche Durchsuchen von ein paar Tausend Einträgen nach dem richtigen dauert nicht lange. Also würde ich sagen, was auch immer Sie tun, planen Sie, bei der Master-Liste zu bleiben. Erwägen Sie, eine Karte zu verwenden, wenn sie groß wird. (Wenn Sie jedoch Indizes anstelle von Schlüsseln verwenden können, können Sie sich an Arrays halten und haben etwas Besseres als eine Map. Ich hatte immer große Lücken zwischen den Indexnummern, also musste ich darauf verzichten.)

Ein Wort zu Arrays: Sie sind unglaublich schnell. Aber wenn sie ungefähr 100.000 Elemente haben, geben sie Garbage Collectors Fits. Wenn Sie den Speicherplatz einmal zuweisen und ihn anschließend nicht mehr berühren können, ist dies in Ordnung. Wenn Sie das Array jedoch kontinuierlich erweitern, werden Sie kontinuierlich große Speicherblöcke zuweisen und freigeben, und es besteht die Gefahr, dass Ihr Spiel sekundenlang hängen bleibt und sogar ein Speicherfehler auftritt.

So schnell und effizienter? Sicher. Eine Karte für den wahlfreien Zugriff. Bei sehr großen Listen schlägt eine verknüpfte Liste ein Array, wenn Sie keinen Direktzugriff benötigen. Mehrere Karten / Listen, um die Objekte nach Bedarf auf verschiedene Arten zu organisieren. Sie könnten die Aktualisierung mit mehreren Threads durchführen.

Aber es ist alles viel Arbeit. Dieses einzelne Array ist schnell und einfach. Sie können andere Listen und Dinge nur bei Bedarf hinzufügen, und Sie werden sie wahrscheinlich nicht benötigen. Sei dir nur bewusst, was schief gehen kann, wenn dein Spiel groß wird. Möglicherweise möchten Sie eine Testversion mit 10-mal so vielen Objekten wie in der Realversion haben, um eine Vorstellung davon zu bekommen, wann Sie auf Probleme stoßen. (Tun Sie dies nur , wenn Ihr Spiel ist groß zu bekommen.) (Und versuchen Sie nicht das Multi-Threading , ohne es zu vielen Gedanken. Alles anderes leicht , mit zu beginnen, und Sie können , wie Sie so viel oder so wenig tun , wie Mit Multithreading zahlen Sie einen hohen Preis für den Zugang, die Gebühren für den Aufenthalt sind hoch und es ist schwierig, wieder herauszukommen.)

Mit anderen Worten, mach einfach weiter, was du tust. Ihre größte Grenze ist wahrscheinlich Ihre eigene Zeit, und Sie machen den bestmöglichen Nutzen daraus.


Ich glaube wirklich nicht, dass eine verknüpfte Liste ein Array in irgendeiner Größe übertrifft, es sei denn, Sie fügen häufig Daten zur Mitte hinzu oder entfernen sie.
Zan Lynx

@ZanLynx: Und auch dann. Ich denke, die neuen Laufzeitoptimierungen helfen Arrays sehr - nicht, dass sie es brauchten. Wenn Sie jedoch wiederholt und schnell große Speicherbereiche zuweisen und freigeben, wie dies bei großen Arrays der Fall sein kann, können Sie den Garbage Collector in Knoten zusammenfassen. (Auch hier spielt die Gesamtgröße des Speichers eine Rolle. Das Problem hängt von der Größe der Blöcke, der Häufigkeit der Freigabe und Zuweisung sowie dem verfügbaren Speicher ab.) Ein plötzlicher Fehler tritt auf, wenn das Programm hängt oder abstürzt. Normalerweise gibt es viel freien Speicherplatz. Der GC kann es einfach nicht verwenden. Verknüpfte Listen vermeiden dieses Problem.
RalphChapin

Andererseits ist die Wahrscheinlichkeit, dass verknüpfte Listen bei Iteration einen Cache-Fehler enthalten, erheblich höher, da die Knoten im Speicher nicht zusammenhängend sind. Es ist nicht annähernd so trocken. Sie können die Neuzuweisungsprobleme verringern, indem Sie dies nicht tun (wenn möglich im Voraus, da dies häufig der Fall ist), und Sie können die Cache-Kohärenzprobleme in einer verknüpften Liste durch Poolzuweisung der Knoten verringern (obwohl Sie immer noch die Folgekosten der Referenz haben) , es ist relativ trivial).
Josh

Ja, aber speichern Sie nicht auch in einem Array nur Zeiger auf Objekte? (Es sei denn, es handelt sich um ein kurzlebiges Array für ein Partikelsystem oder ähnliches.) In diesem Fall wird es immer noch viele Cache-Fehler geben.
Todd Lehman

1

Ich habe eine ähnliche Methode für einfache Spiele verwendet. Das Speichern von allem in einem Array ist sehr nützlich, wenn die folgenden Bedingungen erfüllt sind:

  1. Sie haben eine kleine Anzahl oder ein bekanntes Maximum an Spielobjekten
  2. Sie bevorzugen Einfachheit gegenüber Leistung (dh optimierte Datenstrukturen wie Bäume sind die Mühe nicht wert)

Auf jeden Fall habe ich diese Lösung als Array mit fester Größe implementiert, das eine gameObject_ptrStruktur enthält . Diese Struktur enthielt einen Zeiger auf das Spielobjekt selbst und einen Booleschen Wert, der angibt, ob das Objekt "lebendig" war oder nicht.

Der Zweck des Booleschen Werts besteht darin, die Erstellungs- und Löschvorgänge zu beschleunigen. Anstatt Spielobjekte zu zerstören, wenn sie aus der Simulation entfernt werden, würde ich einfach das "lebendig" -Flag auf setzen false.

Bei der Ausführung von Berechnungen für Objekte durchläuft der Code das Array und führt Berechnungen für Objekte durch, die als "lebendig" gekennzeichnet sind. Es werden nur Objekte gerendert, die als "lebendig" gekennzeichnet sind.

Eine weitere einfache Optimierung besteht darin, das Array nach Objekttyp zu organisieren. Beispielsweise könnten alle Aufzählungszeichen in den letzten n Zellen des Arrays leben. Durch Speichern eines int mit dem Wert des Index oder eines Zeigers auf das Array-Element können bestimmte Typen zu reduzierten Kosten referenziert werden.

Es versteht sich von selbst, dass diese Lösung nicht für Spiele mit großen Datenmengen geeignet ist, da das Array unannehmbar groß sein wird. Es ist auch nicht für Spiele mit Speicherbeschränkungen geeignet, die verhindern, dass nicht verwendete Objekte Speicher belegen. Die Quintessenz ist, dass, wenn Sie eine bekannte Obergrenze für die Anzahl der Spielobjekte haben, die Sie zu einem bestimmten Zeitpunkt haben werden, nichts falsch daran ist, sie in einem statischen Array zu speichern.

Dies ist mein erster Beitrag! Ich hoffe es beantwortet deine Frage!


erster Beitrag! Yay!
Tili

0

Es ist akzeptabel, wenn auch ungewöhnlich. Begriffe wie "schneller" und "effizienter" sind relativ; In der Regel werden Spielobjekte in separate Kategorien unterteilt (z. B. eine Liste für Kugeln, eine Liste für Feinde usw.), um die Programmierung besser zu organisieren (und damit effizienter zu gestalten). Es ist jedoch nicht so, als würde der Computer alle Objekte schneller durchlaufen .


2
In den meisten XNA-Spielen stimmt das, aber das Organisieren Ihrer Objekte kann das Durchlaufen tatsächlich beschleunigen: Objekte desselben Typs treffen mit größerer Wahrscheinlichkeit den Befehls- und Datencache Ihrer CPU. Cache-Fehler sind besonders auf Konsolen teuer. Auf der Xbox 360 beispielsweise kostet ein L2-Cache-Fehler ca. 600 Zyklen. Das lässt eine Menge Leistung auf dem Tisch. Dies ist eine Stelle, an der verwalteter Code gegenüber systemeigenem Code einen Vorteil hat: Objekte, die zeitlich eng beieinander liegen, werden ebenfalls im Arbeitsspeicher gespeichert, und die Situation verbessert sich mit der Garbage Collection (Komprimierung).
Chronic Game Programmer

0

Sie sagen, einzelnes Array und Objekte.

Also (ohne deinen Code), ich vermute, sie haben alle etwas mit 'uberGameObject' zu tun.

Also vielleicht zwei Probleme.

1) Möglicherweise haben Sie ein "Gott" -Objekt - erledigt Tonnen von Dingen, die nicht wirklich nach dem segmentiert sind, was sie tun oder sind.

2) Durchlaufen Sie diese Liste und geben Sie das Casting ein? oder Unboxing jeweils. Dies hat einen großen Leistungsverlust zur Folge.

Überlegen Sie, ob Sie verschiedene Typen (Kugeln, Bösewichte usw.) in ihre eigenen stark typisierten Listen aufteilen möchten.

List<BadGuyObj> BadGuys = new List<BadGuyObj>();
List<BulletsObj> Bullets = new List<BulletObj>();

 //Game 
OnUpdate(gameticks gt)
{  foreach (BulletObj thisbullet in Bullets) {thisbullet.Update();}  // much quicker.

Es ist nicht erforderlich, ein Typ-Casting durchzuführen, wenn es eine Art gemeinsame Basisklasse oder Schnittstelle gibt, die alle Methoden enthält, die in der Aktualisierungsschleife aufgerufen werden (z. B. update()).
Bummzack

0

Ich kann einen Kompromiss zwischen einer allgemeinen Einzelliste und separaten Listen für jeden Objekttyp vorschlagen.

Behalten Sie den Verweis auf ein Objekt in mehreren Listen bei. Diese Listen werden durch das Basisverhalten definiert. Zum Beispiel MarineSoldierwird Objekt referenziert in PhysicalObjList, GraphicalObjList, UserControlObjList, HumanObjList. Wenn Sie also die Physik bearbeiten, durchlaufen Sie sie PhysicalObjList, wenn Sie durch Gift beschädigte Prozesse ausführen - HumanObjListwenn Soldier stirbt - entfernen Sie sie, HumanObjListaber behalten Sie sie bei PhysicalObjList. Damit dieser Mechanismus funktioniert, müssen Sie das automatische Einfügen / Entfernen von Objekten beim Erstellen / Löschen organisieren.

Vorteile des Ansatzes:

  • typisierte Organisation von Objekten (nein AllObjectsListmit Casting on Update)
  • Listen basieren auf Logik, nicht auf Objektklassen
  • Keine Probleme mit Objekten mit kombinierten Fähigkeiten ( MegaAirHumanUnderwaterObjectwürde in entsprechende Listen eingefügt, anstatt eine neue Liste für ihren Typ zu erstellen)

PS: Bei der Selbstaktualisierung treten Probleme auf, wenn mehrere Objekte interagieren und jedes von anderen abhängig ist. Um dies zu lösen, müssen wir das Objekt von außen beeinflussen, nicht von innen selbst aktualisieren.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.