OOP-RPG: Spielstand abspeichern (pickle/shelve vs. ORM)

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
nunatak
User
Beiträge: 17
Registriert: Donnerstag 3. März 2011, 19:42

Hallo,

ich versuche mich mittels eines kleinen RPG ein bisschen mit dem OOP Ansatz vertraut zu machen. Hierbei will ich den Spielstand natürlich auch abspeichern und zu einem späteren Zeitpunkt fortfahren können. Um auch ein kleines Übungsprogramm so anzulegen, dass es theoretisch auch wachsen kann, suche ich nach der besten Variante. Bisher habe ich bei meinen Projekten (Datenbankprogrammierung und Datenprozessierung) immer entweder direkt in der Datenbank oder einfache Textfiles gespeichert. Bei vielen Objekten die Instanzen meiner Klassen (Player, NPC, Weapons, ...) repräsentieren, kam mir auf Anhieb eigentlich pickle/shelve als der beste Ansatz vor. Hierbei stellte sich aber schnell die Frage: Wie in einer einfachen Funktion wirklich ALLE Objekte und Variablen des Spiels speichern? In einem ersten Versuch mit shelve waren zudem die einfachen Variablen wieder verfügbar, Instanzen einiger Monster und des Player allerdings nicht.

Auf Nachfrage wurde mir im IRC gesagt, pickle sei dafür ohnehin nicht wirklich geeignet, da zu langsam, ich solle lieber ORM verwenden. Also lese ich mich gerade etwas in SQLAlchemy ein.

Was meint ihr, ist der beste Ansatz dafür?
BlackJack

@nunatak: Ich würde sagen weder noch und *zu langsam* sollte eigentlich keine Rolle spielen. Oder wie viele zehntausende Objekte hast Du da‽

Wenn eine Monster- und Spieler-Objekte nach dem deserialisieren nicht mehr da waren, dann hast Du sie aus irgendwelchen Gründen nicht gespeichert.

`pickle` hat das Problem das man sich entweder auf Grunddatentypen beschränkt oder man das Problem bekommt, das man nicht mehr so frei ist Klassen zu ändern oder zu verschieben, weil dann die gespeicherten Daten nicht mehr geladen werden können.

Und warum ein ORM und eine relationale Datenbank sinnvoll sein soll, erschliesst sich mir jetzt auch nicht. Das macht eigentlich nur Sinn wenn die Datenmengen so gross werden, dass man sie nicht immer komplett im Speicher haben möchte und man Teile des Spiels separat sichern und laden möchte.

Vergiss das mit der einfachen Funktion. Die gibt's bei einer ORM-Lösung ja auch schon nicht wirklich, auch da muss man die Attribute die gespeichert werden sollen alle speziell definieren.

Ich würde JSON, YAML, oder XML zum speichern verwenden. Und dazu jeder Klasse deren Objekte man (de)serialialisieren können möchte, entsprechende Methoden verpassen. Bei JSON/YAML beispielsweise eine `as_dict()`-Methode und eine `from_dict()`-Klassenmethode die das entsprechende Objekt aus einem Wörterbuch mit passenden Werten erstellt.
nunatak
User
Beiträge: 17
Registriert: Donnerstag 3. März 2011, 19:42

Danke für die Tipps. Auf die Idee mit der entsprechenden Methode bin ich auch vorhin gekommen. Werde ich versuchen. Und wie gebe ich der Methode den vergebenen Variablennamen mit (meist dynamisch erzeugt) unter dem das Objekt gespeichert und hinterher wieder hergestellt werden soll?
BlackJack

@nunatak: Gar nicht‽ Wozu? Und wie erzeugst Du dynamische Variablennamen?
nunatak
User
Beiträge: 17
Registriert: Donnerstag 3. März 2011, 19:42

Bzw. Namen von Instanzen, Objekten.

Zum Testen habe ich das gestern bei ein paar Orcs z.B. so gemacht:

Code: Alles auswählen

for x in range(random.randint(1, 5)):
	g.NPCs['orc{}'.format(x)] = NPC()
g. war in diesem Fall, ebenfalls aus Testzwecken eine leere, globale Containerklasse um darin meine Objekte zu storen. Das hatte ich noch den ANsatz verfolgt, alle Objekte in einem Dictionary zu sammeln um sie dann in einer Schleife aus diesem Dictionary heraus mit shelve speichern zu können.

Ansonsten hatte ich auch schon dynamischen Code als String erzeugt und mit exec() ausgeführt. Da ich ja nicht weiß wie viele NPCs oder andere Objekte ich habe, muss ich deren Instanznamen/-nummern doch dynamisch erzeugen. Variablenname war vielleicht der falsche Begriff dafür. Wie gesagt, bin mit der OOP noch nicht sehr vertraut.

So wie ich das jetzt verstehe, muss ich von jedem Objekt den Instanznamen (also hier orc0, orc1, orc2,...) zusammen mit dem Objekt abspeichern (ob in XML, JSON oder wie auch immer) um das dann zusammen wieder herzustellen. Die dann alle in einer Liste speichern und wenn der Spielstand gespeichert wird jedes Objekt mit dump-Methode aufrufen.

