Ulis Adventure Game Engine

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
ulrich1992
User
Beiträge: 42
Registriert: Montag 8. November 2010, 15:25
Wohnort: Braunschweig
Kontaktdaten:

Ulis Adventure Game Engine ist eine auf pygame basierende Engine für die Entwicklung von klassischen Point & Click Adventure Games a la Myst.
Es lassen sich damit jedoch nicht nur Adventure Games erstellen, sondern auch Spiele aus verwandten Genres wie z.B. Interaktive Filme, Wimmelbildspiele oder Escape-The-Room Games.
Außerdem lässt sich diese Engine für die Entwicklung von interaktiven Multimedia-Produkten wie z.B. Lernsoftware oder auch für öffentliche Info-Terminals nutzen.

Ulis Adventure Game Engine basiert auf Scenes. Scenes sind Standbilder oder auch Animationen mit klickbaren Bereichen (Hotspots).
Es kann ein Text definiert werden, der beim überfahren eines Klickbaren Bereichs mit der Maus angezeigt wird.
Dort kann z.B. die mögliche Aktion stehen.
Beim anklicken eines Hotspots wird eine Aktion ausgeführt.

UAGE bietet ein Dialogsystem, mit dem sich z.B. interaktive Interviews umsetzen lassen.
Ein einfaches Inventar ist ebenfalls vorhanden.

Eine Scene kann mit einigen Effekten wie z.B. animierter Nebel, Regen und Schnee versehen werden.

Eine neue Version mit zwei grafischen Tools zur Videokonvertierung ist bereits fast fertig.

Hier die Projektseite:
http://uligames.de/?seite=ulis_adventure_game_engine
mutetella
User
Beiträge: 1695
Registriert: Donnerstag 5. März 2009, 17:10
Kontaktdaten:

ulrich1992 hat geschrieben:Ulis Adventure Game Engine basiert auf Scenes.
Hab' mir interessehalber Deine `scene.py` angeschaut (die vielen, vielen Leerzeilen machen das etwas mühsam... :wink: ) und verstehe folgendes nicht:
  • Code: Alles auswählen

    from __builtin__ import False
    Welchen Sinn hat das?
  • Code: Alles auswählen

    class Scene:
        ...
        def initRain(self, intensity = 900 ):
        ...
        def initSnow(self, intensity = 100 ):
        ...
        def initConfig(self):
        ...
        def initScreen(self):
        ...
        def __init__(self, game_path, scene_dir, scene_filename):
        ...
        def initFog(self):
        ...
    Hat dieses Durcheinander historische Gründe? Eigentlich wollte ich fragen, weshalb Deine `Scene`-Klasse keine `__init__`-Methode, dafür aber jede Menge `init..`-Methoden hat. Hab' sie dann aber doch noch gefunden... :wink:
  • Code: Alles auswählen

    f = open(savegame_file, "r")
    json_string = f.read()
    f.close()
    obj = json.loads(json_string)
    Sowas findet sich öfters. Weshalb verwendest Du nicht `json.load`? Und warum verzichtest Du auf das ``with``-statement?
Boah... ist echt schwer, Deinen Code zu lesen! Riesige Methoden, endlose ``if...elif...`` Konstrukte! Kommst Du damit noch klar?

mutetella
Entspanne dich und wisse, dass es Zeit für alles gibt. (YogiTea Teebeutel Weisheit ;-) )
ulrich1992
User
Beiträge: 42
Registriert: Montag 8. November 2010, 15:25
Wohnort: Braunschweig
Kontaktdaten:

mutetella hat geschrieben:
ulrich1992 hat geschrieben:Ulis Adventure Game Engine basiert auf Scenes.
Hab' mir interessehalber Deine `scene.py` angeschaut (die vielen, vielen Leerzeilen machen das etwas mühsam... :wink: ) und verstehe folgendes nicht:
Das ist in der Tat ziemlich unnötig.
Da hat mir wohl PyDev einen Streich gespielt.
Ich kann mich jedenfalls nicht daran erinnern, das selber geschrieben zu haben.
mutetella hat geschrieben: [*]

Code: Alles auswählen

class Scene:
    ...
    def initRain(self, intensity = 900 ):
    ...
    def initSnow(self, intensity = 100 ):
    ...
    def initConfig(self):
    ...
    def initScreen(self):
    ...
    def __init__(self, game_path, scene_dir, scene_filename):
    ...
    def initFog(self):
    ...
