Wie strukturiert man ein Spiel?

Hier werden alle anderen GUI-Toolkits sowie Spezial-Toolkits wie Spiele-Engines behandelt.
Antworten
lukcod3
User
Beiträge: 1
Registriert: Montag 31. Juli 2017, 23:30

Guten Tag :) ,
nach einem Jahr Programmieren in Python versuche ich gerade mein erstes Spiel in Python zusammen zu schustern. Genauer gesagt geht es um das Kartenspiel 'Bonanza' der Firma 'Amigo Spiele'. Als Endprodukt stelle ich mir vor, dass man (erstmal) gegen eine definierbare Anzahl von AIs spielt.
Da es mir nur um die ungefähre Strukturierung eines solchen Kartenspieles geht, verzichte ich hier auf eine genauere Beschreibung des Spiels.

Meine jetzige Struktur:
Es gibt einen Input-Manager, der 'Input' einholt, einen Output-Manager, der jeglichen 'Output' verwaltet (auch die Aufforderung zu einer Eingabe seitens des Spielers) und die Logik.
Ich habe die Manager eingeführt, um mir den späteren Umstieg von der Konsole zu einer GUI mithilfe von pygame, zu erleichtern.
Rückgabe-Werte von Funktionen, die auch Fehler, wie beispielsweise eine nicht gültige Eingabe des Nutzers, zurückgeben können, werden als Instanzen der Klassen 'Output' und 'Error' zurückgegeben.
Die Klassen sehen ungefähr so aus:

Code: Alles auswählen

class Output:
	def __init__(self, output):
		self.output = output
		self.type = 'output'
	
	def get_output(self):
		return self.output
	
	def get_type():
		return self.type


class Error(Output):
	def __init__(self, error):
		Output.__init__(self, error)
		self.type = 'error'

Dieses System erachte ich allerdings nicht mehr als sinnvoll, sondern eher als sperrig und werde es wahrscheinlich abschaffen.

Soweit die kurze Beschreibung meines jetzigen Systems.

Ist eine solche Gliederung sinnvoll oder gibt es andere gängige Strukturen, die ich bei meinem Programm einführen könnte?
Ich bin für jegliche Art der Kritik dankbar.

Mit freundlichen Grüßen
lukcod3
Zuletzt geändert von Anonymous am Dienstag 1. August 2017, 00:05, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
BlackJack

@lukcod3: Das sieht in der Tat sperrig aus und so gar nicht nach Python und auch nicht nach guter objektorientierter Programmierung (OOP). Womit hast Du denn Python gelernt? Oder hast Du vielleicht vorher was mit Java gemacht?

Der Namenszusatz `Manager` ist so ein Javaismus der oft genommen wird wenn man nicht so genau beschreiben kann was die Klasse eigentlich macht. Oder es ist ein unnötiger Zusatz. Denn das eine Klasse ihren Zustand verwaltet ist auch ohne den Namenszusatz „Verwalter“ (Manager) klar. Das machen Klassen halt.

Triviale Getter und Setter schreibt man in Python nicht. Und einen Typ haben Klassen doch sowieso von Haus aus schon, das muss man nicht noch einmal als Attribut haben. Du wiederholst da ja nur den Klassennamen in leicht abgewandelter Schreibweise. Ein Typtest mit `isinstance()` ist dann auch gleich mächtiger, weil das auch die Vererbung mit berücksichtigt wird. Allerdings ist das prüfen auf einen Typ in OOP ein „code smell“, denn eigentlich sollte der Typ selbst entsprechendes Verhalten haben und nicht von aussen auf den Typ geprüft werden um dann das Verhalten anhand des Typs zu wählen. Andererseits gibt es `functools.singledispatch()` (oder den Backport für Python 2).

Von Deinem Codebeispiel würde in „pythonischem“ Python das hier übrig bleiben:

Code: Alles auswählen

class Output(object):
    def __init__(self, output):
        self.output = output

 
class Error(Output):
    pass
