UDN
Search public documentation:

DevelopmentKitGemsSaveGameStatesCH
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 主页 > 虚幻开发工具包精华文章 > 保存游戏状态
UE3 主页 > 输入 / 输出 > 保存游戏状态

保存游戏状态


最后一次测试是在2012年3月份的UDK版本上进行的

可以与 PC 兼容
可以与Mac机兼容
可以同iOS平台兼容

概述


虚幻引擎1和2都支持将整关卡从内存保存到磁盘中的游戏存储方式。虽然这个方法在制作过程中运行良好,但是任何内容的开发者稍后对关卡所进行的修改都不会在关卡中反应。要解决这个问题,另一个呈现已存游戏的方法就是只保存足够的数据,即已存游戏要在初次加载关卡时存储,然后将已存游戏状态应用到所有在关卡中的actor和对象上。

开发游戏时要记住已存游戏状态这个概念


由于已存游戏状态运行方式的关系,您必须特别留意您在世界中所销毁的是什么。一旦销毁了一个actor,已存游戏序列化工具就不能识别这个ator,因为它现在已经不存在了。如果这个actor是临时的,通常来说不是问题。但是如果actor是关卡设计师摆放的,那么重新加载关卡并应用已存游戏数据时,关卡设计师所放置的actor不会受到影响,因为actor的数据已经不存在了。

已存游戏状态工作方式的大致流程


玩家在运行关卡一段时间后保存游戏

为了举例方便,我们只在这篇精华文章中添加一个控制台命令。当然,您的游戏会拥有一个附加在游戏里的图解用户界面,不过您也可以用任何方法调用带有文件名参数的同一个控制台命令。或者可以把控制台命令函数设置成静态的,因为它并不依赖于在一个特定的actor或对象实例范围中执行(它调用PlayerController::ClientMessage(),但是您总能通过使用Actor::GetALocalPlayerController()来获取本地玩家控制器)。

当执行控制台命令时,它启动了保存游戏状态的进程。首先 SaveGameState(保存游戏状态)对象已经被实例化。SaveGameState(保存游戏状态)对象处理actor、Kismet和Matinee的迭代和序列化,。然后我们"检查清理"文件名。虽然在这个事例中只会检测空格, 但是获取文件名只是为了确保其中没有加入任何非法字符。如果需要更强大的文件名获取功能,您可能需要考虑确保像 \, /, ?, ! 这样的字符 不会 出现在文件名中。文件名获取功能同样保证了文件扩展名"sav"会在其还不存在的情况下自动加入。接下来SaveGameState(保存游戏状态)需要迭代和序列化Actor、Kismet和Matinee。最后,如果 SaveGameState(保存游戏状态)通过BasicSaveObject()成功保存在磁盘中,那么玩家会收到游戏已经被保存的信息。

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()会为您执行将真实数据存到磁盘里那样。如果任何动态加载关卡可见,或要求暂停加载,那么它们就会被存入一个数组,这样当重新加载已存游戏状态,动态加载关卡就会被直接载入。这个步骤同时还会保存当前GameInfo类。

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));
      }
    }
  }

  // 保存游戏info类
  GameInfoClassName = PathName(WorldInfo.Game.Class);
}

将所有实现SaveGameStateInterface 的actors序列化为JSon

只需序列化动态actor,这样在此可供选择的迭代器就是DynamicActors(动态actor)了。还要添加一个SaveGameStateInterface(已存游戏状态界面)的筛选器,因为它能让您决定哪些动态actor需要进行序列化,哪些不需要。在这里使用一个接口,这样稍后扩展已存游戏状态更加容易,因为它是一个稍后会序列化和反序列化JSon数据的actor。当实现这个actor时, 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));
      }
    }
  }

  // 保存游戏info类
  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非常简洁。该接口有两个函数,每个actor都必须要实现这两个函数。Serialize()在加载时序列化该actor所有的数据。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

存储游戏状态也能序列化Kismet事件和Kismet变量。这让游戏策划能使用Kismet来实现一部分游戏功能。它通过在关卡中迭代Kismet事件和Kismet变量并序列化每一个kismet事件和kismet变量来完成。