Zusätzlich wäre vielleicht ein Autosave sinnvoll. Um dann nicht immer alle Objekte speichern zu müssen, wäre dann eine zusätzliche Liste, die nur alle veränderten Objekte seit der letzten Speicherung enthält und dann am Ende der Runde oder einer bestimmten Zeitspanne sichert. Da macht sich dann aber ein Problem mit Plain Text Files auf. Soweit ich weiß werden die nur ganz geschrieben/gelesen bzw. etwas hinten rangehängt. Es ist schwierig in einem großen XML nur einzelene Objekte zu ändern. Das heißt am Ende hängen vielleicht viele Versionen des gleichen Objektes (nach ID, Instanzname) hintereinander und nur das letzte ist das beim Restore das Gültige. Das wäre dann mit einer SQLite DB deutlich einfacher zu handhaben.
Zuletzt geändert von nunatak am Dienstag 4. Oktober 2016, 07:48, insgesamt 1-mal geändert.
Sirius3
User
Beiträge: 17738
Registriert: Sonntag 21. Oktober 2012, 17:20

@nunatak: Wörterbücher ist schon mal der richtige Ansatz, exec der total falsche. Statt eines globalen Objekts g wäre ein Attribut in der Klasse, die ein Spiel beschreibt, besser. Namen sollten keine Abkürzungen enthalten, g geht gar nicht und was heißt NPC? Spontan würde ich sagen, sowohl die computergenerierten Gegner als auch die Spieler wären besser in einer Liste aufgehoben. Über den Schlüssel orc5 wirst Du sowieso nie auf Dein Orc-Objekt zugreifen müssen. Um den Ansatz von BlackJack weiterzudenken hätte die Spiel-Klasse eine as_dict-Methode, die in einer Schleife die as_dict-Methoden aller Charaktere-Instanzen aufruft.
nunatak
User
Beiträge: 17
Registriert: Donnerstag 3. März 2011, 19:42

Ist es wirklich sinnvoll ein DIctionary als Container für alle Objekte zu verwenden. Ich dachte das könnte mit zunehmender Anzahl Performance-Probleme bereiten. Wahrscheinlich nicht in meinem konkreten keinen Testspiel, aber ich versuche es halt von Anfang an etwas skalierbar zu machen um nicht einen total falschen Weg gewählt zu haben, wenn mal ein größeres Projekt realisiert werden soll.
Sirius3
User
Beiträge: 17738
Registriert: Sonntag 21. Oktober 2012, 17:20

@nunatak: was für Performance-Probleme erwartest Du denn bei der Verwendung eines Wörterbuchs und was würdest Du statt dessen nehmen? Aber wie schon geschrieben, würde ich sowieso eine List nehmen und zwar weil der Schlüssel irgendwie irrelevant ist.
nunatak
User
Beiträge: 17
Registriert: Donnerstag 3. März 2011, 19:42

Wie soll so eine as_dict()-Methode aussehen? Verstehe ich das richtig, diese soll mir das gesamte Objekt als Dictionary ausgeben?

@Sirius3: OK, ich denke drüber nach. Hast Recht, im Grunde werden die Objekte nicht über den Instanznamen aufgerufen, sondern in irgendeiner Schleife.
nunatak
User
Beiträge: 17
Registriert: Donnerstag 3. März 2011, 19:42

Ah ok. as_dict() ist quasi schon builtin. self.__dict__
Sirius3
User
Beiträge: 17738
Registriert: Sonntag 21. Oktober 2012, 17:20

@nunatak: nein, as_dict muß ein Wörterbuch zurückliefern, das nur noch primitive Typen (Zahl, String, Liste, Wörterbuch) enthält, damit man dieses Wörterbuch (z.B. mit JSON) einfach serialisieren kann.
BlackJack

@nunatak: Natürlich kann das Wörterbuch mit der Welt irgendwann zu Performance-Problemen führen, aber Du versuchst hier welche zu lösen die Du noch gar nicht hast. Implementiere am besten erst einmal Dein Spiel sauber. Wenn Du dann *messbare* Probleme bekommst, kannst Du die Persistenz der Spielwelt ja immer noch anders lösen. Dabei musst Du jetzt nur darauf achten, dass dieser Teil sauber getrennt und damit leicht austauschbar ist.

Falls Du so etwas wie Schlüssel für die einzelnen Objekte benötigst, würde ich die entweder im jeweiligen Objekt speichern, oder eine Klasse schreiben, die sich um so etwas kümmert wie die Zuordnung von Schlüsseln zu Objekten und das laden/speichern. Wenn Du so etwas implementieren möchtest wie “nur veränderte Objekte speichern” musst Du relativ tief in Python und Metaprogrammierung einsteigen um überhaupt sinnvoll feststellen zu können, ob sich ein Objekt verändert hat. Da bist Du dann tatsächlich nahe dran so etwas wie ein eigenes ORM zu programmieren. In der Situation würde ich dann wohl eher zu SQLAlchemy greifen und in eine SQLite-Datenbank sichern als das Rad neu zu erfinden.

Für automatische Sicherungspunkte der Welt im laufenden Spiel könnte man `pickle` dann übrigens wieder verwenden. Wenn es nicht um die langfristige Speicherung geht, sondern nur darum während eines Spiels mal kurz zurückspringen zu können, oder nach einem Absturz mit der gleichen Codebasis wieder starten zu können um eine ordentliche Speicherung mit dem letzten Spielstand durchzuführen.

`__dict__()` eignet sich nicht zwingend als Ergebnis für `is_dict()`. Nur in sehr einfachen Fällen. Für JSON oder YAML darf das am Ende ja nur bestimmte Datentypen enthalten. Und vielleicht möchte man auch gar nicht alle Attribute tatsächlich speichern. Manchmal hat man auch Redundanzen, oder Dinge die sich nicht so einfach serialisieren lassen, wie Kreise im Objektgraph. Oder die Anordnung der Objekte soll in der serialisierten Form anders aussehen als im Objektgraph im Speicher.
Antworten