Hat dieses Durcheinander historische Gründe? Eigentlich wollte ich fragen, weshalb Deine `Scene`-Klasse keine `__init__`-Methode, dafür aber jede Menge `init..`-Methoden hat. Hab' sie dann aber doch noch gefunden... :wink:
Das hat durchaus einen Sinn:
Die Klasse Scene repräsentiert immer die aktuelle Szene.
Sie ist quasi das Herzstück der Engine.
Die Funktion __init__ wird nur beim Start! des Spiels aufgerufen. (siehe playgame.py)
Die Methoden initFog, initRain usw. werden in der loadScene Funktion nur aufgerufen, wenn die zu ladende Szene auch diesen Effekt enthält.
Es gibt unterschiedliche Parameter für die Effekte (Regenstärke z.B. ), deshalb werden die Effekte in jeder Szene neu initialisiert.
mutetella hat geschrieben: [*]

Code: Alles auswählen

f = open(savegame_file, "r")
json_string = f.read()
f.close()
obj = json.loads(json_string)
Sowas findet sich öfters. Weshalb verwendest Du nicht `json.load`? Und warum verzichtest Du auf das ``with``-statement?[/list]
Stimmt eigentlich.
json.load wäre kürzer.
mutetella hat geschrieben: Boah... ist echt schwer, Deinen Code zu lesen! Riesige Methoden, endlose ``if...elif...`` Konstrukte! Kommst Du damit noch klar?
mutetella
Gibt es denn in Python eine elegantere Methode, eine Textdatei zeilenweise zu parsen?
Zuletzt geändert von ulrich1992 am Freitag 21. März 2014, 09:55, insgesamt 1-mal geändert.
mutetella
User
Beiträge: 1695
Registriert: Donnerstag 5. März 2009, 17:10
Kontaktdaten:

ulrich1992 hat geschrieben:Das hat durchaus einen Sinn: ...
Ich meinte damit erstmal, dass Du Dich an sinnvolle Konventionen halten solltest. Und dazu gehört unter anderem, dass eine `__init__`-Methode die erste Methode einer Klasse sein sollte. Wer Deinen Code liest, erwartet das so. Genauso sind auch die vielen Leerzeilen fürchterlich für den, der sich da durchscrollen muss. Nach einer Funktion/Methode eine Leerzeile, vielleicht noch zwischen Klassen zwei.
ulrich1992 hat geschrieben:Gibt es denn in Python eine elegantere Methode, eine Textdatei zeilenweise zu parsen?
Die Frage ist erstmal: Weshalb kümmert sich eine Klasse `scene` um das Parsen? Wenn ich eine Funktion/Methode schreibe, die ich ohne zu scrollen nicht mehr vollständig lesen kann, kann ich fast immer davon ausgehen, dass ich Dinge zusammengefasst habe, die nicht zusammen gehören.

mutetella
Entspanne dich und wisse, dass es Zeit für alles gibt. (YogiTea Teebeutel Weisheit ;-) )
ulrich1992
User
Beiträge: 42
Registriert: Montag 8. November 2010, 15:25
Wohnort: Braunschweig
Kontaktdaten:

mutetella hat geschrieben:
ulrich1992 hat geschrieben:Das hat durchaus einen Sinn: ...
Ich meinte damit erstmal, dass Du Dich an sinnvolle Konventionen halten solltest. Und dazu gehört unter anderem, dass eine `__init__`-Methode die erste Methode einer Klasse sein sollte. Wer Deinen Code liest, erwartet das so. Genauso sind auch die vielen Leerzeilen fürchterlich für den, der sich da durchscrollen muss. Nach einer Funktion/Methode eine Leerzeile, vielleicht noch zwischen Klassen zwei.
Habe jetzt die Leerzeilen in der Version auf GitHub entfernt.
mutetella hat geschrieben:Gibt es denn in Python eine elegantere Methode, eine Textdatei zeilenweise zu parsen?Die Frage ist erstmal: Weshalb kümmert sich eine Klasse `scene` um das Parsen? Wenn ich eine Funktion/Methode schreibe, die ich ohne zu scrollen nicht mehr vollständig lesen kann, kann ich fast immer davon ausgehen, dass ich Dinge zusammengefasst habe, die nicht zusammen gehören.mutetella
Ich könnte eine klasse "Parser" machen und dieser den Dateinamen der zu ladenden Skriptdatei und das self von der scene-Klasse als parent mitgeben beim Instanzieren.
Die Funktion loadScene und alles was dazu gehört, würde ich dann in die Parser-Klasse verschieben.
Meinst du das so?
mutetella
User
Beiträge: 1695
Registriert: Donnerstag 5. März 2009, 17:10
Kontaktdaten:

