UDN
Search public documentation:

DevelopmentKitGemsSaveGameStatesJP
English Translation
中国翻译
한국어

Interested in the Unreal Engine?
Visit the Unreal Technology site.

Looking for jobs and company info?
Check out the Epic games site.

Questions about support via UDN?
Contact the UDN Staff

UE3 ホーム > Unreal Development States Gems > ゲームステートの保存
UE3 ホーム > 入力 / 出力 > ゲームステートの保存

ゲームステートの保存


2012年3月に UDK について最終テスト実施済み

PC 対応
Mac 対応
iOS 対応

概要


「Unreal Engine 1」と「Unreal Engine 2」では、レベル全体をメモリからディスクに単に保存するという方法で、ゲームの保存がサポートされていました。このやり方は、生産においては十分機能しましたが、残念なことに、コンテンツ開発者によって後に加えられた変更は反映されませんでした。この問題を解決するために、保存されたゲームを表す別の方法として、過不足ないデータを保存するというやり方を取ります。これによって、保存されたゲームの回復は、まずレベルがロードされ、次に保存されたゲームのステートがレベル内のすべてのアクタおよびオブジェクトに適用されることによって実現します。

保存されたゲームのステートに留意して開発する


保存されたゲームステートの機能の仕方のせいで、ワールド内部で破壊するものについて十分注意する必要があります。アクタが破壊されると、アクタはもはや存在しなくなるため、保存されたゲームステートのシリアライザによってそのアクタを得ることはできなくなります。アクタが一時的なものであれば、通常は問題にならないでしょう。しかし、アクタがレベルデザイナーによって配置されたものであれば、レベルが再ロードされ、保存されたゲームステートが適用された際、レベルデザイナーによって位置されたアクタは、そのデータが存在しないため、その影響を受けることはありません。

保存されたゲームステートの機能に関する一般的なフロー


プレイヤーがしばらくレベルをプレイした後にゲームを保存する

例示目的のために、この開発キット記事では、コンソールコマンドだけが追加されています。当然のことながら、ご自分のゲームでは GUI を付け加えることになりますが、いずれにしても、常にファイル名のパラメータをともなって同一のコンソールコマンドを呼び出すことができます。あるいは、アクタまたはオブジェクトの特定のインスタンス内部で実行することに依存していないため、コンソールコマンド関数を静的にすることができます。 (PlayerController::ClientMessage() を呼び出しますが、Actor::GetALocalPlayerController() を使用することによって、常にローカルのプレイヤーコントローラを得ることができます)。

コンソールコマンドが実行されると、save game state (ゲーム保存ステート) のプロセスが開始されます。まず、SaveGameState オブジェクトがインスタンス化されます。SaveGameState オブジェクトは、アクタおよび Kismet、Matinee をイタレートおよびシリアライズします。さらに、ファイル名を「スクラブ」します。ファイル名をスクラブすることによって、不正な文字が加わらないようにすることができます。この場合は、スペースだけをチェックしているに過ぎませんが。もっと厳密にスクラブを実行する場合は、\ や /、?、! といった文字がファイル名に 入らない ようにする必要があるでしょう。スクラブ関数は、sav 拡張子がまだ付けられていない場合に、それを付加するようにもします。さらに、SaveGameState は、アクタおよび Kismet、Matinee をイタレートおよびシリアライズするように依頼を受けます。最後に、BasicSaveObject() によって SaveGameState がディスクに首尾よく保存されると、ゲームが保存されたことを告げるメッセージがプレイヤーに送られます。

SaveGameStatePlayerController.uc
/**
 * This exec function will save the game state to the file name provided.
 *
 * @param      FileName      File name to save the SaveGameState to
 */
exec function SaveGameState(string FileName)
{
  local SaveGameState SaveGameState;

  // Instance the save game state
  SaveGameState = new () class'SaveGameState';
  if (SaveGameState == None)
  {
    return;
  }

  // Scrub the file name
  FileName = ScrubFileName(FileName);

  // Ask the save game state to save the game
  SaveGameState.SaveGameState();

  // Serialize the save game state object onto disk
  if (class'Engine'.static.BasicSaveObject(SaveGameState, FileName, true, class'SaveGameState'.const.SAVEGAMESTATE_REVISION))
  {
    // If successful then send a message
    ClientMessage("Saved game state to "$FileName$".", 'System');
  }
}

レベル名をシリアライズする

保存されたゲームステートは、レベル名 (マップファイル名) をシリアライズすることによって、保存されたゲームステート自身がロードされたときに、どのマップをロードすべきか知ることができるようになります。レベル名をコンフィギュレーション ファイルといった他のファイルに格納するよりも、保存されたゲームステート内に格納する方が合理的です。保存されたゲームステートは、格納したい変数だけをセットするだけで済みます。ディスクに実際に保存するのは、BasicSaveObject() だからです。ストリーミング レベルが、ビジブルである場合や、ペンディングしているロードリクエストを受けている場合は、配列に保存されることによって、save game state が再ロードされたときにストリーミング レベルが即座にロードされることになります。同時に、この手順で現在のGameInfo_classを保存します。

SaveGameState.uc
/**
 * Saves the game state by serializing all of the actors that implement the SaveGameStateInterface, Kismet and Matinee.
 */
function SaveGameState()
{
  local WorldInfo WorldInfo;

  // Get the world info, abort if the world info could not be found
  WorldInfo = class'WorldInfo'.static.GetWorldInfo();
  if (WorldInfo == None)
  {
    return;
  }

  // Save the map file name
  PersistentMapFileName= String(WorldInfo.GetPackageName());

  // Save the currently streamed in map file names
  if (WorldInfo.StreamingLevels.Length > 0)
  {
    // Iterate through the streaming levels
    for (i = 0; i < WorldInfo.StreamingLevels.Length; ++i)
    {
      // Levels that are visible and has a load request pending should be included in the streaming levels list
      if (WorldInfo.StreamingLevels[i] != None && (WorldInfo.StreamingLevels[i].bIsVisible || WorldInfo.StreamingLevels[i].bHasLoadRequestPending))
      {
        StreamingMapFileNames.AddItem(String(WorldInfo.StreamingLevels[i].PackageName));
      }
    }
  }

  // Save the game info class
  GameInfoClassName = PathName(WorldInfo.Game.Class);
}

SaveGameStateInterface を実装しているすべてのアクタを JSon としてシリアライズする

動的なアクタだけがシリアライズされる必要があるため、この場合最適なイタレータは DynamicActors です。SaveGameStateInterface のためのフィルターも追加されることによって、シリアライズすべき動的アクタを選別することができるようになります。save game state を後に拡張するほうが簡単であるため (後に JSon データをシリアライズおよびデシリアライズするのはアクタであるため)、ここではインターフェイスが使用されます。SaveGameStateInterface を実装しているアクタが、自身をシリアライズするように求められると、エンコードされた JSon 文字列を返します。この文字列は、SerializedActorData 配列に追加され、さらにこの配列は BasicSaveObject() によって保存されます。

SaveGameState.uc
/**
 * Saves the game state by serializing all of the actors that implement the SaveGameStateInterface, Kismet and Matinee.
 */
