保存したゲームの下位互換性を維持するにはどうすればよいですか?


8

セーブゲーム機能を追加したい複雑なシミュレーションゲームがあります。私はリリース後も継続的に新機能で更新します。

更新によって既存のセーブゲームが中断されないようにするにはどうすればよいですか?これを可能にするには、どのようなアーキテクチャに従う必要がありますか?


私はこの目標のための一般的なアーキテクチャを知りませんが、パッチ適用プロセスでセーブゲームを更新/変換して、新機能との互換性を確保します。
loodakrawa 2014年

回答:


9

簡単な方法の1つは、古い読み込み関数を維持することです。最新バージョンのみを書き出す保存機能が1つだけ必要です。load関数は、正しいバージョンのロード関数を検出して呼び出します(通常、保存ファイル形式の先頭のどこかにバージョン番号を書き出すことにより)。何かのようなもの:

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)

これは、ファイル全体、ファイルの個々のセクション、個々のゲームオブジェクト/コンポーネントなどに対して行うことができます。最適な分割は、ゲームとシリアル化する状態の量によって異なります。

これは今のところあなたを得るだけであることに注意してください。ある時点で、ゲームを変更して、以前のバージョンのセーブデータが意味をなさないようにするかもしれません。たとえば、RPGには、プレイヤーが選択できるさまざまなキャラクタークラスがある場合があります。文字クラスを削除すると、そのクラスを持つ文字の保存でできることはそれほど多くありません。たぶん、あなたはそれをまだ存在している類似のクラスに変換することができます...たぶん。ゲームの他の部分を変更して、古いバージョンとよく似ていない場合も同様です。

ゲームを出荷すると、「完了」になることに注意してください。DLCやその他のアップデートは時間の経過とともにリリースされる可能性がありますが、ゲーム自体に特に大きな変更が加えられることはありません。ほとんどのMMOを例にとると、WoWは何年もの間、新しい更新と変更によって維持されてきましたが、それでも、最初に登場したときとほぼ同じゲームです。

初期の開発では、私はそれについて心配する必要はありませんでした。初期のテストでは、節約は一時的なものです。ただし、パブリックベータ版に移行すると、また別の話になります。


1
この。残念ながら、これが宣伝どおりに機能することはめったにありません。通常、これらのロード関数はヘルパー関数に依存しているため(ReadCharacterを呼び出す可能性がありますReadStat。これは、あるバージョンから次のバージョンに変更される場合とそうでない場合があります)、それぞれのバージョンを保持する必要があるため、継続することがますます難しくなります。いつものように、特効薬はありません。古いローディング関数を維持することは良い出発点です。
パンダパジャマ2014年

5

バージョン管理の類似を実現する簡単な方法は、シリアル化するオブジェクトのメンバーを理解することです。コードがシリアル化されるさまざまなタイプのデータを理解している場合、あまり多くの作業を行わなくても、ある程度の堅牢性を得ることができます。

次のようなシリアル化されたオブジェクトがあるとします。

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

タイプがいることを確認するために簡単なはずObjectTypeのデータメンバーが呼ばれたm_namem_sizem_someStruct。実行時に(どういうわけか)データメンバーをループまたは列挙できる場合、このファイルを読み取るときに、メンバー名を読み取って、オブジェクトインスタンス内の実際のメンバーと一致させることができます。

このルックアップ段階で、一致するデータメンバーが見つからない場合は、保存ファイルのこの部分を無視しても問題ありません。たとえば、バージョン1.0にデータメンバーがSomeStructあったとしm_nameます。次に、パッチを適用すると、このデータメンバーは完全に削除されました。保存ファイルをロードするときにm_name、一致するメンバーを見つけて検索し、一致するものを見つけません。コードは、クラッシュすることなく、ファイルの次のメンバーに移ることができます。これにより、古い保存ファイルの破損を心配することなく、データメンバーを削除できます。

同様に、新しいタイプのデータメンバーを追加し、古い保存ファイルからロードしようとすると、コードは新しいメンバーを初期化しない可能性があります。これは有利に利用できます。新しいデータメンバーは、おそらくデフォルト値を導入することによって(またはよりインテリジェントな手段によって)手動でパッチ中に保存ファイルに挿入できます。

この形式では、保存ファイルを手動で簡単に操作または変更することもできます。データメンバーの順序は、シリアル化ルーチンの有効性とはあまり関係がありません。各メンバーは個別に検索および初期化されます。これは、もう少し堅牢性を追加する便利な機能です。

これはすべて、何らかの型のイントロスペクションを通じて実現できます。文字列ルックアップによってデータメンバーをクエリできるようになり、データメンバーが実際のデータタイプを判別できるようにする必要があります。これは、カスタムイントロスペクションの形式を使用してC ++で実現できます。他の言語には、イントロスペクション機能が組み込まれている場合があります。


これは、データとクラスをより堅牢にするのに役立ちます。(.NETでは、この機能は「リフレクション」と呼ばれます)。コレクションについて知りたい...私のAIは複雑で、多くの一時的なコレクションを使用してデータを処理しています。保存しないようにしましょう...?おそらく、処理が終了した「安全なポイント」に保存を制限します。
ライ麦パン

