Game-Engine für textbasierendes Spiel: Architektur?

faraday

Mitglied
Hallo liebe Forumsgemeinde, ich hab da einige Fragen bezüglich der Architektur bzw. das Nutzen bestimmter Pattern. Ich versuche mich gerade an einem textbasierenden Adventure und versuche gerade eine Engine zu entwickeln. Die Welt soll durch eine Klasse World erzeugt werden. Da nur eine Welt existieren soll, hat der Konstruktur bereits folgende Struktur:
Java:
public class World {
	private Player player;
	private static World world; 
	
	private World( String name ) {
		this.player = new Player( name );
		this.generateWorld();
	}
	
	public static synchronized World getInstance( String name ) 
	{ 
		if ( world == null ) 
			world = new World( name ); 
		return world;
	}
	
	private void generateWorld() {
	}
Mit generateWorld() wird dann letztlich die Welt generiert. Wenn mich nicht alles täuscht heißt das Singleton-Pattern. Korrigiert mich bitte, wenn ich mich irre.

Jetzt frage ich mich allerdings, wie ich den Rest gestalten soll. Meine Eingabe soll durch eine Klasse InputHandler verarbeitet werden. Ist es nun sinnvoll die ganze Klasse mit statischen Fabrikmethoden auszustatten, der dann jedes mal die Welt-Instanz erzeugt/zurückgibt? Denn der Handler braucht ja die Information über die Welt.

Der Handler soll das Ganze dann an ein (noch nicht gedanklich entwickeltes) Eventsystem übergeben. Das funktioniert dann vermutlich am Besten über Observer/Observables, die ja bereits über die Bean implementiert sind. Oder?

Ich hoffe ich hab die Architekturen/Strukturen/Pattern jetzt nicht durcheinandergebracht und hoffe ihr könnt mir helfen meine Ideen in ein sinnvollen Gesamtkontext bzw Pattern zu übersetzen. Falls ich mich irgendwie unklar ausgedrückt habe, lasst es mich bitte wissen.

Liebe Grüße fara
 
Das Singleton-Pattern ist nur notwendig (sollte nur verwendet werden), wenn es dringend erforderlich ist, dass es nur eine Instanz der Klasse gibt. Bei einem Spiel ist es i.d.R. irrelevant wie viele Maps es gibt. Es wird auf dem Bildschirm nur eine Map angezeigt, Beim Spiel-Neustart wäre es theoretisch egal ob ich die alte Map wegwerfe und eine neue erstelle ... Zudem ist deine Implementierung falsch:

World.getInstance("Max");
World.getInstance("Mustermann");
World.getInstance("Peter");

-> Was erwartest du als Entwickler? Drei verschiedene Welten für die einzelnen Personen oder eine Welt, bei dem der Spieler "Max" heißt?

Jetzt frage ich mich allerdings, wie ich den Rest gestalten soll. Meine Eingabe soll durch eine Klasse InputHandler verarbeitet werden. Ist es nun sinnvoll die ganze Klasse mit statischen Fabrikmethoden auszustatten, der dann jedes mal die Welt-Instanz erzeugt/zurückgibt? Denn der Handler braucht ja die Information über die Welt.

Dein InputHandler braucht die Informationen über deine Welt definitiv nicht. Er leitet die Eingaben nur an die jeweiligen Klassen weiter und dir kümmern sich selbst um die Verarbeitung. Simples Beispiel:

Java:
public class InputHandler {
    onInput(Key key) {
        if(key.getValue() == Key.ESC) {
              World.getInstance("Gustav").setPause(true);
        } 

       if(key.getValue() == Key.UP) {
               World.getInstance("Peter").getPlayer().moveUp(true); 
       }

       if(key.getValue() == Key.DOWN) {
               World.getInstance("Lasmiranda").getPlayer().moveDown(true); 
       }
   }
}

public class Player {
   public move() {
      if(moveDown ^ moveUp) {
           // move ...
      }
   }
}

Baue erstmal eine Engine ohne viel "static" und dann kann man nochmal drüber reden, wo es Probleme gibt und wie man diese evtl. mit Patterns lösen kann ...
 
Hey, vielen Dank für deine Antwort Ruzmanz! 🙂

Den "Schönheitsfehler" mit der Eingabe des Spielers hab ich bereits korrigiert. Stattdessen habe ich eine statische Methode eingefügt, die einen Spieler hinzufügt. Du bist also der Meinung, dass die World-Klasse Knotenpunkt jeglicher Aktionen sein sollte oder wie meinst du das?

Dass der Handler die Informationen nicht braucht seh ich ein! Aber dein Beispiel zeigt ja gerade genau den Vorteil der Singleton, nämlich dass ich alle Methoden dieser Klasse statisch aufrufen kann, ohne mir Gedanken um eine Instanz zu machen. Ferner wollte ich mit der Singleton-World sicherstellen, dass im Hintergrund nicht mehrere Welten geladen werden, sondern tatsächlich nur eine gleichzeitig. Ich weiß nicht wie aufwändig die Generierung einer Welt sein wird, darum war mir das vorerst wichtig. Ist das kein guter Gedanke?

Außerdem möchte ich das Ganze mit zwei Hintergründen machen (vielleicht sind meine Ziele auch zu hoch...).

Erstens: Ich möchte später Spielstände speichern können.
Zweitens: Ich möchte die Möglichkeit haben, die Engine von textbasierend auf grafisch erweitern zu können.

Was meinst du dazu?

Liebe Grüße fara
 
Die Zeitschrift "iX DEVELOPER" widmet sich in der aktuellen Ausgabe 1/2015 ganz der Spiele-Entwicklung. Das ist recht interessant. Es wird zwar alles nur extremst oberflächlich behandelt, aber es liefert doch ein paar interessante Infos.

Ich persönlich fand den Beitrag zur künstlichen Intelligenz recht interessant. Aber gleich vorweg: was in dem Artikel beschrieben wird, hat noch überhaupt nichts mit KI zu tun, weil in keinster Weise darauf eingegangen wird, wie ein System lernen kann. Was aber an dem Artikel interessant ist, wie man ein Spiel modular aufbauen kann.

Hier wird scheinbar auf das Command-Pattern zurückgegriffen. Für alle möglichen Aktionen gibt es eigene Bausteine. Die können dann beliebig in einer Baumstruktur (im Artikel "Behavior Tree" genannt) angeordnet werden. Statt also das ganze Programm in einem einzigen Stück zu schreiben, wird es in Bausteine aufgeteilt. Dadurch wird das Programm recht flexibel, da man Bausteine einfach aus dem Baum entfernen und irgendwo hinzufügen kann.

Neben Aktions-Bausteinen gibt es auch Bausteine, die den Datenfluss steuern. So gibt es z. B. Sequenz-, Selektor- und Iterator-Bausteine. Es kann auch rekursiv gearbeitet werden. Die Bausteine arbeiten alle mit einer gemeinsamen Datenbasis. Das ist eine key-value-Datenbank, die im Artikel "Blackboard" genannt wird. Gibt es einen Baustein, um eine Figur irgendwo hinzubewegen, steht im Blackboard die Position, wo sie hinbewegt werden soll. Die Position ist der value, mittels eines eindeutigen keys wird dieser value ausgelesen.

Was ich diesem Artikel leider nicht entnehmen konnte ist, woher die Bausteine ihre keys kennen. Ich vermute, die müssen ihnen im Konstruktor übergeben werden. Die sind sicher auch wichtig, um für rekursive Aufrufe Abbruchbedingungen definieren zu können. Leider ist die ganze iX-Ausgabe fast nur reine Theorie. Programme sind absolute Mangelware. Zu dem Beitrag "KÜNSTLICHE INTELLIGENZ" gibt es kein einziges Programm.

iX DEVELOPER gibt es im Zeitschriften-Handel, kostet 12,90 € in Deutschland, 14,20 € in Österreich und 25,80 CHF in der Schweiz. Informationen zum Behavior-Tree gibt es auf Behave Documentation (habe ich mir aber noch nicht durchgelesen).
 
Zuletzt bearbeitet:
Hier ein Pseudocode, wie das vielleicht aussehen könnte:
Java:
interface Node
{
   const FAILURE = 0;
   const RUNNING = 1;
   const SUCCESS = 2;
   int tick();
}
abstract class Control implements Node
{
   protected Node[] childs;
}
abstract class Action implements Node
{
}
class Sequence extends Control
{
   private int i;
   Sequence(Node[] childs)
   {
      this.childs = childs;
      i = 0;
   }
   int tick()
   {
      if(childs.length > 0)
      {
         childs[i].tick();
         i = (i + 1) % childs.length;
      }
      return Node.SUCCESS;
   }
}
class RunToPosition extends Action
{
   private Set<Object> blackboard;
   private string key;
   RunToPosition(Set<Object> blackboard, string key)
   {
      this.blackboard = blackboard;
      this.key = key;
   }
   int tick()
   {
      int position = blackboard.get(key);
      System.out.println("Laufe zu Position " + position);
   }
}
class Main
{
   public static void main(String[] args)
   {
      Set<Object> blackboard = new Set<Object>();
      blackboard.put("position1", 1);
      
      RunToPosition runTo1 = new RunToPosition(blackboard, "position1");
      
      Sequence root = new Sequence({runTo1});
      root.tick();
   }
}
Bitte nicht als Java-Programm ansehen. Es hat sicher mindestens 1001 Fehler 😉
Mehr Zeit habe ich jetzt leider nicht.
 
Zuletzt bearbeitet:
@minzee: Wenn ich das also recht verstehe, basiert das Ganze darauf eine endliche Menge an Aktionen zu definieren und ein Event anhand einer Sequenz dieser Aktionen, die nacheinander abgearbeitet werden, zu realisieren?

Liebe Grüße fara
 
Im Interface habe ich es schon angedeutet. Bei jedem Durchlauf (tick) werden die childs gecheckt. Ein child kann FAILURE, RUNNING oder SUCCESS zurückliefern. In meiner Klasse RunToPosition fehlt dieser return noch.

- Ist ein Baustein noch mit einer Aktion beschäftigt, liefert er RUNNING zurück. Z. B. eine Figur bewegt sich noch in Richtung seiner Zielposition.

- Hat ein Baustein nichts zu tun, weil er vielleicht irgendeine bestimmte Aktion bereits erfolgreich beendet hat, gibt er SUCCESS zurück. Z. B. eine Figur hat eine bestimmte Zielposition eingenommen.

- Kann ein Baustein ein geforderte Aktion nicht ausführen, gibt er FAILURE zurück. Z. B. eine Figur soll eine bestimmte Zielposition einnehmen, kann es jedoch nicht, weil sich dort bereits eine andere Figur befindet.

Der Baustein "Sequenz" ist nun ein ganz bestimmtes Steuerungselement. Ihm können mehrere childs übergeben werden.

- Liefert irgendeines dieser childs ein FAILURE, werden die folgenden childs nicht mehr abgearbeitet und das Sequenz-Steuerelement gibt selbst FAILURE zurück.

- Liefert einer dieser childs RUNNING, werden die folgenden childs ebenfalls nicht mehr abgearbeitet und das Sequenz-Steuerelement gibt ebenfalls RUNNING zurück. Sofern dieses Steuerelement beim nächsten Durchlauf (tick) jedoch nicht mehr RUNNING, sondern dann SUCCESS zurückliefert, werden die folgenden childs abgearbeitet.

- Liefern die childs SUCCESS, wird gleich das nächste child abgearbeitet.

Neben dem Baustein Sequenz gibt es noch weitere Steuerknoten. Die haben dann eine etwas andere Aufgabe und zeigen im Umgang mit den Rückmeldungen der childs ein etwas anderes Verhalten.

Zumindest habe ich das so verstanden. Sofern das so korrekt ist, vermute ich, dass man hier mit Threads arbeiten muss. Denn ich wüsste nicht, wie ansonst ein child ein RUNNING zurückgeben könnte. Aber vielleicht wisst ihr das, wie man so etwas programmieren kann.
 
Zuletzt bearbeitet:
Hey,
ich glaube das geht gerade etwas über meine Programmierkenntnisse hinaus. Das ließe sich sicher irgendwann implementieren - wenn mehr Zeit zur Verfügung steht. Aber generell die Idee, die Aktionen einzeln zu betrachten und Events als Objekte darzustellen, die sequenziell Aktionen durchlaufen, ist an sich ziemlich gut...denke ich.

Beizeiten werde ich mich mal mit deinem Link beschäftigen. Ich hab ihn bereits in meinen Favourites.^^

Liebe Grüße
 

Zurück
Oben