function SaveGameState()
{
  local WorldInfo WorldInfo;
  local Actor Actor;
  local String SerializedActorData;
  local SaveGameStateInterface SaveGameStateInterface;
  local int i;

  // Get the world info, abort if the world info could not be found
  WorldInfo = class'WorldInfo'.static.GetWorldInfo();
  if (WorldInfo == None)
  {
    return;
  }

  // Save the persistent map file name
  PersistentMapFileName = String(WorldInfo.GetPackageName());

  // Save the currently streamed in map file names
  if (WorldInfo.StreamingLevels.Length > 0)
  {
    // Iterate through the streaming levels
    for (i = 0; i < WorldInfo.StreamingLevels.Length; ++i)
    {
      // Levels that are visible and has a load request pending should be included in the streaming levels list
      if (WorldInfo.StreamingLevels[i] != None && (WorldInfo.StreamingLevels[i].bIsVisible || WorldInfo.StreamingLevels[i].bHasLoadRequestPending))
      {
        StreamingMapFileNames.AddItem(String(WorldInfo.StreamingLevels[i].PackageName));
      }
    }
  }
 // Save the game info class
  GameInfoClassName = PathName(WorldInfo.Game.Class);

  // Iterate through all of the actors that implement SaveGameStateInterface and ask them to serialize themselves
  ForEach WorldInfo.DynamicActors(class'Actor', Actor, class'SaveGameStateInterface')
  {
    // Type cast to the SaveGameStateInterface
    SaveGameStateInterface = SaveGameStateInterface(Actor);
    if (SaveGameStateInterface != None)
    {
      // Serialize the actor
      SerializedActorData = SaveGameStateInterface.Serialize();
      // If the serialzed actor data is valid, then add it to the serialized world data array
      if (SerializedActorData != "")
      {
        SerializedWorldData.AddItem(SerializedActorData);
      }
    }
  }
}

SaveGameStateInterface はとてもシンプルです。2 つの関数が備わっており、これらは、このインターフェイスを実装しているすべてのアクタによって、実装される必要があります。Serialize() は、ロード時にアクタによって要求されるデータすべてをシリアライズします。Deserialize() は、先の時点で保存されていた JSon データを読み取るとともに、適切な値をリストアします。

SaveGameStateInterface.uc
interface SaveGameStateInterface;

/**
 * Serializes the actor's data into JSon
 *
 * @return  JSon data representing the state of this actor
 */
function String Serialize();

/**
 * Deserializes the actor from the data given
 *
 * @param  Data  JSon data representing the differential state of this actor
 */
function Deserialize(JSonObject Data);

Kismet と Matinee を JSon としてシリアライズする

save game state は、Kismet のイベントと変数もシリアライズすることができます。これによって、ゲームデザイナーは Kismet を使用してゲームの一部を実装することができるようになります。このためには、レベルにある Kismet のイベントと変数をイタレートして、それぞれをシリアライズします。

Kismet のイベントは、ActivationTime がオフセットとして計算されます。保存されたゲームステートが再ロードされると、WorldInfo.TimeSeconds が通常ゼロまたは非常に小さい数値になっています。これは、ゲームが前に保存されたときの時間ではないでしょう。Kismet のイベントが ReTriggerDelay 変数をセットしている場合は、通常、ActivationTime が重要となります。そこで、保存およびロードすることによってあまりにも素早く Kismet イベントが再トリガーされるというバグを防ぐには、ReTriggerDelay を考慮に入れて、ActivationTime から残っている時間を計算することが必要となります。このようにして、Kismet イベントが再ロードされると、通常、ActivationTime がこの後セットされます (すでにトリガーされてしまっている場合)。保存されるもう一つの値は、TriggerCount です。これは普通、MaxTriggerCount の値がゼロ以外にセットされているトリガーに必要となります。

Kismet の変数は、型キャストの試行錯誤によって見つけます。もう一つの選択肢としては、Kismet シーケンス オブジェクトをイタレートすることによって、各型の Kismet 変数を探すというやり方があります。どちらの方法でもかまいません。Kismet 変数が見つかると、その値がシリアライズされます。

SaveGameState.uc
/**
 * Saves the Kismet game state
 */