Benutzereingaben sollte man im GUI-Code prüfen bevor er in die Programmlogik gelangt. Gegebenfalls mit entsprechenden Prüffunktionen/-methoden in der Programmlogik. Oder die Programmlogik löst Ausnahmen aus um falsche Eingaben oder Spielzustände an den GUI-Code zurück zu melden. Ausnahmen wurden erfunden um spezielle Fehlerrückgabewerte los zu werden. (GUI-Code meint hier allgemein Benutzerinteraktion, also auch wenn es eigentlich eine Text-UI ist.)

Aus der funktionalen Ecke hat sich zwar in einigen Programmiersprachen ein Ergebnistyp zu den Werkzeugen gesellt der ein Ergebnis oder eine Ausnahme kapselt, aber Python hat diese Welle offenbar noch nicht erreicht. Obwohl… `concurrent.futures.Future` ist ja so etwas. :-)
Astorek
User
Beiträge: 72
Registriert: Samstag 24. Januar 2009, 15:06
Kontaktdaten:

Ich für meinen Teil bin da ein großer Fan des Event-Systems, das mir u.a. mal von Blackjack hier im Forum gezeigt wurde^^. Vorneweg muss ich aber immer dringend erwähnen: Das ist eine Methode, die sich für mich bewährt hat. Ich habe keine Ahnung, wie "pythonisch" oder überhaupt sauber meine Methode ist, oder ob ich mir ggf. auch einfach unnötig viel Arbeit aufhalse^^.

Das Grundprinzip läuft bei mir eigentlich immer so ab: Eine "Spiel"-Klasse kümmert sich um die Verwaltung und die Logikteile. Entsprechende Methoden ändern Spielelemente und - ganz wichtig - werfen dann auch Events ab, wenn sich etwas ändert.

Dann programmiere ich eine primitive CMD-/Kommandozeilen-Oberfläche, in der ich alle möglichen Logikteile auf Herz und Nieren teste. Wobei "programmieren" nicht das richtige Wort dafür ist; das schludere ich mehr oder weniger so hin. Einfach nur, weil dieser Quelltext-Teil später nie produktiv genutzt wird; Hauptsache ist, dass ich in besagter CMD-Oberfläche dann das eigentliche "Spiel"-Objekt erstelle und deren Events dann an eigene Methoden binde, um die GUI dann entsprechend anzupassen.

Passt alles, schmeiße ich die CMD-Oberfläche raus und implementiere alles in einer passenderen Oberfläche (je nachdem welches GUI-Toolkit ich dann haben will, z.B. Tkinter oder Kivy). Der Punkt ist: Es hat den Vorteil, dass man auftretende Fehler dann (in der CMD-Oberfläche) darauf abklopfen kann, ob es sich um einen Logik- oder um einen Darstellungsbug handelt. Ich für meinen Teil empfinde es als große Erleichterung (gerade bei halbwegs komplexen GUIs, die es in Spielen fast immer gibt), Logikbugs dank der CMD-Oberfläche komplett ausschließen zu können.

Ich habe hier ein Programmierbuch über Spieleprogrammierung rumliegen, dass das Ganze in einer (für mich) sehr heftigen Art und Weise durchzieht; dort werden Einzelklassen von z.B. der Spiellogik selbst, Input, Grafikdarstellung, Sound und sogar das verwendete GUI-Toolkit einzeln gekapselt und dann in einer "Manager"-ähnlichen Klasse zusammengestöpselt. Das Eventsystem verwendet der Autor in einer Singleton-Klasse und ist auch recht umfangreich aufgebaut, z.B. mit der Möglichkeit, Events mitzuteilen, dass bei Änderungen an anderen Events wieder neue Events spawnen sollen. Für kleinere Projekte ist das natürlich kompletter Overkill... Aber die Auseinandersetzung mit dem Thema hilft, das Prinzip dahinter zu verstehen und plötzlich kommen einem selbst "große" und komplexe Programme/Spiele garnicht mehr so "magisch" vor. (TL;DR, ich weiß^^)

