Wie kann ich die Abwärtskompatibilität des gespeicherten Spiels aufrechterhalten?


8

Ich habe ein komplexes Simulationsspiel, dem ich Funktionen zum Speichern von Spielen hinzufügen möchte. Ich werde es nach der Veröffentlichung ständig mit neuen Funktionen aktualisieren.

Wie kann ich sicherstellen, dass meine Updates vorhandene Speicherspiele nicht beschädigen? Welcher Architektur sollte ich folgen, um dies zu ermöglichen?


Mir ist keine generische Architektur für dieses Ziel bekannt, aber ich würde dafür sorgen, dass der Patch-Prozess auch gespeicherte Spiele aktualisiert / konvertiert, um die Kompatibilität mit neuen Funktionen sicherzustellen.
Loodakrawa

Antworten:


9

Ein einfacher Ansatz besteht darin, alte Ladefunktionen beizubehalten. Sie benötigen nur eine einzige Speicherfunktion, die nur die neueste Version ausgibt. Die Ladefunktion erkennt die korrekte versionierte Ladefunktion, die aufgerufen werden soll (normalerweise durch Schreiben einer Versionsnummer irgendwo am Anfang Ihres Sicherungsdateiformats). Etwas wie:

class GameState:
  loadV1(stream):
    // do stuff

  loadV2(stream):
    // do different stuff

  loadV3(stream):
    // yet other stuff

  save(stream):
    // note this is version 3
    stream.write(3)
    // write V3 data

  load(stream):
    version = stream.read()
    if version == 1: loadV1(stream)
    else if version == 2: loadV2(stream)
    else if version == 3: loadV3(stream)

Sie können dies für die gesamte Datei, für einzelne Abschnitte der Datei, für einzelne Spielobjekte / -komponenten usw. tun. Welche Aufteilung am besten ist, hängt von Ihrem Spiel und dem Status ab, den Sie serialisieren.

Beachten Sie, dass Sie damit nur so weit kommen. Irgendwann könnten Sie Ihr Spiel so weit ändern, dass das Speichern von Daten aus früheren Versionen einfach keinen Sinn ergibt. Zum Beispiel kann ein Rollenspiel verschiedene Charakterklassen haben, die der Spieler auswählen kann. Wenn Sie eine Zeichenklasse entfernen, können Sie nicht viel mit dem Speichern von Zeichen tun, die diese Klasse haben. Vielleicht könnten Sie es in eine ähnliche Klasse konvertieren, die es noch gibt ... vielleicht. Gleiches gilt, wenn Sie andere Teile des Spiels so weit ändern, dass sie den alten Versionen nicht sehr ähnlich sind.

Seien Sie sich bewusst, dass Ihr Spiel nach dem Versand "fertig" ist. Sie können DLC oder andere Updates im Laufe der Zeit veröffentlichen, aber es werden keine besonders großen Änderungen am Spiel selbst sein. Nehmen wir zum Beispiel die meisten MMOs: WoW wird seit vielen Jahren mit neuen Updates und Änderungen gepflegt, aber es ist immer noch mehr oder weniger das gleiche Spiel wie beim ersten Erscheinen.

Für die frühe Entwicklung würde ich mir einfach keine Sorgen machen. Einsparungen sind in frühen Tests kurzlebig. Es ist jedoch eine andere Geschichte, wenn Sie die öffentliche Beta erreichen.


1
Diese. Leider funktioniert dies selten so hübsch wie beworben. Normalerweise basieren diese Ladefunktionen auf Hilfsfunktionen ( ReadCharacterkönnen aufgerufen werden ReadStat, die sich von einer Version zur nächsten ändern können oder nicht). Daher müssen Sie für jede dieser Versionen Versionen beibehalten, was es immer schwieriger macht, Schritt zu halten. Wie immer gibt es keine Silberkugel, und die Beibehaltung alter Ladefunktionen ist ein guter Ausgangspunkt.
Pyjama-Pyjama

5

Eine einfache Möglichkeit, einen Anschein von Versionierung zu erwecken, besteht darin, die Mitglieder der Objekte, die Sie serialisieren, zu verstehen. Wenn Ihr Code die verschiedenen zu serialisierenden Datentypen versteht, können Sie eine gewisse Robustheit erzielen, ohne zu viel Arbeit zu leisten.

Angenommen, wir haben ein serialisiertes Objekt, das so aussieht:

ObjectType
{
  m_name = "a string"
  m_size = { 1.2, 2.1 }
  m_someStruct = {
    m_deeperInteger = 5
    m_radians = 3.14
  }
}

Es sollte leicht zu erkennen sein, dass der Typ ObjectType Datenelemente aufgerufen wurden m_name, m_sizeundm_someStruct . Wenn Sie Datenelemente zur Laufzeit (irgendwie) durchlaufen oder auflisten können, können Sie beim Lesen dieser Datei einen Mitgliedsnamen einlesen und ihn einem tatsächlichen Mitglied in Ihrer Objektinstanz zuordnen.

