Wie kann die Spielelogik von Animationen und der Draw-Schleife getrennt werden?

9

Ich habe bisher nur Flash-Spiele mit MovieClips und dergleichen erstellt, um meine Animationen von meiner Spielelogik zu trennen. Jetzt versuche ich, ein Spiel für Android zu entwickeln, aber die Spielprogrammierungstheorie zur Trennung dieser Dinge verwirrt mich immer noch. Ich komme aus der Entwicklung von Nicht-Spiel-Webanwendungen, daher kenne ich mich mit MVC-ähnlichen Mustern aus und stecke in dieser Denkweise fest, wenn ich mich der Spielprogrammierung nähere.

Ich möchte beispielsweise mein Spiel abstrahieren, indem ich beispielsweise eine Spielbrettklasse habe, die die Daten für ein Kachelraster mit Instanzen einer Kachelklasse enthält, die jeweils Eigenschaften enthalten. Ich kann meiner Draw-Loop Zugriff darauf gewähren und das Spielbrett basierend auf den Eigenschaften der einzelnen Kacheln auf dem Spielbrett zeichnen lassen, aber ich verstehe nicht, wohin genau die Animation gehen soll. Soweit ich das beurteilen kann, befindet sich die Animation zwischen der abstrahierten Spiellogik (Modell) und der Zeichenschleife (Ansicht). Mit meiner MVC-Einstellung ist es frustrierend zu entscheiden, wohin die Animation eigentlich gehen soll. Es würde eine Menge Daten wie ein Modell enthalten, muss aber anscheinend sehr eng mit der Zeichenschleife gekoppelt sein, um beispielsweise rahmenunabhängige Animationen zu erhalten.

Wie kann ich aus dieser Denkweise ausbrechen und über Muster nachdenken, die für Spiele sinnvoller sind?

TMV
quelle

Antworten:

6

Die Animation kann immer noch perfekt zwischen Logik und Rendering aufgeteilt werden. Der abstrakte Datenstatus der Animation ist die Information, die Ihre Grafik-API zum Rendern der Animation benötigt.

In 2D-Spielen kann dies beispielsweise ein rechteckiger Bereich sein, der den Bereich markiert, in dem der aktuelle Teil Ihres Sprite-Blattes angezeigt wird, der gezeichnet werden muss (wenn Sie ein Blatt haben, das beispielsweise aus 30 80x80-Zeichnungen besteht, die die verschiedenen Schritte Ihres Charakters enthalten springen, sitzen, sich bewegen etc.). Es können auch alle Arten von Daten sein, die Sie nicht zum Rendern benötigen, sondern möglicherweise zum Verwalten der Animationszustände selbst, z. B. die verbleibende Zeit bis zum Ablauf des aktuellen Animationsschritts oder der Name der Animation ("Gehen", "Stehen"). etc.) All das kann beliebig dargestellt werden. Das ist der logische Teil.

Im Rendering-Teil machen Sie es einfach wie gewohnt, holen dieses Rechteck aus Ihrem Modell und verwenden Ihren Renderer, um die Aufrufe der Grafik-API tatsächlich auszuführen.

Im Code (hier mit C ++ - Syntax):

class Sprite //Model
{
    private:
       Rectangle subrect;
       Vector2f position;
       //etc.

    public:
       Rectangle GetSubrect() 
       {
           return subrect;
       }
       //etc.
};

class AnimatedSprite : public Sprite, public Updatable //arbitrary interface for classes that need to change their state on a regular basis
{
    AnimationController animation_controller;
    //etc.
    public:
        void Update()
        {
            animation_controller.Update(); //Good OOP design ;) It will take control of changing animations in time etc. for you
            this.SetSubrect(animation_controller.GetCurrentAnimation().GetRect());
        }
        //etc.
};

Das sind die Daten. Ihr Renderer nimmt diese Daten und zeichnet sie. Da sowohl normale als auch animierte Sprites auf dieselbe Weise gezeichnet werden, können Sie hier die Polymorphie verwenden!

class Renderer
{
    //etc.
    public:
       void Draw(const Sprite &spr)
       {
           graphics_api_pointer->Draw(spr.GetAllTheDataThatINeed());
       }
};

TMV:

Ich habe mir ein anderes Beispiel ausgedacht. Angenommen, Sie haben ein Rollenspiel. Ihr Modell, das beispielsweise die Weltkarte darstellt, müsste wahrscheinlich die Position des Charakters in der Welt als Kachelkoordinaten auf der Karte speichern. Wenn Sie den Charakter jedoch bewegen, gehen sie jeweils einige Pixel zum nächsten Quadrat. Speichern Sie diese Position "zwischen Kacheln" in einem Animationsobjekt? Wie aktualisiere ich das Modell, wenn der Charakter endlich an der nächsten Kachelkoordinate auf der Karte "angekommen" ist?