protected function SaveKismetState()
{
  local WorldInfo WorldInfo;
  local array<Sequence> RootSequences;
  local array<SequenceObject> SequenceObjects;
  local SequenceEvent SequenceEvent;
  local SeqVar_Bool SeqVar_Bool;
  local SeqVar_Float SeqVar_Float;
  local SeqVar_Int SeqVar_Int;
  local SeqVar_Object SeqVar_Object;
  local SeqVar_String SeqVar_String;
  local SeqVar_Vector SeqVar_Vector;
  local int i, j;
  local JSonObject JSonObject;

  // Get the world info, abort if it does not exist
  WorldInfo = class'WorldInfo'.static.GetWorldInfo();
  if (WorldInfo == None)
  {
    return;
  }

  // Get all of the root sequences within the world, abort if there are no root sequences
  RootSequences = WorldInfo.GetAllRootSequences();
  if (RootSequences.Length <= 0)
  {
    return;
  }

  // Serialize all SequenceEvents and SequenceVariables
  for (i = 0; i < RootSequences.Length; ++i)
  {
    if (RootSequences[i] != None)
    {
      // Serialize Kismet Events
      RootSequences[i].FindSeqObjectsByClass(class'SequenceEvent', true, SequenceObjects);
      if (SequenceObjects.Length > 0)
      {
        for (j = 0; j < SequenceObjects.Length; ++j)
        {
          SequenceEvent = SequenceEvent(SequenceObjects[j]);
          if (SequenceEvent != None)
          {
            JSonObject = new () class'JSonObject';
            if (JSonObject != None)
            {
              // Save the path name of the SequenceEvent so it can found later
              JSonObject.SetStringValue("Name", PathName(SequenceEvent));
              // Calculate the activation time of what it should be when the saved game state is loaded. This is done as the retrigger delay minus the difference between the current world time
              // and the last activation time. If the result is negative, then it means this was never triggered before, so always make sure it is larger or equal to zero.
              JsonObject.SetFloatValue("ActivationTime", FMax(SequenceEvent.ReTriggerDelay - (WorldInfo.TimeSeconds - SequenceEvent.ActivationTime), 0.f));
              // Save the current trigger count
              JSonObject.SetIntValue("TriggerCount", SequenceEvent.TriggerCount);
              // Encode this and append it to the save game data array
              SerializedWorldData.AddItem(class'JSonObject'.static.EncodeJson(JSonObject));
            }
          }
        }
      }

      // Serialize Kismet Variables
      RootSequences[i].FindSeqObjectsByClass(class'SequenceVariable', true, SequenceObjects);
      if (SequenceObjects.Length > 0)
      {
        for (j = 0; j < SequenceObjects.Length; ++j)
        {
          // Attempt to serialize as a boolean variable
          SeqVar_Bool = SeqVar_Bool(SequenceObjects[j]);
          if (SeqVar_Bool != None)
          {
            JSonObject = new () class'JSonObject';
            if (JSonObject != None)
            {
              // Save the path name of the SeqVar_Bool so it can found later
              JSonObject.SetStringValue("Name", PathName(SeqVar_Bool));
              // Save the boolean value
              JSonObject.SetIntValue("Value", SeqVar_Bool.bValue);
              // Encode this and append it to the save game data array
              SerializedWorldData.AddItem(class'JSonObject'.static.EncodeJson(JSonObject));
            }

            // Continue to the next one within the array as we're done with this array index
            continue;
          }

          // Attempt to serialize as a float variable
          SeqVar_Float = SeqVar_Float(SequenceObjects[j]);
          if (SeqVar_Float != None)
          {
            JSonObject = new () class'JSonObject';
            if (JSonObject != None)
            {
              // Save the path name of the SeqVar_Float so it can found later
              JSonObject.SetStringValue("Name", PathName(SeqVar_Float));
              // Save the float value
              JSonObject.SetFloatValue("Value", SeqVar_Float.FloatValue);
              // Encode this and append it to the save game data array
              SerializedWorldData.AddItem(class'JSonObject'.static.EncodeJson(JSonObject));
            }

            // Continue to the next one within the array as we're done with this array index
            continue;
          }

          // Attempt to serialize as an int variable
          SeqVar_Int = SeqVar_Int(SequenceObjects[j]);
          if (SeqVar_Int != None)
          {
            JSonObject = new () class'JSonObject';
            if (JSonObject != None)
            {
              // Save the path name of the SeqVar_Int so it can found later
              JSonObject.SetStringValue("Name", PathName(SeqVar_Int));
              // Save the int value
              JSonObject.SetIntValue("Value", SeqVar_Int.IntValue);
              // Encode this and append it to the save game data array
              SerializedWorldData.AddItem(class'JSonObject'.static.EncodeJson(JSonObject));
            }

            // Continue to the next one within the array as we're done with this array index
            continue;
          }

          // Attempt to serialize as an object variable
          SeqVar_Object = SeqVar_Object(SequenceObjects[j]);
          if (SeqVar_Object != None)
          {
            JSonObject = new () class'JSonObject';
            if (JSonObject != None)
            {
              // Save the path name of the SeqVar_Object so it can found later
              JSonObject.SetStringValue("Name", PathName(SeqVar_Object));
              // Save the object value
              JSonObject.SetStringValue("Value", PathName(SeqVar_Object.GetObjectValue()));
              // Encode this and append it to the save game data array
              SerializedWorldData.AddItem(class'JSonObject'.static.EncodeJson(JSonObject));
            }

            // Continue to the next one within the array as we're done with this array index
            continue;
          }

          // Attempt to serialize as a string variable
          SeqVar_String = SeqVar_String(SequenceObjects[j]);
          if (SeqVar_String != None)
          {
            JSonObject = new () class'JSonObject';
            if (JSonObject != None)
            {
              // Save the path name of the SeqVar_String so it can found later
              JSonObject.SetStringValue("Name", PathName(SeqVar_String));
              // Save the string value
              JSonObject.SetStringValue("Value", SeqVar_String.StrValue);
              // Encode this and append it to the save game data array
              SerializedWorldData.AddItem(class'JSonObject'.static.EncodeJson(JSonObject));
            }

            // Continue to the next one within the array as we're done with this array index
            continue;
          }

          // Attempt to serialize as a vector variable
          SeqVar_Vector = SeqVar_Vector(SequenceObjects[j]);
          if (SeqVar_Vector != None)
          {
            JSonObject = new () class'JSonObject';
            if (JSonObject != None)
            {
              // Save the path name of the SeqVar_Vector so it can found later
              JSonObject.SetStringValue("Name", PathName(SeqVar_Vector));
              // Save the vector value
              JSonObject.SetFloatValue("Value_X", SeqVar_Vector.VectValue.X);
              JSonObject.SetFloatValue("Value_Y", SeqVar_Vector.VectValue.Y);
              JSonObject.SetFloatValue("Value_Z", SeqVar_Vector.VectValue.Z);
              // Encode this and append it to the save game data array
              SerializedWorldData.AddItem(class'JSonObject'.static.EncodeJson(JSonObject));
            }

            // Continue to the next one within the array as we're done with this array index
            continue;
          }
        }
      }
    }
  }
}

Matinee の保存は、Kismet の保存と同じ方法で行われます。保存されるものが Matinee Kismet シーケンス アクションであるためです。すなわち、Kismet シーケンス オブジェクトのすべてがイタレートされ、SeqAct_Interp クラスのためにフィルタリングが行われます。 さらに、関係する変数がシリアライズされ、SerializedWorldData 配列に追加されます。

SaveGameState.uc
/**
 * Saves the Matinee game state
 */
protected function SaveMatineeState()
{
  local WorldInfo WorldInfo;
  local array<Sequence> RootSequences;
  local array<SequenceObject> SequenceObjects;
  local SeqAct_Interp SeqAct_Interp;
  local int i, j;
  local JSonObject JSonObject;

  // Get the world info, abort if it does not exist
  WorldInfo = class'WorldInfo'.static.GetWorldInfo();
  if (WorldInfo == None)
  {
    return;
  }

  // Get all of the root sequences within the world, abort if there are no root sequences
  RootSequences = WorldInfo.GetAllRootSequences();
  if (RootSequences.Length <= 0)
  {
    return;
  }

  // Serialize all SequenceEvents and SequenceVariables
  for (i = 0; i < RootSequences.Length; ++i)
  {
    if (RootSequences[i] != None)
    {
      // Serialize Matinee Kismet Sequence Actions
      RootSequences[i].FindSeqObjectsByClass(class'SeqAct_Interp', true, SequenceObjects);
      if (SequenceObjects.Length > 0)
      {
        for (j = 0; j < SequenceObjects.Length; ++j)
        {
          SeqAct_Interp = SeqAct_Interp(SequenceObjects[j]);
          if (SeqAct_Interp != None)
          {
            // Attempt to serialize the data
            JSonObject = new () class'JSonObject';
            if (JSonObject != None)
            {
              // Save the path name of the SeqAct_Interp so it can found later
              JSonObject.SetStringValue("Name", PathName(SeqAct_Interp));
              // Save the current position of the SeqAct_Interp
              JSonObject.SetFloatValue("Position", SeqAct_Interp.Position);
              // Save if the SeqAct_Interp is playing or not
              JSonObject.SetIntValue("IsPlaying", (SeqAct_Interp.bIsPlaying) ? 1 : 0);
              // Save if the SeqAct_Interp is paused or not
              JSonObject.SetIntValue("Paused", (SeqAct_Interp.bPaused) ? 1 : 0);
              // Encode this and append it to the save game data array
              SerializedWorldData.AddItem(class'JSonObject'.static.EncodeJson(JSonObject));
            }
          }
        }
      }
    }
  }
}

BasicSaveObject を使用してデータを保存する

すでに見たように、save game state データは、BasicSaveObject() によって保存されます。BasicSaveObject() は、ファイルの書き出しに成功したか否かによって、true または false を返します。これに基づいて、ファイルの書き出しに成功したか否かをメッセージとして表示することができます。