Wenn Sie in dieser Suchphase kein passendes Datenelement finden, können Sie diesen Teil der Sicherungsdatei ignorieren. Sagen Sie zum Beispiel Version 1.0 vonSomeStruct hatte ein m_nameDatenelement. Dann patchen Sie und dieses Datenelement wurde vollständig entfernt. Beim Laden Ihrer Sicherungsdatei werden Sie darauf stoßenm_name ein passendes Mitglied und keine Übereinstimmung finden. Ihr Code kann einfach ohne Absturz zum nächsten Mitglied in der Datei weitergeleitet werden. Auf diese Weise können Sie Datenelemente entfernen, ohne sich Sorgen machen zu müssen, dass alte Sicherungsdateien beschädigt werden.

Wenn Sie einen neuen Typ eines Datenelements hinzufügen und versuchen, aus einer alten Sicherungsdatei zu laden, initialisiert Ihr Code das neue Mitglied möglicherweise nicht. Dies kann zu einem Vorteil genutzt werden: Neue Datenelemente können beim manuellen Patchen in Sicherungsdateien eingefügt werden, möglicherweise durch Einführung von Standardwerten (oder auf intelligentere Weise).

Mit diesem Format können die gespeicherten Dateien auch einfach von Hand bearbeitet oder geändert werden. Die Reihenfolge, in der die Datenelemente nicht wirklich viel mit der Gültigkeit der Serialisierungsroutine zu tun haben. Jedes Mitglied wird unabhängig nachgeschlagen und initialisiert. Dies könnte eine Schönheit sein, die ein wenig zusätzliche Robustheit hinzufügt.

All dies kann durch irgendeine Art von Selbstbeobachtung erreicht werden. Sie möchten in der Lage sein, ein Datenelement durch Zeichenfolgensuche abzufragen und den tatsächlichen Datentyp des Datenelements zu ermitteln. Dies kann in C ++ mithilfe einer benutzerdefinierten Introspektion erreicht werden, und in anderen Sprachen sind möglicherweise Introspektionsfunktionen integriert.


Dies ist nützlich, um Daten und Klassen robuster zu machen. (In .NET heißt die Funktion "Reflektion"). Ich wundere mich über Sammlungen ... meine KI ist kompliziert und verwendet viele temporäre Sammlungen, um Daten zu verarbeiten. Sollte ich versuchen, sie nicht zu speichern ...? Beschränken Sie das Speichern möglicherweise auf "sichere Punkte", an denen die Verarbeitung beendet ist.
Roggenbrot

@aman Wenn Sie eine Sammlung speichern, können Sie die tatsächlichen Daten in diese Sammlungen wie in meinem ursprünglichen Beispiel schreiben, außer in einem "Array-Format", wie in vielen von ihnen hintereinander. Sie können immer noch dieselbe Idee auf jedes einzelne Element eines Arrays oder jeden anderen Container anwenden. Sie müssen nur einen generischen "Array-Serializer", "List-Serializer" usw. schreiben. Wenn Sie einen generischen "Container-Serializer" möchten, benötigen Sie wahrscheinlich eine Zusammenfassung SerializingIterator, und dieser Iterator wird für jeden Containertyp implementiert.
RandyGaul

1
Oh und ja, Sie sollten versuchen, das Speichern komplizierter Sammlungen mit Zeigern so weit wie möglich zu vermeiden. Oft kann dies mit viel Nachdenken und cleverem Design vermieden werden. Serialisierung kann sehr kompliziert werden, daher lohnt es sich, sie so weit wie möglich zu vereinfachen. @aman
RandyGaul

Es gibt auch das Problem, ein Objekt zu deserialisieren, wenn sich die Klasse geändert hat ... Ich denke, der .NET-Deserializer stürzt in vielen Fällen ab.
Roggenbrot

2

Dies ist ein Problem, das nicht nur bei Spielen, sondern auch bei jeder Dateiaustauschanwendung auftritt. Sicherlich gibt es keine perfekten Lösungen, und es ist wahrscheinlich unmöglich, ein Dateiformat zu erstellen, das mit jeder Art von Änderung Schritt hält. Daher ist es wahrscheinlich eine gute Idee, sich auf die Art der Änderungen vorzubereiten, die Sie möglicherweise erwarten.

In den meisten Fällen werden Sie wahrscheinlich nur Felder und Werte hinzufügen / entfernen, während die allgemeine Struktur Ihrer Dateien unberührt bleibt. In diesem Fall können Sie einfach Ihren Code schreiben, um unbekannte Felder zu ignorieren, und sinnvolle Standardeinstellungen verwenden, wenn ein Wert nicht verstanden / analysiert werden kann. Die Implementierung ist recht einfach und ich mache viel.

