Tutorial: Ein GameStateManager mit Interfaces

Übersicht Monkey FAQs und Tutorials

Neue Antwort erstellen

Farbfinsternis

Betreff: Tutorial: Ein GameStateManager mit Interfaces

BeitragDo, Nov 28, 2013 22:47
Antworten mit Zitat
Benutzer-Profile anzeigen
Interfaces sind eine extrem nützliche Sache und ich möchte versuchen diese hier halbwegs verständlich zu erklären. Als Beispiel soll ein GameStateManager dienen der es gestattet zwischen den verschiedenen States eines Spiels zu wechseln.
Stelle Dir vor Dein Spiel besitzt die States Intro, MainMenu, InGame und GameOver. Diese lassen sich in der Regel wechseln, das heißt man kommt vom Intro in das MainMenu, von dort ins Game und zurück in MainMenu und irgendwann erscheint der State GameOver. Um das zu realisieren könnte man das Probem mit dem Wechsel der States (oder Ansichten) mit ganz viel Code bewerfen, oder aber einfach ein Interface realisieren welches alle States implementieren.

Jetzt gehts los.

Interfaces werden in Monkey, wer hätte das gedacht, mit dem Schlüsselwort Interface eingeleitet und wie andere Konstrukte auch mit End abgeschlossen:
Code: [AUSKLAPPEN]

Interface IMyInterface
   ...
End

In einem Interface schreibt man aber keinen ausufernden Code, sondern nur Deklarationen der Methoden die die Klassen implementieren müssen wenn sie dieses Interface nutzen wollen. Fangen wir gleich mit dem kalten Wasser an: Das GameState Interface:
Code: [AUSKLAPPEN]

Interface IGameState
   Method update:IGameState()
   Method render:Void()
End

Dieses Interface sieht vor dass ein GameState eine Methode update besitzt welche einen Wert vom Typ IGameState zurück gibt (wozu das so sein soll kommt noch!) und eine Methode render in der das Eyecandy eingebaut werden soll.

Das Interface alleine macht aber noch keinen GameStateManager, denn eine Methode die nur deklariert ist kann man nicht aufrufen.

Tipp: Etwas zu deklarieren bedeutet: Ich sage schon mal an dass ich es irgendwann benutzen will, gehe aber noch nicht ins Detail. Etwas zu definieren heißt: Jetzt wird es ernst, ich sage Dir wie das Ding aussieht und wie man es benutzt!

Um dieses Interface also nutzen zu können benötigen wir eine Klasse welche dieses Interface implementiert. Um auch gleich noch den Rundumschlag zur Veerbung zu schaffen basteln wir uns eine Klasse welche IGameState implementiert und von welcher unsere eigentlichen States später erben. Das ist super bequem und ihr werdet bald erkennen warum.
Code: [AUSKLAPPEN]

Class GameState Implements IGameState
   Method update:GameState()
      Return Self
   End
   
   Method render:Void()
   
   End
End

Sieht erstmal recht komisch aus aber passt auf, das ist ganz einfach:
Unsere Klasse GameState implementiert das Interface IGameState, somit haben alle Methoden in GameState zwei Rückgabetypen: GameState und IGameState.

Die Klasse GameState soll aber nur als Vorlage für unsere eigentlichen GameStates dienen, also basteln wir uns mal eine MainMenu-Klasse welche vom GameState erbt:
Code: [AUSKLAPPEN]

Class MainMenu Extends GameState
   
End

Unsere Klasse MainMenu kann nun nicht nur alles was GameState kann, sondern implementiert gleich noch IGameState mit. Wir überschreiben natürlich die Methoden der Elternklasse, denn wir wollen ja selbst bestimmen was in der Klasse passiert:
Code: [AUSKLAPPEN]

Class MainMenu Extends GameState
   Method update:GameState()
      Local result:GameState = Self
      
      Return result
   End
   
   Method render:Void()
      Cls(255, 0, 0)
   End
End

Die Klasse MainMenu beschreibt die Methoden update und render nun schon etwas genauer. Wir wollen dass die Methode update solange sich selbst zurück gibt bis sie nichts mehr zu tun hat und in den nächsten State wechseln möchte. Den State-Wechsel machen wir uns beobachtbar indem wir in render den Screen/das Canvas rot färben. Um aber einen nächsten State zu haben müssen wir unsere Klasse GameState noch etwas erweitern:
Code: [AUSKLAPPEN]

Class GameState Implements IGameState
   Method update:GameState()
      Return Self
   End
   
   Method render:Void()
   
   End
   
   Method setNext:Void(state:GameState)
      Self._next = state
   End
   
   Method setPrevious:Void(state:GameState)
      Self._prev = state
   End
   
   Private
   Field _next:GameState, _prev:GameState
End

Mit den neuen Methoden setNext und setPrevious ist es uns nun möglich zu definieren was vor dem aktuellen State für ein GameState war und welcher GameState angefahren werden soll wenn der aktuelle State beendet wird. Bedenkt dass wir das in MainMenu direkt verwenden können da wir ja von der Klasse GameState erben.

Brauchen wir noch unsere "InGame" Klasse, welche wir natürlich auch von GameState erben:
Code: [AUSKLAPPEN]

Class InGame Extends GameState
   Method update:GameState()
      Local result:GameState = Self
      
      Return result
   End
   
   Method render:Void()
      Cls(0, 255, 0)
   End
End

In dieser Klasse lassen wir den Screen grün füllen um ein visuelles Feedback über den Wechsel zu haben.

Um das alles nutzen zu können benötigen wir natürlich eine Klasse die als Hauptklasse fungiert und all das verwaltet was hier bisher beschrieben wurde. Leiten wir diese Klasse ganz normal von mojo.app ab und definieren unsere States:
Code: [AUSKLAPPEN]