ulrich1992 hat geschrieben:Ich könnte eine klasse "Parser" machen und dieser den Dateinamen der zu ladenden Skriptdatei ...
Nicht alles muss in eine Klasse. Ein savegame oder eine Konfiguration oder ähnliches einzulesen und zu parsen ist in der Regel ein einmaliger Vorgang. Eine Klasse, die instanziiert wird, nur um einen Parsingprozess anzustossen, macht kaum Sinn. Ich würde dazu schlichtweg ein Modul mit den nötigen Funktionen verwenden.
ulrich1992 hat geschrieben:... und das self von der scene-Klasse als parent mitgeben beim Instanzieren.
Das `scene`-Exemplar (Instanz) benötigt doch überhaupt keine Kenntnis der Parserfunktion und umgekehrt. Überlege Dir, welche Datenstruktur Deine Parsingfunktion sinnvollerweise zurückgeben sollte und übergebe diese dann der `scene`-Klasse.

mutetella
Entspanne dich und wisse, dass es Zeit für alles gibt. (YogiTea Teebeutel Weisheit ;-) )
ulrich1992
User
Beiträge: 42
Registriert: Montag 8. November 2010, 15:25
Wohnort: Braunschweig
Kontaktdaten:

mutetella hat geschrieben:
ulrich1992 hat geschrieben:Ich könnte eine klasse "Parser" machen und dieser den Dateinamen der zu ladenden Skriptdatei ...
Nicht alles muss in eine Klasse. Ein savegame oder eine Konfiguration oder ähnliches einzulesen und zu parsen ist in der Regel ein einmaliger Vorgang. Eine Klasse, die instanziiert wird, nur um einen Parsingprozess anzustossen, macht kaum Sinn. Ich würde dazu schlichtweg ein Modul mit den nötigen Funktionen verwenden.
ulrich1992 hat geschrieben:... und das self von der scene-Klasse als parent mitgeben beim Instanzieren.
Das `scene`-Exemplar (Instanz) benötigt doch überhaupt keine Kenntnis der Parserfunktion und umgekehrt. Überlege Dir, welche Datenstruktur Deine Parsingfunktion sinnvollerweise zurückgeben sollte und übergebe diese dann der `scene`-Klasse.
mutetella
Das Problem ist, dass meine loadScene Funktion auf diverse Variablen der scene-Klasse zugreift.
Ich könnte das jetzt zwar so umschreiben, dass die Parser-Funktion ein Dictionary zurückgibt, und ich dann die Werte aus dem Dictionary alle einzeln in die entsprechenden Variablen schreibe, jedoch frage ich mich, wo da der Mehrwert gegenüber der Methode, wo ich die Variablen direkt in der Parser klasse setze ist, und ob sich der Aufwand das umzuschreiben lohnt.
BlackJack

Kleiner Zwischenruf bezüglich der Konventionen was Leerzeichen, -zeilen, und Namensschreibweisen angeht: Style Guide for Python Code.

Und Deine `Scene` ist ziemlich deutlich ein Gottobjekt. (Ob Du davon noch mehr hast weiss ich nicht, ich hatte nur in die Datei geschaut.)

Es scheinen auch Ausnahmen vermieden zu werden in dem an vielen Stellen ein spezieller Fehlerwert oder True/False als Rückgabewerte verwendet werden. Also die Praxis die man mit Ausnahmen eigentlich loswerden möchte, wird hier wieder eingeführt.
ulrich1992
User
Beiträge: 42
Registriert: Montag 8. November 2010, 15:25
Wohnort: Braunschweig
Kontaktdaten:

BlackJack hat geschrieben:Kleiner Zwischenruf bezüglich der Konventionen was Leerzeichen, -zeilen, und Namensschreibweisen angeht: Style Guide for Python Code.

Und Deine `Scene` ist ziemlich deutlich ein Gottobjekt. (Ob Du davon noch mehr hast weiss ich nicht, ich hatte nur in die Datei geschaut.)
Ich könnte z.B. ein package "effects" machen und darin die Funktion drawFog, drawRain usw. auslagern.
Und ich könnte ein Klasse für inventar-Objekte machen, statt Dictionaries dafür zu nutzen.
Da meine Inventarobjekte lediglich eine Grafik und einen Namen haben, habe ich mir halt gedacht:
Für zwei Attribute brauche ich keine eigene Klasse.