Kismet事件把它们的ActivationTime(激活时间)用偏移量方式来计算。当已存游戏状态重新加载时, WorldInfo.TimeSeconds总是在0或者很小的数字上。这不太可能是先前游戏保存的时间。如果kismet事件已经设置了它的ReTriggerDelay(重新触发延迟)变量,那么ActivationTime(激活时间)通常是很重要的。这样是为了避免由于保存和加载导致在Kismet事件重新触发太快的地方会有bug出现,这要求在计算ActivationTime 的剩余时间时考虑ReTriggerDelay 。这样当重新加载该kismet事件,如果已经触发该事件,则通常会在以后设置ActivationTime。另一个已经保存的数值是TriggerCount。这通常要求触发器把它们 MaxTriggerCount(最大触发量)的数值设置为非零。

Kismet变量通过使用类型反转和错误方式进行侦测。还有一种选择就是迭代 Kismet Sequence Objects (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 Sequence Action(Matinee Kismet序列动作)。由此,迭代所有 Kismet Sequence Objects(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保存数据

如前所示,保存游戏状态的数据是通过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))
  {
  }
}

加载地图,地图后面附加命令行来存储已存游戏状态的文件名称

如果已经成功地加载了已存游戏状态,那么就可以加载序列化的地图,该地图具有命令行参数,该参数指出当该地图加载完成时应该继续加载所定义的已存游戏状态 如果您决定使这个函数成为静态函数,您可以从其他全局可引用actor处调用ConsoleCommand()。

ALERT! 注意: 在这里使用命令"start",而不是"open",因为"start"总是重置命令行参数,而"open"' 附加 命令行参数。这点非常重要,否则命令行参数"SaveGameState" 会进行多次添加,从而导致已存游戏状态加载错误。

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()会判断出一个存储游戏状态命令行参数是否存在。如果存在,则在 PendingSaveGameFileName里保存数值。当开始进行匹配,则再次从磁盘中加载已存游戏状态对象,并要求加载游戏状态。当加载了已存游戏状态时,玩家会收到信息,提示他们已存游戏已经加载。如果有任何动态载入关卡存在,则 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数据并在关卡中的actor和对象上对数据进行反序列化。

既然已存游戏状态对象已经完成加载,那就可以迭代实现SaveGameStateInterface的actor、kismet和Matinee,并基于存储在SerializedWorldData数组(这个数组现在已经编码成JSon格式)中恢复它们。

当SerializedWorldData迭代时,每个元素都会被解码成JSonObject. 获得其名称将会使您对JsonObject数据的关联内容有深入了解。针对SeqAct_Interp进行测试可以知道 这个数据是同Matinee对象、Kismet 事件或Kismet 变量的SeqEvent或SeqVar相关的。如果这三个均不相关,那么它在这个世界中只能和一个actor相关。

如果JSonObject数据在世界中针对一个actor,那么通过使用 FindObject()函数可以找到这个actor。在存储了actor完整的路径名称之后, FindObject()应该就能找到关卡设计师所放置的任何一个actor。如果FindObject()失败,那么它一定是在运行中被实例化的一个actor。这就是为什么存储ObjectArchetype也十分有用的原因,这样如果需要的话,已存游戏状态就能对它重新实例化。一旦actor被找到或被实例化,这个actor就会被转换成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 的方法和反序列化Actors基本一样,但有一点例外是如果找不到kismet序列对象的话,虚幻脚本不会对它进行实例化。一旦通过使用 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会在 JSonObject中保存为1。这样ForceStartPosition完成设置,然后要求运行Matinee。否则Matinee会将它的位置根据在JSonObject中所保存的位置数值进行设置。

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示例


这个示例向您展示如何设置一个actor来使用草村游戏状态系统对其自身进行序列化和反序列化。请记住,对任何您想让保存游戏系在加载或保存时自动识别的Actor类来说,您需要实现SaveGameStateInterface。

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

序列化KActor

只有位置和旋转度数值才能在此保存。路径名称和对象原型必要数据,否则 Save Game State System(保存游戏状态系统)不能识别对哪个actor或对象应用数据,或者,如果actor或对象需要被实例化,Save Game State System(保存游戏状态系统)将不能识别对哪个actor或者对象原型进行实例化。