Strict

Import mojo

Class Game Extends App
   Public
   Method OnCreate:Int()
      SetUpdateRate(30)
      
      Self._mainMenu = New MainMenu()
      Self._inGame = New InGame()
      
      Self._mainMenu.setNext(Self._inGame)
      Self._inGame.setPrevious(Self._mainMenu)
      Self._currentState = Self._mainMenu
      
      Return 1
   End
   
   Method OnUpdate:Int()
      Return 1
   End
   
   Method OnRender:Int()
      Return 1
   End
   
   Private
   Field _mainMenu:GameState
   Field _inGame:GameState
   Field _currentState:GameState
End

Function Main:Int()
   New Game()
   Return 1
End

In OnCreate erzeugen wir zuerst das MainMenu und dann den InGame-State und weisen diesen dann den jeweiligen Vorgänger und den jeweiligen Nachfolger zu. Zusätzlich sichern wir in "_currentState" die Instanz die gerade aktuell ist, in diesem Fall das MainMenu.
Um nun zwischen den States wechseln zu können müssen wir in den Klassen MainMenu und InGame auf ein Event reagieren und dann jeweils den letzten oder den nächsten State zurückgeben. Da dieses Event nicht immer gleich sein muss (im Menu klicken wir auf "Los", im Game stirbt der Character etc.) müssen wir diesen Wechsel direkt in den betroffen Klassen implementieren und können das nicht der Elternklasse überlassen:
Code: [AUSKLAPPEN]

Class MainMenu Extends GameState
   Method update:GameState()
      Local result:GameState = Self
      
      If MouseHit() result = Self._next
      
      Return result
   End
   
   Method render:Void()
      Cls(0, 255, 0)
   End
End

Class InGame Extends GameState
   Method update:GameState()
      Local result:GameState = Self
      
      If MouseHit() result = Self._prev
      
      Return result
   End
   
   Method render:Void()
      Cls(0, 255, 0)
   End
End

Der Einfachheit halber habe ich in diesem Beispiel nur die tatsächlich definierten States implementiert. Das MainMenu kennt halt nur einen nächsten State (InGame) und das Game kennt halt nur den vorherigen State (MainMenu).
Nun müssen wir nur noch die Klasse Game anpassen um zwischen den States per Mausklick wechseln zu können:
Code: [AUSKLAPPEN]

Strict

Import mojo

Class Game Extends App
   Public
   Method OnCreate:Int()
      SetUpdateRate(30)
      
      Self._mainMenu = New MainMenu()
      Self._inGame = New InGame()
      
      Self._mainMenu.setNext(Self._inGame)
      Self._inGame.setPrevious(Self._mainMenu)
      Self._currentState = Self._mainMenu
      
      Return 1
   End
   
   Method OnUpdate:Int()
      Self._currentState = Self._currentState.update()
      Return 1
   End
   
   Method OnRender:Int()
      Self._currentState.render()
      Return 1
   End
   
   Private
   Field _mainMenu:GameState
   Field _inGame:GameState
   Field _currentState:GameState
End

Function Main:Int()
   New Game()
   Return 1
End

Der aktuelle State gibt solange sich selbst zurück und wird in "_currentState" geschrieben bis der Anwender die linke Maustaste drückt, dann wird der nächste oder vorherige State (je nach Aufgabe) zurück gegeben und in "_currentState" geschrieben. Die Methode render ruft die Render-Methode des aktuellen States auf und vóila, es läuft...

Fragen (ich könnte etwas undeutlich formuliert haben) und Kommentare (ich bin nicht omnipotent Wink ) gerne in diesen Thread.

Thorsten

BeitragDo, Dez 19, 2013 13:27
Antworten mit Zitat
Benutzer-Profile anzeigen
Wo genau ist das Interface jetzt nötig?

DAK

BeitragDo, Dez 19, 2013 16:12
Antworten mit Zitat
Benutzer-Profile anzeigen
Ein Interface ist im Grunde eine Überklasse, in der keine Methoden / Funktionen implementiert sind, und die keine Felder hat.
Es definiert aber Funktions-/Methodenheader.

Alle Klassen, die ein Interface implementieren, können überall verwendet werden, wo man Objekte des Interfaces verwenden kann.

Z.B.:

BlitzMax: [AUSKLAPPEN]
Interface ITest
Method mtest:Int(a:Int)
End

Type TTestA Implements ITest
Method mtest:Int(a:Int)
Return a*5;
EndMethod
End Type

Type TTestB Implements ITest
Method mtest:Int(a:Int)
Return a*17;
EndMethod
End Type

Local t1:ITest = New TTestA
Print(t1.mtest(5))
Local t2:ITest = New TTestB
Print(t2.mtest(5))


Trotzdem dass TTestA und TTestB was anderes sind, kann man beide behandeln als wären sie ITest.
Gewinner der 6. und der 68. BlitzCodeCompo

das wurgel

BeitragFr, Dez 20, 2013 18:01
Antworten mit Zitat
Benutzer-Profile anzeigen
Das gute an Interfaces ist, dass unabhängig von der Oberklasse gleich mehrere Inferfaces implemtiert werden können. Das gute an Vererbung ist, dass Felder und bereits ausprogrammierte Methoden mitgerebt werden. Beides ist gut, um eine gemeinsame Schnittstelle bereitzustellen.
1 ist ungefähr 3

Neue Antwort erstellen


Übersicht Monkey FAQs und Tutorials

Gehe zu:

Powered by phpBB © 2001 - 2006, phpBB Group