Das Dialogsystem, die Funktion zur Wiedergabe von Cutscenes, die Funktion zur Ausgabe von Sprachdatei, die Videokonvertiertools und noch einige Helper-Funktionen sind alle recht kleinen Klassen.
BlackJack hat geschrieben: Es scheinen auch Ausnahmen vermieden zu werden in dem an vielen Stellen ein spezieller Fehlerwert oder True/False als Rückgabewerte verwendet werden. Also die Praxis die man mit Ausnahmen eigentlich loswerden möchte, wird hier wieder eingeführt.
Wenn ich z.B. eine Funktion habe, die ein Bild laden soll, und dieses Bild existiert nicht, dann soll ich statt einen Fehler im Terminal zu loggen und None zurückzugeben eine Exception werfen?

Code: Alles auswählen

raise Exception("Grafikdatei " + filename + " konnte nicht geladen werden")
Zuletzt geändert von ulrich1992 am Freitag 21. März 2014, 10:34, insgesamt 1-mal geändert.
mutetella
User
Beiträge: 1695
Registriert: Donnerstag 5. März 2009, 17:10
Kontaktdaten:

@ulrich1992
Der Mehrwert liegt darin, dass Du am Ende einen robusteren Code hast. Du verwebst zwei völlig unterschiedliche Dinge wie das Repräsentieren einer Spielszene auf der einen und das Parsen einer savegame-Datei auf der anderen Seite miteinander. Irgendwann wirst Du ein Codemonster haben, das niemals mehr berührt werden darf, weil es dadurch, dass sich alles irgendwie gegenseitig bedingt und stützt super fragil geworden ist. Zudem kann Deinen Code heute schon kaum jemand lesen, weil die Dinge, die zusammengehören, nicht zusammen sind. Und wenn Du einmal ein paar Wochen nicht mehr an Deiner `scene.py` gearbeitet hast, wirst Du nur mit viel Mühe wieder reinfinden.

mutetella
Entspanne dich und wisse, dass es Zeit für alles gibt. (YogiTea Teebeutel Weisheit ;-) )
mutetella
User
Beiträge: 1695
Registriert: Donnerstag 5. März 2009, 17:10
Kontaktdaten:

ulrich1992 hat geschrieben:Wenn ich z.B. eine Funktion habe, die ein Bild laden soll, und dieses Bild existiert nicht, dann soll ich statt einen Fehler im Terminal zu loggen und None zurückzugeben eine Exception werfen?
Welche andere Möglichkeit hast Du denn sonst? Du hast Dir ja zum Ziel gesetzt, eine Engine/ein Framework zu schreiben. Das richtet sich ja wiederum an Leute, die damit etwas entwickeln. Wenn also z. B. etwas geladen werden soll, das nicht existiert, dann erwarte ich von einem Framework erstmal nicht, dass es auf magische Weise diesen Fehler irgendwie ausmerzt, sondern dass es mich mit einer Fehlermeldung darauf aufmerksam macht, damit ich selbst diesen Fehler adäquat behandeln kann.
Entspanne dich und wisse, dass es Zeit für alles gibt. (YogiTea Teebeutel Weisheit ;-) )
BlackJack

Die `Scene`-Klasse kann man sicher auch noch in eine Klasse aufteilen die tatsächlich eine Szene repräsentiert und eine die das Spiel repräsentiert und den Zustand speichert der über die aktuelle Szene hinaus geht.

Die ganzen Pfade und der Bilder-Cache wären Kandidaten für mindestens eine Klasse die für das laden und ggf. Cachen von Ressourcen zuständig ist.

Eigentlich braucht man in den meisten Fällen gar nichts zu machen damit eine Ausnahme ausgelöst wird. Denn beim laden einer nicht vorhandenen Bilddatei wird automatisch eine Ausnahme ausgelöst.

Bei anderen Lademethoden könnte man sich auch überlegen ob man es nicht so einrichten kann, dass sie *immer* einen passenden Rückgabewert liefern. Beim laden von gespeicherten Spielständen könnte man zum Beispiel wenn die Spielstandsdatei nicht vorhanden ist, einfach einen leeren Spielstand zurück geben. Denn soweit ich das sehe werden die ja dem Spieler als ”Slots” präsentiert, die entweder einen Spielstand enthalten oder leer sind.

Die Spielstandsverwaltung könnte auch in eine eigene Klasse.

Das Inventory als Liste zu halten scheint unnötig umständlich, denn die Gegenstände bestehen aus Name und Bild und der Name muss eindeutig sein. Damit wäre ein Wörterbuch das Name auf Bild abbildet einfacher als zum Beispiel immer linear nach den Namen in dieser Liste zu suchen.

Die `readLines()`-Methode ist keine Methode sondern eigentlich eine Funktion. Die Datei die dort geöffnet wird, wird nicht explizit wieder geschlossen. Wenn man nach einem `rstrip('\n')` auf das Ergebnis `strip()` anwendet, dann ist der erste Aufruf überflüssig, denn das `strip()` entfernt auch '\n' am Ende.

