"Optimale" Spieleschleife für 2D-Side-Scroller


14

Ist es möglich, ein "optimales" (in Bezug auf die Leistung) Layout für die Spielschleife eines 2D-Side-Scrollers zu beschreiben? In diesem Zusammenhang nimmt die "Spielschleife" Benutzereingaben entgegen, aktualisiert den Status von Spielobjekten und zeichnet die Spielobjekte.

Zum Beispiel könnte eine GameObject-Basisklasse mit einer tiefen Vererbungshierarchie für die Wartung nützlich sein. Sie können Folgendes tun:

foreach(GameObject g in gameObjects) g.update();

Ich denke jedoch, dass dieser Ansatz zu Leistungsproblemen führen kann.

Andererseits könnten die Daten und Funktionen aller Spielobjekte global sein. Das wäre ein Wartungsproblem, könnte sich aber einer optimal ausgeführten Spielrunde nähern.

Irgendwelche Gedanken? Ich interessiere mich für praktische Anwendungen mit nahezu optimaler Spielschleifenstruktur ... auch wenn ich im Gegenzug für hervorragende Leistung Wartungsprobleme habe.


Antworten:


45

Zum Beispiel könnte eine GameObject-Basisklasse mit einer tiefen Vererbungshierarchie gut für die Wartung sein ...

Tatsächlich sind tiefe Hierarchien für die Wartbarkeit im Allgemeinen schlechter als flache, und der moderne Architekturstil für Spielobjekte tendiert zu flachen, aggregationsbasierten Ansätzen .

Ich denke jedoch, dass dieser Ansatz zu Leistungsproblemen führen kann. Andererseits könnten die Daten und Funktionen aller Spielobjekte global sein. Das wäre ein Wartungsproblem, könnte sich aber einer optimal ausgeführten Spielrunde nähern.

Die von Ihnen gezeigte Schleife weist möglicherweise Leistungsprobleme auf, jedoch nicht, wie in Ihrer nachfolgenden Anweisung impliziert, da die GameObjectKlasse Instanzdaten und Memberfunktionen enthält. Vielmehr ist das Problem, dass Sie, wenn Sie alle behandeln Objekt im Spiel als genau gleich behandeln, diese Objekte wahrscheinlich nicht intelligent gruppieren - sie sind wahrscheinlich zufällig über diese Liste verteilt. Potenziell ist dann jeder Aufruf der Aktualisierungsmethode für dieses Objekt (ob diese Methode eine globale Funktion ist oder nicht, und ob als Objekt Instanzdaten oder "globale Daten" in einer Tabelle schweben, in die Sie indizieren, oder was auch immer) unterscheidet sich von dem Aktualisierungsaufruf in den letzten Schleifeniterationen.

Dies kann einen erhöhten Druck auf das System ausüben, da Sie möglicherweise den Speicher mit der entsprechenden Funktion in den Arbeitsspeicher verschieben und häufiger den Befehls-Cache neu füllen müssen, was zu einer langsameren Schleife führt. Ob dies mit dem bloßen Auge (oder sogar in einem Profiler) erkennbar ist, hängt genau davon ab, was als "Spielobjekt" angesehen wird, wie viele davon durchschnittlich vorhanden sind und was in Ihrer Anwendung sonst noch vor sich geht.

Komponentenorientierte Objektsysteme sind derzeit ein beliebter Trend und nutzen die Philosophie, dass die Aggregation der Vererbung vorzuziehen ist . In solchen Systemen können Sie möglicherweise die "Aktualisierungs" -Logik von Komponenten aufteilen (wobei "Komponente" grob als eine Funktionseinheit definiert ist, z. B. als das, was den physikalisch simulierten Teil eines Objekts darstellt, der vom physikalischen System verarbeitet wird ) auf mehrere Threads - nach Komponententyp unterschieden -, wenn möglich und gewünscht, die einen Leistungsgewinn haben könnten. Zumindest können Sie die Komponenten so organisieren, dass alle Komponenten eines bestimmten Typs Update zusammen , so dass eine optimale Nutzung der Cache der CPU. Ein Beispiel für ein solches komponentenorientiertes System wird in diesem Thread erläutert .

Solche Systeme sind oft stark entkoppelt, was sich auch positiv auf die Wartung auswirkt.