SaveGameStatePlayerController.uc
/**
 * This exec function will save the game state to the file name provided.
 *
 * @param      FileName      File name to save the SaveGameState to
 */
exec function SaveGameState(string FileName)
{
  local SaveGameState SaveGameState;

  // Instance the save game state
  SaveGameState = new () class'SaveGameState';
  if (SaveGameState == None)
  {
    return;
  }

  // Scrub the file name
  FileName = ScrubFileName(FileName);

  // Ask the save game state to save the game
  SaveGameState.SaveGameState();

  // Serialize the save game state object onto disk
  if (class'Engine'.static.BasicSaveObject(SaveGameState, FileName, true, class'SaveGameState'.const.SAVEGAMESTATE_REVISION))
  {
    // If successful then send a message
    ClientMessage("Saved game state to "$FileName$".", 'System');
  }
}


プレイヤーが、保存されたゲームステートからゲームをロードする

LoadGameState() は、保存されたゲームステートがロードされるエントリーポイントです。繰り返しになりますが、この関数は、どのクラス インスタンスにも依存しないため、静的関数にすることができます。

SaveGameStatePlayerController.uc
/**
 * This exec function will load the game state from the file name provided
 *
 * @param    FileName    File name of load the SaveGameState from
 */
exec function LoadGameState(string FileName);

保存されたゲームステート オブジェクトをロードする

保存されたゲームステート オブジェクトは、まず、BasicLoadObject() を使ってディスクからロードされます。

SaveGameStatePlayerController.uc
/**
 * This exec function will load the game state from the file name provided
 *
 * @param    FileName    File name of load the SaveGameState from
 */
exec function LoadGameState(string FileName)
{
  local SaveGameState SaveGameState;

  // Instance the save game state
  SaveGameState = new () class'SaveGameState';
  if (SaveGameState == None)
  {
    return;
  }

  // Scrub the file name
  FileName = ScrubFileName(FileName);

  // Attempt to deserialize the save game state object from disk
  if (class'Engine'.static.BasicLoadObject(SaveGameState, FileName, true, class'SaveGameState'.const.SAVEGAMESTATE_REVISION))
  {
  }
}

保存されたゲームステートのファイル名を格納するコマンドラインをともなってマップをロードする

保存されたゲームステート オブジェクトのロードに成功すると、シリアライズされたマップがロードされます。その際、マップのロードが完了すると、引き続いて定義された保存されたゲームステートをロードするという旨のコマンドライン パラメータが伴われます。この関数を静的関数にすると決めた場合、他のグローバルな参照可能なアクタから ConsoleCommand() を呼び出すことができます。

ALERT! 注意: ここでは、コンソールコマンドの start が open の代わりに使用されています。start は必ずコマンドラインパラメータをリセットします。他方、open はコマンドライン パラメータを 追加 します。これは非常に重要です。このようにしなければ、コマンドライン パラメータの SaveGameState が何回も追加されることになるため、save game state が不正にロードされてしまいます。

SaveGameStatePlayerController.uc
/**
 * This exec function will load the game state from the file name provided
 *
 * @param    FileName    File name of load the SaveGameState from
 */
exec function LoadGameState(string FileName)
{
  local SaveGameState SaveGameState;

  // Instance the save game state
  SaveGameState = new () class'SaveGameState';
  if (SaveGameState == None)
  {
    return;
  }

  // Scrub the file name
  FileName = ScrubFileName(FileName);

  // Attempt to deserialize the save game state object from disk
  if (class'Engine'.static.BasicLoadObject(SaveGameState, FileName, true, class'SaveGameState'.const.SAVEGAMESTATE_REVISION))
  {
    // Start the map with the command line parameters required to then load the save game state
    ConsoleCommand("start "$SaveGameState.PersistentMapFileName$"?Game="$SaveGameState.GameInfoClassName$"?SaveGameState="$FileName);
  }
}

マップのロードが完了すると、保存されたゲームステート オブジェクトを再ロードする