Beim parsen sollte man sich fragen ob man entweder ein bereits vorhandenes Format verwendet, zum Beispiel JSON, oder einen ordentlichen Parser für eine richtige Sprache schreibt. Wobei ich da dann auch in Frage stellen würde, ob das wirklich sein muss eine eigene Sprache zu erfinden, wo es doch Python gibt. Das gilt für das laden von Szenen und für das Ausführen von Skripten mit `executeScriptLines()`. Letztendlich ist das nicht nur ein Fall für eine Parser-Klasse sondern schreit auch nach einem Interpreter als eigenen Datentyp, wenn man denn wie schon gesagt, unbedingt eine eigene Skriptsprache erfinden muss.

Zum Wert diese Gottklasse aufzubrechen: Du hast da 1000 Zeilen die *ein* Objekt beschreiben, mit 44 Methoden, und 25 Attributen die irgendwo in diesen Methoden und nicht in der `__init__()` definiert sind, insgesamt über 60 Attribute. Das kann kein Mensch mehr vernünftig überblicken.
ulrich1992
User
Beiträge: 42
Registriert: Montag 8. November 2010, 15:25
Wohnort: Braunschweig
Kontaktdaten:

BlackJack hat geschrieben: Das Inventory als Liste zu halten scheint unnötig umständlich, denn die Gegenstände bestehen aus Name und Bild und der Name muss eindeutig sein. Damit wäre ein Wörterbuch das Name auf Bild abbildet einfacher als zum Beispiel immer linear nach den Namen in dieser Liste zu suchen.
Die Items sollen aber in der Reihenfolge in der Sie aufgenommen wurden im Inventar erscheinen.
Das ist mit einem Dict nicht möglich, da ein Dict keine Reihenfolge hat.
Deshalb eine Liste.
BlackJack

@ulrich1992: Dann halt `collections.OrderedDict`.
ulrich1992
User
Beiträge: 42
Registriert: Montag 8. November 2010, 15:25
Wohnort: Braunschweig
Kontaktdaten:

BlackJack hat geschrieben:@ulrich1992: Dann halt `collections.OrderedDict`.
ein OrderedDict kannte ich noch nicht.

Also heißt das, mehr oder weniger alles löschen und nochmal komplett neu anfangen?
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Hallo.
ulrich1992 hat geschrieben:Also heißt das, mehr oder weniger alles löschen und nochmal komplett neu anfangen?
Das würde ich so generell nicht sagen, Refactoring ist auch eine wichtige Arbeit. An deiner Stelle würde ich, da es schon realtiv viel Code ist, überlegen, wass du mit dem Programm machen willst. Wenn alles stabil läuft und du "nur" ein Spiel entwickeln möchtest, dann würde ich den Code so belassen und den zusätzlichen Code sauberer schreiben. Das bedeutet natürlich auch, dass dein alter Code möglicherweise immer undurchdringlicher wird, da du hier und dort kleine Hacks einbauen musst und der Code sich zieht. Als Konsequenz bedeutet das, dass du deinen Code dann vielleicht nicht wiederverwenden kannst.

Wenn es dir um das Entwickeln der Engine selbst geht und nicht um ein Spiel, dann würde ich in diesem Stadium noch über eine Neuentwicklung nachdenken. Bzw. über eine ordentliche Aufräumaktion. Dazu aber vielleicht eine kleine Warnung: ich kenne jede Menge Leute, welche ständig so iteriert haben. Irgendwas gefielt nicht, neu schreiben, viel Arbeit reinstecken. Irgendwann kommt wieder der Punkt: ach, das ist aber unschön. Also wieder neu schreibe, weitere zusätzliche Arbeit, etc. Bei vielen scheitern solche Projekte dann.

Vielleicht lernst du auch mehr, wenn du mit deinem jetzigen Ansatz einfach mal weitermachst und etwas mehr Komplexität einbaust. Dabei wirst du viel lernen, da du Fehler selber machen kannst und vielleicht die Auswirkungen von Fehlentscheidungen beim Design "hautnah" erlebst. Das ist nichts Schlechtest, da muss jeder durch. Beim nächsten Programm weißt du es dann besser. Irgendwann erreichst du dann den Punkt, an dem es nicht mehr weiter geht und dann kannst du noch immer darüber nachdenken, was du als nächstes machst. Oder du erreichst ihn nicht, weil dich irgendwann etwas anderes interessiert.

Noch eine kleine Anmerkung: Python besitzt ein logging-Modul, das ist komfortabler als print-Statements.
Das Leben ist wie ein Tennisball.
Antworten