Manchmal möchten Sie jedoch die Struktur der Datei ändern. Sprich von textbasiert zu binär; oder von festen Feldern zu Größenwert. In diesem Fall möchten Sie höchstwahrscheinlich die Quelle des alten Dateireaders einfrieren und eine neue für den neuen Dateityp erstellen, wie in Seans Lösung. Stellen Sie sicher, dass Sie den gesamten Legacy-Reader isolieren, da Sie sonst möglicherweise etwas ändern, das ihn betrifft. Ich empfehle dies nur für größere Änderungen der Dateistruktur.

Diese beiden Methoden sollten in den meisten Fällen funktionieren. Beachten Sie jedoch, dass dies nicht die einzigen möglichen Änderungen sind, auf die Sie stoßen können. Ich hatte einen Fall, in dem ich den gesamten Level-Ladecode von Lesen auf Streaming ändern musste (für die mobile Version des Spiels, die auf Geräten mit deutlich reduzierter Bandbreite und Speicher funktionieren sollte). Eine solche Änderung ist viel tiefer und erfordert höchstwahrscheinlich Änderungen in vielen anderen Teilen des Spiels, von denen einige Änderungen in der Struktur der Datei selbst erforderlich machten.


0

Auf einer höheren Ebene: Wenn Sie dem Spiel neue Funktionen hinzufügen, verfügen Sie über eine Funktion "Neue Werte erraten", mit der Sie die alten Funktionen übernehmen und die Werte der neuen erraten können.

Ein Beispiel könnte dies klarer machen. Angenommen, ein Spiel modelliert Städte und diese Version 1.0 verfolgt den Gesamtentwicklungsstand der Städte, während Version 1.1 zivilisationsähnliche Gebäude hinzufügt. (Ich persönlich bevorzuge es, die Gesamtentwicklung zu verfolgen, da sie weniger unrealistisch ist, aber ich schweife ab.) GuessNewValues ​​() für 1.1 würde bei einer Sicherungsdatei von 1.0 mit einer alten Entwicklungsstufe beginnen und basierend darauf raten, was Gebäude wären in der Stadt gebaut worden - vielleicht mit Blick auf die Kultur der Stadt, ihre geografische Lage, den Schwerpunkt ihrer Entwicklung, so etwas.

Ich hoffe, dass dies im Allgemeinen nachvollziehbar ist. Wenn Sie einem Spiel neue Funktionen hinzufügen, müssen Sie zum Laden einer Sicherungsdatei, die diese Funktionen noch nicht enthält, die besten Daten erraten und diese kombinieren mit den Daten, die Sie geladen haben.

Für die Low-Level-Seite würde ich die Antwort von Sean Middleditch (die ich hochgestimmt habe) unterstützen: Behalten Sie die vorhandene Ladelogik bei, möglicherweise sogar alte Versionen der relevanten Klassen, und nennen Sie zuerst diese, dann a Konverter.


0

Ich würde vorschlagen, mit etwas wie XML zu arbeiten (wenn Sie Dateien speichern, die sehr klein sind), damit Sie nur eine Funktion benötigen, um das Markup zu handhaben, unabhängig davon, was Sie darin einfügen. Der Stammknoten dieses Dokuments könnte die Version deklarieren, die das Spiel gespeichert hat, und es Ihnen ermöglichen, Code zu schreiben, um die Datei bei Bedarf auf die neueste Version zu aktualisieren.

<save version="1">
  <player name="foo" score="10" />
  <data>![CDATA[lksdf9owelkjlkdfjdfgdfg]]</data>
</save>

Dies bedeutet auch, dass Sie eine Transformation anwenden können, wenn Sie die Daten vor dem Laden in ein "aktuelles Versionsformat" konvertieren möchten. Anstatt viele versionierte Funktionen herumliegen zu haben, hätten Sie einfach eine Reihe von xsl-Dateien, aus denen Sie auswählen können die Konvertierung durchführen. Dies kann jedoch zeitaufwändig sein, wenn Sie mit xsl nicht vertraut sind.

Wenn Ihre gespeicherten Dateien massiv sind, könnte XML ein Problem sein. Normalerweise funktionieren gespeicherte Dateien sehr gut, wenn Sie nur Schlüsselwertpaare wie folgt in die Datei einfügen ...

version=1
player=foo
data=lksdf9owelkjlkdfjdfgdfg
score=10

Wenn Sie dann aus dieser Datei lesen, schreiben und lesen Sie eine Variable immer auf die gleiche Weise. Wenn Sie eine neue Variable benötigen, erstellen Sie eine neue Funktion, um sie zu schreiben und zu lesen. Sie könnten einfach eine Funktion für Variablentypen schreiben, damit Sie einen "String-Reader" und einen "Int-Reader" haben. Dies würde nur dann funktionieren, wenn Sie einen Variablentyp zwischen den Versionen geändert haben, aber Sie sollten dies niemals tun, da die Variable bei etwas anderes bedeutet An diesem Punkt sollten Sie stattdessen eine neue Variable mit einem anderen Namen erstellen.

Die andere Möglichkeit besteht natürlich darin, ein Datenbanktypformat oder eine CSV-Datei zu verwenden, die jedoch von den gespeicherten Daten abhängt.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.