所以位置保存为三个浮点型值、旋转度保存为三个整型值。您也可以根据要求保存更多的变量。选择JSon的一个原因是您可以使用可以JSonObject::SetObject()函数创建 父项—子项 结构。这样,您也可以在这步中让子actor或对象序列化其自身(确保这些Actors或对象有一种方法来跟踪它们是否已经序列化;因为您不想多次重复地 序列化/反序列化 这些对象)。这自然创造了一个非常简单的方法来处理附加的actor或对象,而不需要去修改 Save Game State System(保存游戏状态)的底层代码

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恢复到它原先的状态。如上所述,如果您希望序列化子actor或子对象,在此就会有一个合适的地方对该数据进行反序列化。

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);
  }
}

玩家控制的Pawn事例


玩家所操控的pawn在关卡设计师没有放置相关actor的地方是十分有趣的事例,即地图中既没有放置PlayController(玩家控制器)也没有Pawn类。但是,关卡设计师能以不同的目的把pawn放置在地图中,比如把敌对怪放在单人地图中。在此用这个方法是要保存一个叫做IsPlayerControlled的额外标记。这样当保存游戏系统实例化并反序列化pawn时,如果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()时,它首先确认是否有等待中的玩家pawn在等待玩家控制器。如果有,则得到玩家控制器。

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。

游戏状态加载的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()中加载保存游戏状态时出发自定义Sequence Event(序列事件)

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();
}

问题


如何处理子actor或子对象

选择JSon的一个原因是您可以使用可以JSonObject::SetObject()函数创建 父项—子项 结构。这样,您也可以在这步中让子actor或对象序列化其自身(确保这些Actors或对象有一种方法来跟踪它们是否已经序列化;因为您不想多次重复地 序列化/反序列化 这些对象)。这自然创造了一个非常简单的方法来处理附加的actor或对象,而不需要去修改 Save Game State System(保存游戏状态)的底层代码 当要求反序列化这个actor或对象时,您可以迭代内部的JSon对象并执行同样的反序列化操作。

已存游戏状态以纯文本形式存储。我怎样制止玩家作弊?

另一个选择JSon的原因是,只要在Notepad或其他种类的文本编辑软件中打开已存游戏状态的文件,就能很容易地对其进行调试。但是可以理解的是,没有以二进制的方式存储可能会有作弊的行为发生。

对这个问题,这里还有一些其他的思路。您可以通过文本乱码功能经过JSon加密对数据进行模糊处理。但即便是这样,那些真正想窃取您所存游戏的人最终也会得到解码。甚至连二进制文件也不会幸免。

因此,一天结束之后;您几乎无法阻止使用秘籍;除非您可以指定信息源,并指定保存数据所存储的地方(联机保存)。

是否有可能在线存储JSon数据?

是的。使用JSon的一个优点就是,它是一种可交互形式数据的纯文本,它能通过TCPLink.传输到服务器。这样已存游戏就能在某地在线存储,而用户就能在不同的机器,或在 不同 的设备上取回数据。或者您甚至可以有一个识别和显示JSon数据的网站,它能显示用户对此的进展。现实当中的可能性是无穷的。

怎样集成这个开发工具精华文章!?

您可以从SaveGameState类继承(最简单),或者您可以在SaveGameState类中将代码转换为您自己的游戏。记住,您 必须 运行的是正确的游戏类型,这样游戏才会使用争取的PlayerController;否则所有代码都会因为使用了错误的类而无法正常工作。要查看当前所使用的是哪个GameInfo和PlayerController,请使用 "showdebug" 控制台命令。它将会在屏幕的左上角显示当前所使用的GameInfo和PlayerController。

我已经进行集成该功能,但是当我加载地图时什么都没发生!

请记住,在默认状态下,当已存游戏状态有动态加载关卡,事例代码使用SaveGameStateGameInfo::StartMatch()及到Super.StartMatch()的延迟调用。当 bDelayedStart为false(假值)并且bWaitingToStartMatch默认为true(真值),则自动调用GameInfo::StartMatch()。但是,如果这同您的游戏不匹配,那么请调用SaveGameStateGameInfo::StartMatch()。您也可以移除SaveGameStateGameInfo::StartMatch()的内容,因为它在这里的主要原因是保存游戏状态需要PlayerController(玩家控制器)在保存游戏状态加载前被实例化。

相关主题


下载


  • 下载脚本及示例地图。