Die Weltkarte kennt die Position des Spielers nicht direkt (sie hat keinen Vector2f oder ähnliches, in dem die Position des Spielers direkt gespeichert ist =, stattdessen verweist sie direkt auf das Spielerobjekt selbst, das wiederum von AnimatedSprite abgeleitet ist So können Sie es einfach an den Renderer übergeben und erhalten alle erforderlichen Daten.

Im Allgemeinen sollte Ihre Tilemap jedoch nicht alles können - ich hätte eine Klasse "TileMap", die sich um die Verwaltung aller Kacheln kümmert, und möglicherweise auch eine Kollisionserkennung zwischen Objekten, die ich ihr übergebe, und die Kacheln auf der Karte. Dann hätte ich eine andere "RPGMap" -Klasse, oder wie auch immer Sie sie nennen möchten, die sowohl einen Verweis auf Ihre Tilemap als auch den Verweis auf den Player enthält und die eigentlichen Update () -Aufrufe an Ihren Player und an Ihren Tilemap.

Wie Sie das Modell aktualisieren möchten, wenn sich der Player bewegt, hängt davon ab, was Sie tun möchten.

Darf sich Ihr Spieler unabhängig zwischen den Plättchen bewegen (Zelda-Stil)? Behandeln Sie einfach die Eingabe und bewegen Sie den Player in jedem Frame entsprechend. Oder soll der Spieler "rechts" drücken und dein Charakter bewegt automatisch ein Plättchen nach rechts? Lassen Sie Ihre RPGMap-Klasse die Position des Spielers interpolieren, bis er an seinem Ziel ankommt, und sperren Sie währenddessen die gesamte Eingabe der Bewegungstasten.

In beiden Fällen verfügen alle Modelle über Update () -Methoden, wenn Sie tatsächlich eine Logik benötigen, um sich selbst zu aktualisieren (anstatt nur die Werte von Variablen zu ändern). Sie geben den Controller nicht weiter Auf diese Weise verschieben Sie im MVC-Muster einfach den Code von "einem Schritt über" (dem Controller) nach unten zum Modell, und der Controller ruft lediglich diese Update () -Methode des Modells auf (in unserem Fall wäre dies der Controller) die RPGMap). Sie können den Logikcode weiterhin problemlos austauschen. Sie können den Code der Klasse direkt ändern oder, wenn Sie ein völlig anderes Verhalten benötigen, einfach von Ihrer Modellklasse ableiten und nur die Update () -Methode überschreiben.

Dieser Ansatz reduziert Methodenaufrufe und ähnliches erheblich - was früher einer der Hauptnachteile des reinen MVC-Musters war (am Ende rufen Sie GetThis () GetThat () sehr, sehr oft auf) - und verlängert den Code sowohl länger als auch länger ein bisschen schwieriger zu lesen und auch langsamer - auch wenn Ihr Compiler sich darum kümmert, der viele solche Dinge optimiert.

TravisG
quelle
Würden Sie die Animationsdaten in der Klasse mit der Spielelogik, der Klasse mit der Spielschleife oder getrennt von beiden behalten? Es liegt auch ganz bei der Schleife oder der Klasse, die die Schleife enthält, zu verstehen, wie Animationsdaten in das tatsächliche Zeichnen des Bildschirms übersetzt werden, oder? Es ist oft nicht so einfach, nur ein Rechteck zu erhalten, das einen Abschnitt des Sprite-Blattes darstellt, und damit einen Bitmap-Draw aus dem Sprite-Blatt zu schneiden.
TMV
Ich habe mir ein anderes Beispiel ausgedacht. Angenommen, Sie haben ein Rollenspiel. Ihr Modell, das beispielsweise die Weltkarte darstellt, müsste wahrscheinlich die Position des Charakters in der Welt als Kachelkoordinaten auf der Karte speichern. Wenn Sie den Charakter jedoch bewegen, gehen sie jeweils einige Pixel zum nächsten Quadrat. Speichern Sie diese Position "zwischen Kacheln" in einem Animationsobjekt? Wie aktualisiere ich das Modell, wenn der Charakter endlich an der nächsten Kachelkoordinate auf der Karte "angekommen" ist?
TMV
Ich habe die Antwort auf Ihre Frage bearbeitet, da die Kommentare nicht genügend Zeichen dafür zulassen.
TravisG
Wenn ich alles richtig verstehe:
TMV
Sie könnten eine Instanz einer "Animator" -Klasse in Ihrer Ansicht haben, und es würde eine öffentliche "Aktualisierungs" -Methode geben, die von der Ansicht jeden Frame aufgerufen wird. Die Aktualisierungsmethode ruft die "Aktualisierungs" -Methoden von Instanzen verschiedener Arten einzelner Animationsobjekte auf. Der Animator und die darin enthaltenen Animationen haben einen Verweis auf das Modell (überliefert durch ihre Konstruktoren), damit sie die Modelldaten aktualisieren können, wenn eine Animation dies ändern würde. In der Zeichenschleife erhalten Sie dann Daten aus den Animationen im Animator auf eine Weise, die von der Ansicht verstanden und gezeichnet werden kann.
TMV
2