Datenorientiertes Design ist ein verwandter Ansatz - es geht darum, sich in erster Linie an den für Objekte erforderlichen Daten zu orientieren, damit diese Daten (zum Beispiel) effektiv in großen Mengen verarbeitet werden können. Dies bedeutet im Allgemeinen eine Organisation, die versucht, Daten, die für denselben Zweck verwendet werden, zusammen zu halten und gleichzeitig zu verarbeiten. Es ist nicht grundsätzlich mit dem OO-Design unvereinbar, und Sie können hier bei GDSE in dieser Frage einiges Geschwätz zu diesem Thema finden .

In der Tat wäre ein optimaler Ansatz für die Spielschleife anstelle Ihres Originals

foreach(GameObject g in gameObjects) g.update();

so etwas wie

ProcessUserInput();
UpdatePhysicsForAllObjects();
UpdateScriptsForAllObjects();
UpdateRenderDataForAllObjects();
RenderEverything();

In einer solchen Welt, jeder GameObjectkönnte einen Zeiger oder Verweis auf seine eigene haben PhysicsDataoder Scriptoder RenderDatafür die Fälle , in denen Sie benötigen, mit Objekten auf individueller Basis zu interagieren, aber die eigentliche PhysicsData, Scripts, RenderDataund so weiter alle durch ihre jeweiligen Subsysteme im Besitz sein würde (Physik-Simulator, Script-Hosting-Umgebung, Renderer) und wie oben angegeben in großen Mengen verarbeitet.

Es ist sehr wichtig anzumerken, dass dieser Ansatz kein Wundermittel ist und nicht immer zu einer Leistungsverbesserung führt (obwohl er im Allgemeinen ein besseres Design als ein tiefer Vererbungsbaum ist). Es ist besonders wahrscheinlich, dass Sie im Grunde genommen keinen Leistungsunterschied bemerken, wenn Sie nur sehr wenige Objekte haben oder sehr viele Objekte, für die Sie die Aktualisierungen nicht effektiv parallelisieren können.

Leider gibt es keine solche magische Schleife, die am optimalsten ist - jedes Spiel ist anders und erfordert möglicherweise eine Leistungsoptimierung auf unterschiedliche Weise. Es ist also sehr wichtig, Dinge zu messen (zu profilieren), bevor Sie blindlings den Rat eines zufälligen Mannes im Internet befolgen.


5
Guter Herr, das ist eine großartige Antwort.
Raveline

2

Der Spielobjekt-Programmierstil ist gut für kleinere Projekte. Da das Projekt sehr umfangreich wird, werden Sie eine tiefe Vererbungshierarchie haben und die Spielobjektbasis wird sehr umfangreich!

Als Beispiel ... Angenommen, Sie haben mit der Erstellung einer Spielobjektbasis mit minimalen Attributen begonnen, z. B. Position (für x und y), displayObject (bezieht sich auf das Bild, wenn es sich um ein Zeichenelement handelt), Breite, Höhe usw.

Wenn wir später eine Unterklasse hinzufügen müssen, die einen Animationsstatus hat, dann verschieben Sie wahrscheinlich den 'Animationssteuercode' (sagen wir currentAnimationId, Anzahl der Frames für diese Animation, etc ..) in GameObjectBase in dem Gedanken, den die meisten haben Die Objekte haben eine Animation.
Wenn Sie später einen starren Körper hinzufügen möchten, der statisch ist (keine Animation hat), müssen Sie den 'Animationssteuercode' in der Aktualisierungsfunktion von GameObject Base überprüfen (auch wenn geprüft wird, ob Animation vorhanden ist oder nicht). ..es ist wichtig!) Im Gegensatz dazu haben Sie, wenn Sie der komponentenbasierten Struktur folgen, eine 'Animationssteuerungskomponente' an Ihr Objekt angehängt. Bei Bedarf wird diese Komponente angehängt. Bei einem statischen Körper wird diese Komponente nicht angehängt Auf diese Weise können wir unnötige Kontrollen vermeiden.

Die Auswahl des Stils hängt also von der Größe des Projekts ab. Die GameObject-Struktur ist nur für kleinere Projekte leicht zu beherrschen. Hoffentlich hilft dies ein bisschen.


@ Josh Petrie - Wirklich eine tolle Antwort!
Ayyappa

@ Josh Petrie - Wirklich gute Antwort mit nützlichen Links! (Sry weiß nicht, wie dieser Kommentar neben Ihrem Beitrag platziert werden soll: P)
Ayyappa

Sie können normalerweise unter meiner Antwort auf den Link "Kommentar hinzufügen" klicken. Dies erfordert jedoch mehr Reputation als Sie derzeit haben (siehe gamedev.stackexchange.com/privileges/comment ). Und danke!
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.