@amanコレクションを保存する場合、元の例と同様に、これらのコレクションに実際のデータを書き込むことができます。それでも、配列やその他のコンテナーの個々の要素に同じアイデアを適用できます。いくつかの一般的な「配列シリアライザー」、「リストシリアライザー」などを記述する必要があります。一般的な「コンテナーシリアライザー」が必要な場合は、おそらくSerializingIterator何らかの抽象が必要であり、このイテレーターはコンテナーのタイプごとに実装されます。
RandyGaul 2014年

1
ああ、そうです。ポインタを使用して複雑なコレクションをできるだけ保存しないようにしてください。多くの場合、これは多くの考えと賢い設計で回避できます。シリアライゼーションは非常に複雑になる可能性があるため、できるだけ単純化することで成果が得られます。@aman
RandyGaul

クラスが変更されたときにオブジェクトをデシリアライズする問題もあります...多くの場合、.NETデシリアライザはクラッシュすると思います。
ライ麦パン

2

これは、ゲームだけでなく、ファイル交換アプリケーションにも存在する問題です。確かに、完璧な解決策はありません。また、あらゆる種類の変更に対応できるファイル形式を作成することは不可能である可能性が高いため、予想される変更の種類に備えることをお勧めします。

ほとんどの場合、ファイルの一般的な構造はそのままに、フィールドと値を追加または削除するだけです。その場合は、不明なフィールドを無視するようにコードを記述し、値を理解または解析できない場合は適切なデフォルトを使用できます。これを実装することは非常に簡単で、私は多くのことを行います。

ただし、ファイルの構造を変更したい場合があります。テキストベースからバイナリへと言ってください。または固定フィールドからサイズ値へ。そのような場合、おそらくSeanのソリューションのように、古いファイルリーダーのソースを凍結し、新しいファイルタイプ用に新しいファイルリーダーを作成する必要があります。レガシーリーダー全体を分離するようにしてください。そうしないと、レガシーリーダーに影響する何かを変更してしまう可能性があります。これは、主なファイル構造の変更にのみお勧めします。

これらの2つの方法はほとんどの場合に機能するはずですが、発生する可能性のある唯一の変更ではないことに注意してください。レベル読み込みコード全体を全体読み取りからストリーミングに変更しなければならないケースがありました(帯域幅とメモリが大幅に削減されたデバイスで動作するモバイルバージョンのゲームの場合)。このような変更はより深く、ゲームの他の多くの部分での変更が必要になる可能性が高く、その一部にはファイル自体の構造の変更が必要でした。


0

より高いレベル:ゲームに新しい機能を追加する場合は、「新しい値の推測」機能を使用して、古い機能を取得し、新しい機能の値を推測できます。

例では、これをより明確にするかもしれません。都市をモデル化するゲームで、バージョン1.0が都市の全体的な開発レベルを追跡し、バージョン1.1が文明のような特定の建物を追加するとします。(個人的には、非現実的ではないので、全体的な開発を追跡することを好みますが、私は割愛します。)1.0のセーブファイルを指定すると、1.1のGuessNewValues()は、古い開発レベルの図で始まり、それに基づいて、何を推測するか建物は都市に建てられたでしょう-おそらく都市の文化、その地理的位置、開発の焦点、そのようなことを見てください。

これが一般的に理解できることを願っています-新しい機能をゲームに追加する場合、それらの機能をまだ備えていないセーブファイルをロードするには、新しいデータが何であるかを最もよく推測し、それらを組み合わせる必要がありますロードしたデータで

下位レベルについては、Sean Middleditchの回答(私は賛成票を投じました)を支持します。既存のロードロジックを保持し、おそらく関連するクラスの古いバージョンを維持し、それを最初に呼び出してから、コンバータ。


0

XMLのようなものを使用することをお勧めします(保存するファイルが非常に小さい場合)。そのため、マークアップに何を入力しても、1つの関数でマークアップを処理するだけで済みます。そのドキュメントのルートノードは、ゲームを保存したバージョンを宣言し、必要に応じてファイルを最新バージョンに更新するコードを記述できます。

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

これは、データをロードする前にデータを「現在のバージョン形式」に変換したい場合に変換を適用できることを意味します。これにより、多数のバージョン管理された関数を配置する代わりに、選択する一連のxslファイルを用意できます。変換を行います。これは、xslに慣れていない場合は時間がかかります。

保存ファイルが大量のxmlである場合、問題が発生する可能性があります。通常、保存ファイルは、このようにキーと値のペアをファイルにダンプするだけで問題なく機能します。

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

次に、このファイルから読み取るときは、常に同じ方法で変数を書き込んだり読み取ったりします。新しい変数が必要な場合は、新しい関数を作成して、それを書き込んで読み取ることができます。「文字列リーダー」と「intリーダー」を使用できるように、変数タイプの関数を作成することができます。これは、バージョン間で変数タイプを変更した場合にのみ失敗しますが、変数は他の場所で何かを意味するため、変更しないでくださいこの点で、代わりに別の名前で新しい変数を作成する必要があります。

もちろんもう1つの方法は、データベースタイプの形式またはcsvファイルのようなものを使用することですが、保存するデータに依存します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.