---

Als (nur exemplarisches) Beispiel: Die Event-Klasse:

Code: Alles auswählen

class Event(object):
    def __init__(self):
        self.listener = list()
        
    def register(self, listener):
        self.listener.append(listener)
        
    def unregister(self, listener):
        if listener in self.listener:
            self.listener.remove(listener)
            
    def notify(self, *args, **kwargs):
        for listener in self.listener:
            listener(*args, **kwargs)
Jetzt die Klasse, die für die Spiellogik verantwortlich ist. Das Spiel selbst ist simpel: Der Spieler muss einen Zähler einfach auf den Wert 3 bringen, dann hat er gewonnen:

Code: Alles auswählen

class Game(object):
    def __init__(self):
        
        # Events bereitstellen
        self.event_raise = Event()  # Wenn Variable erhoeht wurde
        self.event_playerwin = Event()  # Wenn das Spiel gewonnen wurde
        
        # Spielvariable: Ziel ist es, diese auf 2 zu bringen
        self.gamevalue = 0
        
    def player_move(self):
        self.gamevalue += 1
        self.event_raise.notify(self.gamevalue)
        
        if self.gamevalue == 3:
            self.event_playerwin.notify()
Faustregel (auch wieder von Blackjack übernommen): Die Spiellogik muss garnicht wissen, was hinterher mit dem Spiel gemacht wird. Es meldet einfach nur brav, ob sich interne Daten in der Spiellogik verändert haben und schickt ggf. noch mit, welche neuen Daten das sind (etwa bei "self.event_raise.notify").

In diesem Beispiel habe ich die "Spiellogik" (hier: Bei Zahl 3 gewinnt der Spieler) direkt in die Klasse eingebaut. Hier gäbs natürlich noch viele andere Möglichkeiten das zu implementieren (z.B. auch in einer anderen Klasse)... Damit das Ganze nicht ausartet (Stichwort Overengineering), lass ich das mal direkt in der Methode so stehen^^.

Jetzt die Klasse, die für die "Oberfläche" (hier: CMD-Fenster) verantwortlich ist:

Code: Alles auswählen

class CmdViewer(object):
    def __init__(self):
        # Spiel-Objekt erstellen
        self.game = Game()
        
        # Events des Spiel-Objekts an eigene Methoden binden:
        self.game.event_raise.register(self.MDL_raise)
        self.game.event_playerwin.register(self.MDL_playerwin)
        
    def run(self):
        # Jetzt in einer Dauerschleife den Spieler um Input bitten:
        while True:
            userinp = input("Soll die Zahl erhoeht werden? [jn]")
            
            # Benutzer will Zahl erhoehen? Dann dem Modell melden:
            if userinp == "j" or userinp == "J":
                self.game.player_move()
            else:
                exit()
                
    def MDL_raise(self, value):
        """Modell meldet, die Zahl wurde erhoeht."""
        print("Die Zahl wurde erhoeht und hat jetzt den Wert {}".format(value))
        
    def MDL_playerwin(self):
        """Modell meldet, der Spieler hat gewonnen."""
        print("GLUECKWUNSCH! Du hast das schwerste Spiel ever gewonnen! ;)")


if __name__ == "__main__":
    CmdViewer().run()
(Python 2-Nutzer müssen "input" durch "raw_input" ersetzen). Die Kommentare sollten eigentlich alles erklären^^.

Das Ganze müsste für ein Kartenspiel natürlich noch passend und umfangreicher gestaltet werden, aber das Grundprinzip sollte man, denke ich, verstanden haben^^. Auf dieser Grundlage habe ich immerhin Sokoban und Minesweeper schreiben können^^.

Hoffe, ich konnte irgendwie helfen. Alternativ: Hoffe, mir ist noch zu helfen *g* , falls ich z.B. einen extrem umständlichen Weg gegangen bin. Das überlasse ich den Profis hier, die mit Python ihren Lebensunterhalt verdienen^^...
Antworten