Allgemeine, saubere Trennung von Spiellogik und Grafik



  • Ich würde mal gern wissen, wie man in einem Spiel am besten die Spiellogik von der Grafikausgabe trennt und dabei Redundanzen vermeidet.
    O.k., stellen wir uns ein einfaches 2D-Spiel aus der Draufsicht vor und gehen wir der Einfachheit halber davon aus, daß sich die Figuren nicht pixelgenau, sondern nur von einer "Koordinate" zur nächsten bewegen können. Das Spielfeld sieht also so aus:

    +----------+
    |       G  |
    |  SH      |
    |         H|
    |  H   G   |
    | H        |
    +----------+
    

    S steht für Spieler, G für Gegner und H für Hindernis.

    Bei so einem Spiel würde ich die Positionen der Hindernisse und irgendwelcher anderer nicht beweglicher Objekte innerhalb der Spiellogik in einem zweidimensionalen Feld von der Größe 10*5 speichern. Die Figuren würden dann per X- und Y-Variable ihre Positionen auf diesem Spielfeld bekommen. Es gäbe eine Funktion

    void Move(Direction direction)
    

    für die Bewegung, die von außen (beispielsweise von der KeyDown-Funktion der GUI, auf der das Spiel dargestellt wird) aufgerufen wird und die innerhalb der Spiellogik die aktuelle Bewegungsrichtung des Spielers setzt oder (aus KeyUp heraus) wieder aufhebt.

    Die Spielschleife würde ich ebenfalls in dem Logikteil machen, wobei die Geschwindigkeit der Gegner oder auch die Geschwindikeit der Figur bei aktivierter Bewegung durch das Ermitteln der Rechnerzeit realisiert wird. Irgendwie sowas im Stil von

    while (!levelFinished)
    {
        clock_t currentClock = clock();
    
        if ((currentClock - previousClock) / CLOCKS_PER_SEC >= 1)
        {
            MoveOponents();
    
            switch (playerDirection)
            {
            case Up:
                playerY--;
                break;
            // ...
            }
    
            previousClock = currentClock;
        }
    }
    

    Ich denke, Ihr wißt, was ich meine.

    Meine Vorstellung ist nun, daß die Spiellogik völlig plattformunabhängig abläuft, und zwar sowohl in den Headerdateien, als auch in der Implementierung. Immerhin handelt es sich nur um Algorithmen. Positionen von Objekten werden lediglich mit einfachen Zahlenvariablen gespeichert und die Zeitmessung erfolgt mit clock und nicht mit irgendwelchen Windows-Timern oder Grafikframeberechnungen. Also läuft der Logikteil immer gleich ab, egal, ob das Spiel als simples 2D-Spiel dargestellt wird oder ob ich die Figuren und Objekte auf dem Bildschirm 3dimensional modelliere.

    Doch jetzt kommen wir zur Grafik: Wie baue ich die am besten ein? Ich denke mal, daß der Logikteil nicht auf den Grafikteil zugreift, sondern eher umgekehrt: Der Grafikteil holt sich das Abbild des Spielfeldes aus der Spiellogik (dieses zweidimensionale Feld) und baut daraufhin das Bild auf, das es dann an den GUI-Teil weitergibt. Wäre ja an sich nicht so kompliziert, aber das Problem ist jetzt, daß nicht der gesamte Bildschirm alle paar Millisekunden von Grund auf neu gezeichnet werden kann. Das heißt, folgender Algorithmus würde im Grafikteil viel zu langsam sein:

    - Spielfeldabbild holen
    - Gucken, welche Spielobjekte zu sehen sind und die entsprechenden Grafiken aus den Ressourcen laden
    - Hintergrund zusammenbasteln und in den Back-Buffer kopieren
    - Spielfiguren in den Back-Buffer kopieren
    - Back-Buffer ausgeben

    Stattdessen würde man ja vor dem Level alles machen, was sich später nicht wiederholen muß:

    - Gucken, welche Spielobjekte in dem Level gebraucht werden und die entsprechenden Grafiken aus den Ressourcen laden
    - Hintergrund zusammenbasteln

    Und dann, wärend des Spiels passiert nur noch folgendes:

    - Spielfeldabbild holen
    - Hintergrund in den Back-Buffer kopieren
    - Spielfiguren in den Back-Buffer kopieren
    - Back-Buffer ausgeben

    Jetzt ist da aber die Frage: Wie macht man das am elegantesten? Dadurch, daß man im Grafikteil und im Logikteil die Informationen der Spielfiguren hat, hat man ja quasi eine Abhängigkeit geschaffen, die, wenn man sie ändern will, an zwei Stellen geändert werden muß. Das heißt, wenn mir einfällt, daß für Level 3 nicht nur die roten Gegner auftauchen können, sondern zusätzlich auch die blauen, muß ich sowohl in der Spiellogik neue Variablen einbauen, als auch im Grafikteil die neuen Sprites laden. Oder wenn ich es jetzt machen will, daß sich Wände unter bestimmten Umständen bewegen können. Dann muß ich den Spiellogikteil ändern, aber auch beim Grafikteil festlegen, daß die Wände nun nicht am Anfang fest auf den Hintergrund gezeichnet werden.

    Und deshalb würde ich gern wissen: Wie verbindet man am besten die Spiellogik, die bei mir völlig ISO-konform geschrieben werden soll, und die Grafikausgabe, welche vielleicht eine plattformunabhängige Abstraktionsschicht liefert, um die Funktionalitäten auch hier plattformunabhängig zu halten, die aber je nach benutztem Framework, Betriebssystem oder Grafikstil eine andere Implementierung hat?

    Ich würde mich freuen, wenn Ihr mir hierzu helfen könntet. Allerdings möchte ich bereits vorher anmerken: Ein Tutorial im Stil von "Spieleprogrammierung mit C++ und DirectX" würde mir wohl nichts nützen, denn ich weiß, wie man programmiert und ich weiß auch, wie man Grafik auf den Bildschirm bringt. Mir geht es wirklich um ein generelles Konzept der logischen Trennung. Ein allgemeines Spielprogrammierungstutorial, wo dann GUI, Grafik und Spiellogik vermischt werden, bräuchte ich nicht.

    Eine zusätzliche Frage: Nehmen wir an, das Spiel läuft ganz simpel auf einem Fenster: Wie bekomme ich es hin, daß im Programm sowohl die Spielschleife, als auch die Windows-Message-Schleife läuft und eins das andere nicht am Ausführen hindert? Bräuchte man hier einen extra Thread für die Spiellogik, so daß Spielfluß und Fensterbehandlung gleichzeitig und unabhängig voneinander ablaufen können?



  • MVC ist bekannt?



  • Ich hab's mir mal durchgelesen. Wenn ich das richtig verstanden habe, wäre die Zuordnung in meinem Fall ungefähr folgendermaßen:

    Model = Spiellogik
    View = Grafikausgabe
    Controller = Steuerung

    Die GUI würde dabei ja normalerweise zum View gehören und nur deren Events gehören zum Controller. In meinem Fall jedoch würde ich die GUI komplett zum Controller zählen, da sie mit der eigentlich für den Benutzer interessanten Ausgabe, des Spielfeldes, nichts zu tun hat und stattdessen dafür zuständig ist, die Steuerung aufzunehmen (Stichwort Message-Loop und KeyDown) und weiterzuleiten, während das eigentliche Aussehen des Fensters überhaupt keine Relevanz für das Spiel hat.

    Für den Fall, daß ich oben jetzt keinem Trugschluß erliege, muß ich nun schlußfolgern, daß ich mir nicht sicher bin, ob ich die MVC so ohne weiteres auf mein Spieleproblem übertragen kann. Denn der Model-Teil ist ja eigentlich nur für die Ablegung und Abrufung der Daten zuständig. In dem Spiel ist die Spiellogik jedoch nicht nur der Teil, der die Daten speichert (Figurenpositonen, Levelaufau etc.), sondern das ist ja im wesentlichen auch der Teil, wo durch die Spielschleife die "Action" stattfindet. Während also im normalen MVC die Daten irgendwo liegen und dann durch den Controller und den View angestoßen und verändert oder nur abgerufen werden, befinden sich die Daten in meinem Spiel in millisekündlicher Änderung und der Grafikteil, den ich hier als View eingeordnet habe, muß ständig gucken, ob er sich selbst anhand der Daten aktualisieren muß. (Das wäre wohl ungefähr so, als würden sich die Daten in einer Datenbank ständig verändern und eine Windows-Anwendung müßte ihre Text-, Combo- und Checkboxen live aktualisieren. Und ich denke nicht, daß es das ist, was das MVC bezweckt.)

    Auch ist durch dieses Pattern, soweit ich sehe, nicht das Problem gelöst, wie ich den Grafikteil die entsprechenden Grafiken laden lasse, ohne redundante Informationen zwischen Spiellogik und Grafik zu haben. Und im Spiellogikteil kann ich die Grafikressourcen definitiv nicht speichern, da dieser Teil ja wirklich nur eine abstrakte Repräsentation des Spielgeschehens ist, während die visuelle Umsetzung vollkommen davon losgekapselt sein soll.

    Vielleicht bin ich mit meinen Vermutungen auch komplett auf dem Holzweg und das MVC eignet sich durchaus für mein Problem. Aber in dem Fall bräuchte ich eine Erläuterung, wo ich bei meinen Ausführungen einen Fehler gemacht habe.



  • Die GUI würde dabei ja normalerweise zum View gehören

    Das MVC-Pattern wird haeufig fuer GUIs eingesetzt um eine Trennung zwsichen Darstellung und Daten zu gewaehrleisten, sodass die GUI eben gerade nicht komplett zur View gehoert, sondern "G" und "UI" separat und austauschbar sind - was sich soweit mit Deinen Anforderungen deckt.

    In Deinem Fall wuerde das "Model" alle Daten des Spiels enthalten, je nach Art des Spiels zb Objekt- und Spielerposition, deren Zustand/Parameter, Spielfeld, usw.
    Der "Controller" enthaelt Steuerung und Spiellogik und modifiziert entsprechend die Daten im Model.
    Die "View" wird nicht bei jeder Veraenderung des Models aufgerufen sondern in (einigermassen) konstanten Intervallen (VSync) und stellt den gerade aktuellen "Zustand" dar.
    Um diesen Zustand eindeutig zu halten verwendet man fuer gewoehnlich Double-Buffering indem ein neuer Zustand erzeugt wird waehrend der letzte existent und konstant bleibt bis der neue Zustand zur Anzeige gelangt.



  • Gut, aber wie würde das ganze jetzt in der Praxis aussehen? Ich verstehe weiterhin nicht so richtig, wie ich das ganze auf mein Problem anwenden soll, zumal ich nicht nur die Trennung zwischen Model, View und Controller hab, sondern das ganze bei mir ja folgendermaßen aufgebaut ist:

    Der Logikpart beinhaltet die Daten (Model) und die Steuerung (Controller). Wobei die Daten nur durch einfache Variablenwerte repräsentiert werden, nicht durch Grafik, und die Steuerung mit den Werten Oben, Unten, Rechts und Links sowie mit der entsprechenden Intervallmessung erfolgt.

    Der Grafikpart beinhaltet die Grafikdaten (wieder Model).

    Und die GUI beinhaltet dann nochmal die Steuerung (Controller), aber nur die Steruerungseingabe des Spielers, nicht die Bewegung der Gegner, und diesmal durch den tatsächlichen Tastendruck.

    Wie ich die Daten von der GUI trenne, ist ja nicht das Problem. Aber mir geht's eben darum, daß mein Spielinhalt einmal als simple Variablen und einmal als Grafik repräsentiert wird und das muß ja irgendwie miteinander synchronisiert werden. (Das Double Buffering wäre das ja nicht das Problem, sondern die Frage, wie ich ihm beibringe, daß er diese und jene Grafiken für dieses Level im Speicher halten soll, ohne dabei immer an beiden Stellen was ändern zu müssen.)



  • daß mein Spielinhalt einmal als simple Variablen und einmal als Grafik repräsentiert wird

    Ich verstehe nicht ganz, in wie fern das ein Problem ist.
    Die "View" muss in der Lage sein, alle darstellungsrelevanten Objekte des Models auch darstellen zu koennen indem sie die dafuer notwendigen Informationen vom Objekt abliesst.
    Zusaetzliche Resourcen werden dann von der View geladen und moeglichst auch da verwaltet. Ein Objekt "Gegner" koennte zb im einfachsten Fall ein Attribut "Dateiname" haben aus dem die entsprechende Bitmap hervor geht.
    Eine Abbildung zwischen Modelobjekt und Viewresourcen findet zb anhand einer eindeutigen Objekt-ID statt.
    Wenn die Resourcen im Vorfeld geladen werden sollen, sendet das Model ein entsprechendes Synchronisationssignal (neuer Level, folgende Objekte werden benoetigt: ...).



  • ich machs immer so in der art:

    // *** pseudocode ***
    
    // Interface zum verallgemeinern der Grafikausgabe
    interface ISprite
    {
        public void Draw(Graphics g, Vector2 position);
    }
    
    // Interface, das anzeigt, dass ein Objekt gezeichnet werden kann
    interface IDrawable
    {
        public void Draw(Graphics g);
    }
    
    // die spielfigur enthält alle logik und ein objekt, mit dem sie sich zeichnen lassen kann
    class Spielfigur : IDrawable
    {
        private Vector2 position;
        private Sprite ISprite;
    
        //...
    
        public void Draw(Graphics g)
        {
            sprite.Draw(g, position);
        }
    }
    
    // eine mögliche implementierung für das grafikobjekt
    class Sprite2D : ISprite
    {
        private Bitmap bmp;
    
        public void Draw(Graphics g, Vector2 position)
        {
            g.DrawBitmap(position.x, position.y, bmp.width, bmp.height);
        }
    }
    

    die spielfigur ist jetzt von der grafik unabhängig. alles, was sie benötigt ist ein objekt, welches das interface Sprite implementiert. dies kann man auf viele weisen tun und in unterschiedlichen versionen des programms unterschiedlich implementieren, oder von einem zustand abhängig machen kann. es interessiert die spielfigur nicht, wass bei Spielfigur.Draw(...) passiert, sie ruft einfach ihr zuständiges grafikobjekt und dieses kümmert sich dann wieder selbst ums zeichnen. man könnte so zum beispiel eine zeichenmethode haben, die mit directx zeichnet und eine mit gdi und was man sich sonst so ausdenken kann.
    wichtig: man braucht in der spielfigur die möglichkeit, das grafikobjekt (sprite) auszutauschen. dann kann man einfach per "steckprinzip" ein anderes grafikobjekt in die figur "einklinken" und schon wird die neue zeichenmethode angewendet.
    will man nur im textmodus arbeiten, kann man ein dummyobjekt reintun, dass bei Draw() einfach nichts tut und gibt ihr als parameter NULL mit.
    im Haupt-Loop des Spiels gibt es dann eine Draw-Methode, die dann alle zeichenbaren objekte (alle, die das Interface IDrawable implementieren) aus einer Liste heraus zeichenet.
    Für die Steuerung könnte man eine klasse "SpielfigurController" erstellen, die tastatureingaben auffängt und dann an die spielfigur weiterleitet. man könnte dann weitergehen und einen "SpielfigurJoystickController" schreiben, der vom ersten controller erbt und schon wieder hat man ein "stecksystem" wo man einfach nur ein objekt austauscht und anderes verhalten bekommt. der unterschied liegt dann immer nur in der implementierung der operationen.

    man muss immer sich überlegen, wie man ein "ding" auf seine wesentlichen bestandteile reduziert und dann schritt für schritt in der abstraktionsebene vom allgemeinen zum speziellen geht. so bekommt man mehrere, miteinander.. sagen wir kompatible objekte, wo man dann eins austauschen kann und es funktioniert trotzdem mit den anderen zusammen, weil die schnittstellen die gleichen sind.

    ich hab versucht das so einfach wie möglich auszudrücken. wenn was unklar oder ungenau ist, frag einfach 🙂


Anmelden zum Antworten