Ich kann darauf aufbauen, wenn Sie es wünschen, aber ich habe einen zentralen Renderer, der angewiesen wird, in der Schleife zu zeichnen. Eher, als

handle input

for every entity:
    update entity

for every entity:
    draw entity

Ich habe eher ein System wie

handle input (well, update the state. Mine is event driven so this is null)

for every entity:
    update entity //still got game logic here

renderer.draw();

Die Renderer-Klasse enthält einfach eine Liste von Verweisen auf zeichnbare Komponenten von Objekten. Diese sind der Einfachheit halber in den Konstruktoren zugeordnet.

Für Ihr Beispiel hätte ich eine GameBoard-Klasse mit mehreren Kacheln. Jede Kachel kennt offensichtlich ihre Position und ich gehe von einer Art Animation aus. Berücksichtigen Sie dies in einer Art Animationsklasse, die der Kachel gehört, und lassen Sie sie einen Verweis auf eine Renderer-Klasse übergeben. Dort trennten sich alle. Wenn Sie Tile aktualisieren, wird Update für die Animation aufgerufen oder selbst aktualisiert. Wenn Renderer.Draw()es aufgerufen wird, wird die Animation gezeichnet.

Die rahmenunabhängige Animation sollte nicht zu viel mit der Zeichenschleife zu tun haben.

Die kommunistische Ente
quelle
0

Ich habe die Paradigmen in letzter Zeit selbst gelernt. Wenn diese Antwort unvollständig ist, wird sicher jemand etwas hinzufügen.

Die Methode, die für das Spieldesign am sinnvollsten erscheint, besteht darin, die Logik von der Bildschirmausgabe zu trennen.

In den meisten Fällen möchten Sie einen Multithread-Ansatz verwenden. Wenn Sie mit diesem Thema nicht vertraut sind, handelt es sich um eine eigene Frage. Hier ist die Einführung des Wikis . Im Wesentlichen möchten Sie, dass Ihre Spielelogik in einem Thread ausgeführt wird und Variablen sperrt, auf die zugegriffen werden muss, um die Datenintegrität sicherzustellen. Wenn Ihre Logikschleife unglaublich schnell ist (Super-Mega-animiertes 3D-Pong?), Können Sie versuchen, die Frequenz zu korrigieren, die die Schleife ausführt, indem Sie den Thread für kurze Zeit ausschalten (120 Hz wurden in diesem Forum für Spielphysik-Schleifen vorgeschlagen). Gleichzeitig zeichnet der andere Thread den Bildschirm mit den aktualisierten Variablen neu (60 Hz wurden in anderen Themen vorgeschlagen) und fordert erneut eine Sperre für die Variablen an, bevor er auf sie zugreift.

In diesem Fall gehen Animationen oder Übergänge usw. in den Zeichnungsthread, aber Sie müssen durch eine Art Flag (möglicherweise eine globale Statusvariable) signalisieren, dass der Spielelogik-Thread nichts tun darf (oder etwas anderes tun muss ... richten Sie das ein neue Kartenparameter vielleicht).

Sobald Sie sich mit der Parallelität vertraut gemacht haben, ist der Rest ziemlich verständlich. Wenn Sie keine Erfahrung mit Parallelität haben, empfehle ich Ihnen dringend, ein paar einfache Testprogramme zu schreiben, damit Sie verstehen, wie der Ablauf abläuft.

Hoffe das hilft :)

[Bearbeiten] Auf Systemen, die kein Multithreading unterstützen, kann die Animation weiterhin in die Zeichenschleife verschoben werden. Sie möchten den Status jedoch so einstellen, dass der Logik signalisiert wird, dass etwas anderes auftritt, und das nicht weiter verarbeiten aktuelles Level / Karte / etc ...

Stephen
quelle
1
Ich bin hier nicht einverstanden. In den meisten Fällen möchten Sie kein Multithreading durchführen, insbesondere wenn es sich um ein kleines Spiel handelt.
Die kommunistische Ente
@TheCommunistDuck Fair genug, Overhead und Komplexität für Multithreading können definitiv zu einem Overkill führen. Wenn das Spiel klein ist, sollte es schnell aktualisiert werden können.
Stephen