マップがロードされると、SaveStateGameInfo::InitGame() によって、save game state コマンドライン パラメータが存在するか否かが識別されます。存在する場合は、PendingSaveGameFileName 内の値を保存します。さらに、マッチが開始されると、save game state オブジェクトが再びディスクからロードされ、ゲームステートをロードするように求められます。保存されたゲームステートがロードされると、メッセージがプレイヤーに送られて、保存されたゲームがロードされたことが伝えられます。ストリーミングレベルがある場合は、SaveStateGameInfo::StartMatch() が全プレイヤーコントローラに他のマップのストリーミングについて尋ねます。ただし、他のマップにおけるストリーミングは、同一ティックでは完了しないため、SaveStateGameInfo::WaitingForStreamingLevelsTimer() というループタイマーがセットされることによって、すべてのストリーミング レベルのロードが完了するのを見守ります。ストリーミングマップのロードが完了すると、Super.StartMatch() [UTGame::StartMatch() が呼び出されることによって、マッチが開始されます。

SaveGameStateGameInfo.uc
class SaveGameStateGameInfo extends UTGame;

// Pending save game state file name
var private string PendingSaveGameFileName;

/*
 * Initialize the game. The GameInfo's InitGame() function is called before any other scripts (including PreBeginPlay()), and is used by the GameInfo to initialize parameters and spawn its helper classes.
 *
 * @param    Options        Passed options from the command line
 * @param    ErrorMessage    Out going error messages
 */
event InitGame(string Options, out string ErrorMessage)
{
  Super.InitGame(Options, ErrorMessage);

  // Set the pending save game file name if required
  if (HasOption(Options, "SaveGameState"))
  {
    PendingSaveGameFileName = ParseOption(Options, "SaveGameState");
  }
  else
  {
    PendingSaveGameFileName = "";
  }
}

/**
 * Start the match - inform all actors that the match is starting, and spawn player pawns
 */
function StartMatch()
{
  local SaveGameState SaveGameState;
  local PlayerController PlayerController;
  local int i;

  // Check if we need to load the game or not
  if (PendingSaveGameFileName != "")
  {
    // Instance the save game state
    SaveGameState = new () class'SaveGameState';
    if (SaveGameState == None)
    {
      return;
    }

    // Attempt to deserialize the save game state object from disk
    if (class'Engine'.static.BasicLoadObject(SaveGameState, PendingSaveGameFileName, true, class'SaveGameState'.const.SAVEGAMESTATE_REVISION))
    {
      // Synchrously load in any streaming levels
      if (SaveGameState.StreamingMapFileNames.Length > 0)
      {
        // Ask every player controller to load up the streaming map
        ForEach WorldInfo.AllControllers(class'PlayerController', PlayerController)
        {
          // Stream map files now
          for (i = 0; i < SaveGameState.StreamingMapFileNames.Length; ++i)
          {
            PlayerController.ClientUpdateLevelStreamingStatus(Name(SaveGameState.StreamingMapFileNames[i]), true, true, true);
          }

          // Block everything until pending loading is done
          PlayerController.ClientFlushLevelStreaming();
        }

        // Store the save game state in StreamingSaveGameState
        StreamingSaveGameState = SaveGameState;
        // Start the looping timer which waits for all streaming levels to finish loading
        SetTimer(0.05f, true, NameOf(WaitingForStreamingLevelsTimer));
        return;
      }

      // Load the game state
      SaveGameState.LoadGameState();
    }

    // Send a message to all player controllers that we've loaded the save game state
    ForEach WorldInfo.AllControllers(class'PlayerController', PlayerController)
    {
      PlayerController.ClientMessage("Loaded save game state from "$PendingSaveGameFileName$".", 'System');
    }
  }

  Super.StartMatch();
}

function WaitingForStreamingLevelsTimer()
{
  local int i;
  local PlayerController PlayerController;

  for (i = 0; i < WorldInfo.StreamingLevels.Length; ++i)
  {
    // If any levels still have the load request pending, then return
    if (WorldInfo.StreamingLevels[i].bHasLoadRequestPending)
    {
      return;
    }
  }

  // Clear the looping timer
  ClearTimer(NameOf(WaitingForStreamingLevelsTimer));

  // Load the save game state
  StreamingSaveGameState.LoadGameState();
  // Clear it for garbage collection
  StreamingSaveGameState = None;

  // Send a message to all player controllers that we've loaded the save game state
  ForEach WorldInfo.AllControllers(class'PlayerController', PlayerController)
  {
    PlayerController.ClientMessage("Loaded save game state from "$PendingSaveGameFileName$".", 'System');
  }

  // Start the match
  Super.StartMatch();
}

JSon データをイタレートするとともに、レベル内のアクタとオブジェクト上でデータをデシリアライズする

保存されたゲームステート オブジェクトがロードされたため、SaveGameStateInterface を実装するアクタ、Kismet、Matinee をイタレートするとともに、SerializedWorldData 配列に格納されているデータ (JSon としてすでにエンコードされている) に基づいて、それらを復帰させることが可能になりました。

SerializedWorldData がイタレートされると、各エントリが JSonObject としてデコードされます。Name を取得することによって、その JSonObject データが何についてのものかということに関する情報が得られます。SeqAct_Interp のテストを行うことによって、そのデータが、Kismet イベントまたは Kismet 変数のための、Matinee オブジェクトあるいは SeqEvent、SeqVar に関連するデータであることが明らかになります。これらの 3 つではない場合は、ワールドのアクタのためのものであるはずです。

JSonObject データがワールドのアクタのためのものであれば、そのアクタは FindObject() を使用して取得されます。アクタのフルパス名が格納されているため、FindObject() は、レベルデザイナーによって配置された、いかなるアクタをも見つけることができます。 FindObject() が失敗した場合は、プレイ中にインスタンス化されたアクタのためのものであるはずです。そのため、多くの場合 ObjectArchetype も格納することが役立ちます。必要な場合に、保存されたゲームステートによって再インスタンス化することができるためです。アクタが見つかりインスタンス化されると、そのアクタは、SaveGameStateInterface にキャストされ、さらに、JSonObject 内に格納されたデータに基づいて自身をデシリアライズするように求められます。

SaveGameState.uc
/**
 * Loads the game state by deserializing all of the serialized data and applying the data to the actors that implement the SaveGameStateInterface, Kisment and Matinee.
 */
function LoadGameState()
{
  local WorldInfo WorldInfo;
  local int i;
  local JSonObject JSonObject;
  local String ObjectName;
  local SaveGameStateInterface SaveGameStateInterface;
  local Actor Actor, ActorArchetype;

  // No serialized world data to load!
  if (SerializedWorldData.Length <= 0)
  {
    return;
  }

  // Grab the world info, abort if no valid world info
  WorldInfo = class'WorldInfo'.static.GetWorldInfo();
  if (WorldInfo == None)
  {
    return;
  }

  // For each serialized data object
  for (i = 0; i < SerializedWorldData.Length; ++i)
  {
    if (SerializedWorldData[i] != "")
    {
      // Decode the JSonObject from the encoded string
      JSonObject = class'JSonObject'.static.DecodeJson(SerializedWorldData[i]);
      if (JSonObject != None)
      {
        // Get the object name
        ObjectName = JSonObject.GetStringValue("Name");
        // Check if the object name contains SeqAct_Interp, if so deserialize Matinee
        if (InStr(ObjectName, "SeqAct_Interp",, true) != INDEX_NONE)
        {
          LoadMatineeState(ObjectName, JSonObject);
        }
        // Check if the object name contains SeqEvent or SeqVar, if so deserialize Kismet
        else if (InStr(ObjectName, "SeqEvent",, true) != INDEX_NONE || InStr(ObjectName, "SeqVar",, true) != INDEX_NONE)
        {
          LoadKismetState(ObjectName, JSonObject);
        }
        // Otherwise it is some other type of actor
        else
        {
          // Try to find the persistent level actor
          Actor = Actor(FindObject(ObjectName, class'Actor'));

          // If the actor was not in the persistent level, then it must have been transient then attempt to spawn it
          if (Actor == None)
          {
            // Spawn the actor
            ActorArchetype = GetActorArchetypeFromName(JSonObject.GetStringValue("ObjectArchetype"));
            if (ActorArchetype != None)
            {
              Actor = WorldInfo.Spawn(ActorArchetype.Class,,,,, ActorArchetype, true);
            }
          }

          if (Actor != None)
          {
            // Cast to the save game state interface
            SaveGameStateInterface = SaveGameStateInterface(Actor);
            if (SaveGameStateInterface != None)
            {
              // Deserialize the actor
              SaveGameStateInterface.Deserialize(JSonObject);
            }
          }
        }
      }
    }
  }
}

/**
 * Returns an actor archetype from the name
 *
 * @return    Returns an actor archetype from the string representation
 */
function Actor GetActorArchetypeFromName(string ObjectArchetypeName)
{
  local WorldInfo WorldInfo;

  WorldInfo = class'WorldInfo'.static.GetWorldInfo();
  if (WorldInfo == None)
  {
    return None;
  }

  // Use static look ups if on the console, for static look ups to work
  //  * Force cook the classes or packaged archetypes to the maps
  //  * Add packaged archetypes to the StartupPackage list
  //  * Reference the packages archetypes somewhere within Unrealscript
  if (WorldInfo.IsConsoleBuild())
  {
    return Actor(FindObject(ObjectArchetypeName, class'Actor'));
  }
  else // Use dynamic look ups if on the PC
  {
    return Actor(DynamicLoadObject(ObjectArchetypeName, class'Actor'));
  }
}

Kismet のデシリアライズは、アクタのデシリアライズとほぼ同じ方法で行われます。異なる点は、Kismet シーケンス オブジェクトが見つからなかった場合に、UnrealScript がそれをインスタンス化しないという点です。FindObject() を使用して、Kismet シーケンスオブジェクトが見つかると、型キャストが実行されることによって、それが正確には何であるかが突き止められます。そこから、JSonObject の保存された値がリストアされます。

SaveGameState.uc
/**
 * Loads the Kismet Sequence state based on the data provided
 *
 * @param    ObjectName    Name of the Kismet object in the level
 * @param    Data      Data as JSon for the Kismet object
 */
function LoadKismetState(string ObjectName, JSonObject Data)
{
  local SequenceEvent SequenceEvent;
  local SeqVar_Bool SeqVar_Bool;
  local SeqVar_Float SeqVar_Float;
  local SeqVar_Int SeqVar_Int;
  local SeqVar_Object SeqVar_Object;
  local SeqVar_String SeqVar_String;
  local SeqVar_Vector SeqVar_Vector;
  local Object SequenceObject;
  local WorldInfo WorldInfo;

  // Attempt to find the sequence object
  SequenceObject = FindObject(ObjectName, class'Object');

  // Could not find sequence object, so abort
  if (SequenceObject == None)
  {
    return;
  }

  // Deserialize Kismet Event
  SequenceEvent = SequenceEvent(SequenceObject);
  if (SequenceEvent != None)
  {
    WorldInfo = class'WorldInfo'.static.GetWorldInfo();
    if (WorldInfo != None)
    {
      SequenceEvent.ActivationTime = WorldInfo.TimeSeconds + Data.GetFloatValue("ActivationTime");
    }

    SequenceEvent.TriggerCount = Data.GetIntValue("TriggerCount");
    return;
  }

  // Deserialize Kismet Variable Bool
  SeqVar_Bool = SeqVar_Bool(SequenceObject);
  if (SeqVar_Bool != None)
  {
    SeqVar_Bool.bValue = Data.GetIntValue("Value");
    return;
  }

  // Deserialize Kismet Variable Float
  SeqVar_Float = SeqVar_Float(SequenceObject);
  if (SeqVar_Float != None)
  {
    SeqVar_Float.FloatValue = Data.GetFloatValue("Value");
    return;
  }

  // Deserialize Kismet Variable Int
  SeqVar_Int = SeqVar_Int(SequenceObject);
  if (SeqVar_Int != None)
  {
    SeqVar_Int.IntValue = Data.GetIntValue("Value");
    return;
  }

  // Deserialize Kismet Variable Object
  SeqVar_Object = SeqVar_Object(SequenceObject);
  if (SeqVar_Object != None)
  {
    SeqVar_Object.SetObjectValue(FindObject(Data.GetStringValue("Value"), class'Object'));
    return;
  }

  // Deserialize Kismet Variable String
  SeqVar_String = SeqVar_String(SequenceObject);
  if (SeqVar_String != None)
  {
    SeqVar_String.StrValue = Data.GetStringValue("Value");
    return;
  }

  // Deserialize Kismet Variable Vector
  SeqVar_Vector = SeqVar_Vector(SequenceObject);
  if (SeqVar_Vector != None)
  {
    SeqVar_Vector.VectValue.X = Data.GetFloatValue("Value_X");
    SeqVar_Vector.VectValue.Y = Data.GetFloatValue("Value_Y");
    SeqVar_Vector.VectValue.Z = Data.GetFloatValue("Value_Z");
    return;
  }
}

Matinee のデシリアライズは、Kismet のデシリアライズと同様です。ただし、保存ゲームステートが保存されたのと同時に Matinee シーケンスが再生されていた場合は、IsPlaying が 1 として JSonObject に格納されます。そうして、ForceStartPosition がセットされ、Matinee が再生するように求められます。そうでない場合は、JSonObject 内に格納されている Position 値に基づいて、Matinee の位置がセットされます。

SaveGameState.uc
/**
 * Loads up the Matinee state based on the data
 *
 * @param    ObjectName    Name of the Matinee Kismet object
 * @param    Data      Saved Matinee Kismet data
 */
function LoadMatineeState(string ObjectName, JSonObject Data)
{
  local SeqAct_Interp SeqAct_Interp;
  local float OldForceStartPosition;
  local bool OldbForceStartPos;

  // Find the matinee kismet object
  SeqAct_Interp = SeqAct_Interp(FindObject(ObjectName, class'Object'));
  if (SeqAct_Interp == None)
  {
    return;
  }

  if (Data.GetIntValue("IsPlaying") == 1)
  {
    OldForceStartPosition = SeqAct_Interp.ForceStartPosition;
    OldbForceStartPos = SeqAct_Interp.bForceStartPos;

    // Play the matinee at the forced position
    SeqAct_Interp.ForceStartPosition = Data.GetFloatValue("Position");
    SeqAct_Interp.bForceStartPos = true;
    SeqAct_Interp.ForceActivateInput(0);

    // Reset the start position and start pos
    SeqAct_Interp.ForceStartPosition = OldForceStartPosition;
    SeqAct_Interp.bForceStartPos = OldbForceStartPos;
  }
  else
  {
    // Set the position of the matinee
    SeqAct_Interp.SetPosition(Data.GetFloatValue("Position"), true);
  }

  // Set the paused
  SeqAct_Interp.bPaused = (Data.GetIntValue("Paused") == 1) ? true : false;
}

KActor の実例


この実例では、KActor をどのようにセットアップすると、Save Game State システムを使用して自身をシリアライズおよびデシリアライズさせることができるかという点について説明します。記憶に留めておくべきことは、ロードまたは保存時において Save Game システムにアクタを自動的に使用させたい場合は、そのアクタにおいて SaveGameStateInterface が実装されていなければならないということです。

SaveGameStateKActor.uc
class SaveGameStateKActor extends KActor
  Implements(SaveGameStateInterface);

KActor をシリアライズする

位置と回転の値だけがここでは保存されます。パス名とオブジェクトのアーキタイプが必要となるデータです。これらがなければ、Save Game State システムは、データをどのアクタまたはオブジェクトに適用すべきか分からないことになります。あるいは、アクタまたはオブジェクトがインスタンス化される必要がある場合に、Save Game State システムがどのアクタまたはオブジェクトをインスタンス化すべきか分からないことになります。

そこで、位置が3 個の float 型で保存され、回転が 3 個の integer 型で保存されます。もちろん、必要に応じてさらに変数を保存することができます。JSon が選択された理由は、JSonObject::SetObject() 関数を使用して、親 - 子構造を作ることができるからです。したがって、このステップでは、子アクタまたは子オブジェクトに自身をシリアライズさせることもできます (これらのアクタまたはオブジェクトは、自身がシリアライズされたか否かを監視する方法をもつ必要があります。そうでなければ、アクタまたはオブジェクトが、一度ならずシリアライズまたはデシリアライズされることになります)。また、このステップでは、子アクタまたは子オブジェクトを親のデータセットとともに保存することができます。これによって、当然のことながら、付属しているアクタまたはオブジェクトを非常に簡単に扱えるようになります。基礎となる Save Game State システムコードベースを調整する必要がありません。

SaveGameStateKActor.uc
/**
 * Serializes the actor's data into JSon
 *
 * @return    JSon data representing the state of this actor
 */
function String Serialize()
{
  local JSonObject JSonObject;

  // Instance the JSonObject, abort if one could not be created
  JSonObject = new () class'JSonObject';
  if (JSonObject == None)
  {
    `Warn(Self$" could not be serialized for saving the game state.");
    return "";
  }

  // Serialize the path name so that it can be looked up later
  JSonObject.SetStringValue("Name", PathName(Self));

  // Serialize the object archetype, in case this needs to be spawned
  JSonObject.SetStringValue("ObjectArchetype", PathName(ObjectArchetype));

  // Save the location
  JSonObject.SetFloatValue("Location_X", Location.X);
  JSonObject.SetFloatValue("Location_Y", Location.Y);
  JSonObject.SetFloatValue("Location_Z", Location.Z);

  // Save the rotation
  JSonObject.SetIntValue("Rotation_Pitch", Rotation.Pitch);
  JSonObject.SetIntValue("Rotation_Yaw", Rotation.Yaw);
  JSonObject.SetIntValue("Rotation_Roll", Rotation.Roll);

  // Send the encoded JSonObject
  return class'JSonObject'.static.EncodeJson(JSonObject);
}

KActor をデシリアライズする

KActor が自身をデシリアライズするように要請される場合、KActor が自身ですでにシリアライズしていた JSon データが与えられます。これにより、単に反対のことを実行すれば、ゲームステートが保存された時点のステートに KActor を復帰させることができます。すでに見たように、子アクタまたは子オブジェクトをシリアライズさせる必要がある場合は、このステップでデータをデシリアライズさせるのが良いでしょう。

SaveGameStateKActor.uc
/**
 * Deserializes the actor from the data given
 *
 * @param    Data    JSon data representing the differential state of this actor
 */
function Deserialize(JSonObject Data)
{
  local Vector SavedLocation;
  local Rotator SavedRotation;

  // Deserialize the location and set it
  SavedLocation.X = Data.GetFloatValue("Location_X");
  SavedLocation.Y = Data.GetFloatValue("Location_Y");
  SavedLocation.Z = Data.GetFloatValue("Location_Z");

  // Deserialize the rotation and set it
  SavedRotation.Pitch = Data.GetIntValue("Rotation_Pitch");
  SavedRotation.Yaw = Data.GetIntValue("Rotation_Yaw");
  SavedRotation.Roll = Data.GetIntValue("Rotation_Roll");

  if (StaticMeshComponent != None)
  {
    StaticMeshComponent.SetRBPosition(SavedLocation);
    StaticMeshComponent.SetRBRotation(SavedRotation);
  }
}

プレイヤー制御のポーン例


プレイヤー制御のポーンは、関係するアクタのどれもがレベルデザイナーによって配置されないという興味深い例です。(すなわち、PlayerController または Pawn クラスのどちらもマップに配置されなかったということになります)。ただし、ポーンは、レベルデザイナーによってさまざまな目的のために配置されることがあります。たとえば、シングルプレーヤーゲームのためのマップに敵モンスターを配置するような場合です。そのような次第で、ここで取られた方法は、IsPlayerControlled という特別なフラグを保存するというものでした。Save Gameシステムによってポーンがインスタンス化されデシリアライズされる場合に、IsPlayerControlled が 1 にセットされていれば、デシリアライズのコードが GameInfo にそのことを知らせます。

SaveGameStatePlayerController.uc
/**
 * Serializes the actor's data into JSon
 *
 * @return    JSon data representing the state of this actor
 */
function String Serialize()
{
  local JSonObject JSonObject;

  // Instance the JSonObject, abort if one could not be created
  JSonObject = new () class'JSonObject';
  if (JSonObject == None)
  {
    `Warn(Self$" could not be serialized for saving the game state.");
    return "";
  }

  // Serialize the path name so that it can be looked up later
  JSonObject.SetStringValue("Name", PathName(Self));

  // Serialize the object archetype, in case this needs to be spawned
  JSonObject.SetStringValue("ObjectArchetype", PathName(ObjectArchetype));

  // Save the location
  JSonObject.SetFloatValue("Location_X", Location.X);
  JSonObject.SetFloatValue("Location_Y", Location.Y);
  JSonObject.SetFloatValue("Location_Z", Location.Z);

  // Save the rotation
  JSonObject.SetIntValue("Rotation_Pitch", Rotation.Pitch);
  JSonObject.SetIntValue("Rotation_Yaw", Rotation.Yaw);
  JSonObject.SetIntValue("Rotation_Roll", Rotation.Roll);

  // If the controller is the player controller, then saved a flag to say that it should be repossessed by the player when we reload the game state
  JSonObject.SetIntValue("IsPlayerControlled", (PlayerController(Controller) != None) ? 1 : 0);

  // Send the encoded JSonObject
  return class'JSonObject'.static.EncodeJson(JSonObject);
}

/**
 * Deserializes the actor from the data given
 *
 * @param    Data    JSon data representing the differential state of this actor
 */
function Deserialize(JSonObject Data)
{
  local Vector SavedLocation;
  local Rotator SavedRotation;
  local SaveGameStateGameInfo SaveGameStateGameInfo;

  // Deserialize the location and set it
  SavedLocation.X = Data.GetFloatValue("Location_X");
  SavedLocation.Y = Data.GetFloatValue("Location_Y");
  SavedLocation.Z = Data.GetFloatValue("Location_Z");
  SetLocation(SavedLocation);

  // Deserialize the rotation and set it
  SavedRotation.Pitch = Data.GetIntValue("Rotation_Pitch");
  SavedRotation.Yaw = Data.GetIntValue("Rotation_Yaw");
  SavedRotation.Roll = Data.GetIntValue("Rotation_Roll");
  SetRotation(SavedRotation);

  // Deserialize if this was a player controlled pawn, if it was then tell the game info about it
  if (Data.GetIntValue("IsPlayerControlled") == 1)
  {
    SaveGameStateGameInfo = SaveGameStateGameInfo(WorldInfo.Game);
    if (SaveGameStateGameInfo != None)
    {
      SaveGameStateGameInfo.PendingPlayerPawn = Self;
    }
  }
}

GameInfo::RestartPlayer() が呼び出されると、まず、ペンディング中のプレイヤーポーンが、プレイヤーコントローラを待っているか否かをチェックします。もしそうであれば、プレイヤーコントローラが代わりに与えられます。

SaveGameStateGameInfo.uc
/**
 * Restarts a controller
 *
 * @param    NewPlayer    Player to restart
 */
function RestartPlayer(Controller NewPlayer)
{
  local LocalPlayer LP;
  local PlayerController PC;

  // Ensure that we have a controller
  if (NewPlayer == None)
  {
    return;
  }

  // If we have a pending player pawn, then just possess that one
  if (PendingPlayerPawn != None)
  {
    // Assign the pending player pawn as the new player's pawn
    NewPlayer.Pawn = PendingPlayerPawn;

    // Initialize and start it up
    if (PlayerController(NewPlayer) != None)
    {
      PlayerController(NewPlayer).TimeMargin = -0.1;
    }

    NewPlayer.Pawn.LastStartTime = WorldInfo.TimeSeconds;
    NewPlayer.Possess(NewPlayer.Pawn, false);
    NewPlayer.ClientSetRotation(NewPlayer.Pawn.Rotation, true);

    if (!WorldInfo.bNoDefaultInventoryForPlayer)
    {
      AddDefaultInventory(NewPlayer.Pawn);
    }

    SetPlayerDefaults(NewPlayer.Pawn);

    // Clear the pending pawn
    PendingPlayerPawn = None;
  }
  else // Otherwise spawn a new pawn for the player to possess
  {
    Super.RestartPlayer(NewPlayer);
  }

  // To fix custom post processing chain when not running in editor or PIE.
  PC = PlayerController(NewPlayer);
  if (PC != none)
  {
    LP = LocalPlayer(PC.Player);

    if (LP != None)
    {
      LP.RemoveAllPostProcessingChains();
      LP.InsertPostProcessingChain(LP.Outer.GetWorldPostProcessChain(), INDEX_NONE, true);

      if (PC.myHUD != None)
      {
        PC.myHUD.NotifyBindPostProcessEffects();
      }
    }
  }
}

これによって、保存されたゲームステートがロードされたときに、プレイヤーが PlayerStart ではなく前にいたのと同じ位置に置かれるようになります。

Game State Loaded (ゲームステート ロード完了) Kismet イベント


Kismet のアクションを実行することによって、ゲームワールドを完全にリストアしなければならない場合があります。これは、カスタムのシーケンス イベントを作成することによって実現することができます。

SaveGameState_SeqEvent_SavedGameStateLoaded.uc
class SaveGameState_SeqEvent_SavedGameStateLoaded extends SequenceEvent;

defaultproperties
{
  ObjName="Saved Game State Loaded"
  MaxTriggerCount=0
  VariableLinks.Empty
  OutputLinks(0)=(LinkDesc="Loaded")
  bPlayerOnly=false
}

さらに、GameInfo::StartMatch() において save game state がロードされると、カスタムのシーケンス イベントがトリガーされます。

SaveGameStateGameInfo.uc
/**
 * Start the match - inform all actors that the match is starting, and spawn player pawns
 */
function StartMatch()
{
  local SaveGameState SaveGameState;
  local PlayerController PlayerController;
  local int Idx;
  local array<SequenceObject> Events;
  local SaveGameState_SeqEvent_SavedGameStateLoaded SavedGameStateLoaded;

  // Check if we need to load the game or not
  if (PendingSaveGameFileName != "")
  {
    // Instance the save game state
    SaveGameState = new () class'SaveGameState';
    if (SaveGameState == None)
    {
      return;
    }

    // Attempt to deserialize the save game state object from disk
    if (class'Engine'.static.BasicLoadObject(SaveGameState, PendingSaveGameFileName, true, class'SaveGameState'.const.SAVEGAMESTATE_REVISION))
    {
      // Load the game state
      SaveGameState.LoadGameState();
    }

    // Send a message to all player controllers that we've loaded the save game state
    ForEach WorldInfo.AllControllers(class'PlayerController', PlayerController)
    {
      PlayerController.ClientMessage("Loaded save game state from "$PendingSaveGameFileName$".", 'System');

      // Activate saved game state loaded events
      if (WorldInfo.GetGameSequence() != None)
      {
        WorldInfo.GetGameSequence().FindSeqObjectsByClass(class'SaveGameState_SeqEvent_SavedGameStateLoaded', true, Events);
        for (Idx = 0; Idx < Events.Length; Idx++)
        {
          SavedGameStateLoaded = SaveGameState_SeqEvent_SavedGameStateLoaded(Events[Idx]);
          if (SavedGameStateLoaded != None)
          {
            SavedGameStateLoaded.CheckActivate(PlayerController, PlayerController);
          }
        }
      }
    }
  }

  Super.StartMatch();
}

質問


子アクタまたは子オブジェクトはどのように扱われますか?

JSon が選択された理由は、JSonObject::SetObject() 関数を使用して、親 - 子構造を作ることができるからです。したがって、このステップでは、子アクタまたは子オブジェクトに自身をシリアライズさせることもできます (これらのアクタまたはオブジェクトは、自身がシリアライズされたか否かを監視する方法をもつ必要があります。そうでなければ、アクタまたはオブジェクトが、一度ならずシリアライズまたはデシリアライズされることになります)。また、このステップでは、子アクタまたは子オブジェクトを親のデータセットとともに保存することができます。これによって、当然のことながら、付属しているアクタまたはオブジェクトを非常に簡単に扱えるようになります。基礎となる Save Game State システムコードベースを調整する必要がありません。アクタまたはオブジェクトがデシリアライズするように求められた場合、内部の JSonObjects をイタレートして、同種のデシリアライズを実行することができます。

保存されたゲームステートがプレーンテキストとして保存されます! どうしたらプレイヤーのチート行為を防ぐことができるでしょうか?

JSon が選択されたもう一つの理由は、保存されたゲームステート ファイルを Notepad や他のテキストエディタで開き、とても簡単にデバッグすることができるからでした。ただし、バイナリで保存しない場合にチートを招く恐れがあることも分かります。

これについては、いくつかの考え方があります。テキストをかき混ぜる関数を使って暗号化した JSon を渡すことによって、データを分かりづらくすることができます。しかし、このようにしても結局は、保存されたゲームをハックしたがる人々によって解読されてしまうでしょう。たとえバイナリであってもこれから免れることはないでしょう。

結局のところ、情報源と保存データがどこに保存されているか(オンライン上)検証出来ない限り、不正行為を防ぐすべはほとんどありません。

JSon データをオンラインで保存することが可能ですか?

可能です。JSon は、プレーンテキストで互換性のあるデータ形式であるため、 TCPLink を使用してサーバーに送信することができるという利点があります。したがって、保存したゲームをオンライン上のどこかに保存しておき、クライントが別のマシーン上で引き出すことができます。あるいは、 異なる デバイス上からであってもデータを取得することが可能です。さらには、JSon データを読み込み、プレイヤーの進歩を表示するウェブサイトを開くことすら可能です。可能性はほぼ無限大です。

開発キット Gemはどうやって統合出来ますか!? 

SaveGameStateクラスからサブクラス化(もっとも簡単な方法)するか、SaveGameStateクラスから自分のゲームへコードをシフトします。ここで、ゲームが的確なPlayerControllerを使用するように、正しいゲームタイプを実行 しなければなりません 。さもなければ誤ったクラスが使用されコードが動きません。現在使用中のGameInfoとPlayerControllerは、「showdebug」コンソールコマンドで確認出来ます。コマンドを実行すると、使用中のGameInfoとPlayerControllerが画面の左上隅に表示されます。

インテグレートしたはずなのに、マップをロードしても何も起こりません!

忘れないでいただきたいことは、Save Game State にストリーミングレベルがあると、デフォルでは、サンプルコードが SaveGameStateGameInfo::StartMatch() と Super.StartMatch() [UTGame::StartMatch()] への遅延呼び出しを用いるということです。デフォルトで、bDelayedStart が false で、かつ、bWaitingToStartMatch が true である場合、GameInfo::StartMatch() は自動的に呼び出されます。ただし、このことがゲームにふさわしくない場合は、SaveGameStateGameInfo::StartMatch() を呼び出すことを忘れないでください。 SaveGameStateGameInfo::StartMatch() のコンテンツを移動することもできます。そこに置かれている主な理由は、save game state がロードされる前に、PlayerController がインスタンス化されなければならないからです。

関連テーマ


ダウンロード


  • スクリプトとサンプルマップは、 ここ からダウンロードすることができます。