喵的Unity游戏开发之路 - 更多游戏状态:保存一切重要信息

如果丢失格式、图片或视频,请查看原文:https://mp.weixin.qq.com/s/iQAt4P_s3f5rRWj_yR8prg

很多童鞋没有系统的Unity3D游戏开发基础,也不知道从何开始学。为此我们精选了一套国外优秀的Unity3D游戏开发教程,翻译整理后放送给大家,教您从零开始一步一步掌握Unity3D游戏开发。 本文不是广告,不是推广,是免费的纯干货!本文全名:喵的Unity游戏开发之路 - 对象管理 - 更多游戏状态:保存一切重要信息

 

 

  • 跟踪随机性。

  • 保存关卡数据。

  • 遍历生成区域。

  • 创建旋转关卡的对象。

 

 

 

这是有关对象管理的系列教程中的第六篇。除了生成的形状和关卡索引之外,它还包括保存更多游戏状态。

本教程使用Unity 2017.4.4f1制作。

效果之一

 

 

随机形状的可复制轨迹。

 

 

 

 

保存随机性

 

生成形状时使用随机性的目的是获得不可预测的结果。但这并不总是可取的。假设您保存游戏,然后生成更多形状。然后,您加载并再次生成相同数量的形状。您最终应该得到完全相同的形状还是不同的形状?目前,您会得到不同的选择。但是另一个选项同样有效,因此让我们对其进行支持。

 

Unity的Random方法生成的数字并不是真正随机的。他们是伪随机的。它是由数学公式生成的数字序列。在游戏开始时,将根据当前时间使用任意种子值初始化此序列。如果使用相同的种子开始一个新序列,您将获得相同的数字。

 

 

 

写随机状态

 

存储初始种子值是不够的,因为那样会使我们回到序列的开始,而不是保存游戏时序列中的点。但是Random必须跟踪顺序。如果我们可以达到这种状态,那么我们以后可以恢复它,继续旧的顺序。

 

随机状态定义为嵌套在Random类内部的State结构。因此,我们可以声明此Random.State类型的字段或参数。为了保存它,我们必须向其中添加一个可以写入这样一个值的方法GameDataWriter。让我们现在添加此方法,但将其实现留待以后使用。

 

 

 

  •  

 

public void Write (Random.State value) {}

 

 

 

使用这种方法,我们可以保存游戏的随机状态。我们将Game.Save在写入形状计数之后的,开始时执行此操作。同样,增加保存版本以发出新格式的信号。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    const int saveVersion =3;
  2.  
  3.  
    public override void Save (GameDataWriter writer) { writer.Write(shapes.Count); writer.Write(Random.state); writer.Write(loadedLevelBuildIndex); }

 

 

 

 

 

读取随机状态

 

要读取随机状态,请向GameDataReader添加一个ReadRandomState方法。由于我们尚未编写任何内容,因此暂时不阅读任何内容。相反,返回当前的随机状态,因此没有任何变化。当前状态可以通过静态Random.state属性找到。

 

 

 

  •  
  •  
  •  
public Random.State ReadRandomState () {    return Random.state;  }

 

 

 

设置随机状态是通过相同的属性完成的,我们将在Game.Load中进行设置,但这仅适用于版本3及更高版本的保存文件。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    public override void Load (GameDataReader reader) { int count = version <= 0 ? -version : reader.ReadInt();
  2.  
    if (version >= 3) { Random.state = reader.ReadRandomState(); }
  3.  
    StartCoroutine(LoadLevel(version < 2 ? 1 : reader.ReadInt())); }

 

 

 

 

 

JSON序列化

 

Random.State包含四个浮点数。但是,它们不能公开访问,因此我们不可能简单地编写它们。我们必须使用间接方法。

 

幸运的是,它Random.State是可序列化的类型,因此可以使用Unity JsonUtility类的ToJson方法将其转换为相同数据的字符串表示形式。这给了我们一个JSON字符串。要查看其外观,请将其记录到控制台。

 

 

 

  •  
  •  
  •  
  public void Write (Random.State value) {    Debug.Log(JsonUtility.ToJson(value));  } 

 

 

 

 

JSON是什么?

正确的拼写是JSON,所有字母均为大写。它代表JavaScript对象表示法。它定义了一种简单的人类可读数据格式。

 

保存游戏后,控制台现在将在大括号之间记录一个字符串,该字符串包含四个名为s0s3的数字。类似于{“ s0”:-1409360059,“ s1”:1814992068,“ s2”:-772955632,“ s3”:1503742856}

 

我们将此字符串写入文件。如果要使用文本编辑器打开保存的文件,则可以在文件开头附近看到此字符串。

 

 

 

  •  
  •  
  •  
  public void Write (Random.State value) {    writer.Write(JsonUtility.ToJson(value));  } 

 

 

在ReadRandomState中,通过调用ReadString读取此字符串,然后用于JsonUtility.FromJson将其转换回适当的随机状态。

 

 

 

  •  
  •  
  •  
  public Random.State ReadRandomState () {    returnJsonUtility.FromJson(reader.ReadString());  } 

 

 

除了数据之外,FromJson还需要知道根据JSON数据创建的内容的类型。我们可以使用该方法的通用版本,指定应创建一个Random.State值。

 

 

 

  •  
  •  
  •  
  public Random.State ReadRandomState () {    return JsonUtility.FromJson<Random.State>(reader.ReadString());  } 

 

 

 

 

 

解耦关卡

 

现在,我们的游戏将保存并恢复随机状态。您可以通过开始游戏,保存,创建一些形状,然后加载并再次创建完全相同的形状来验证这一点。但是您可以走得更远。您甚至可以在加载后开始新游戏,并在此之后仍然创建相同的形状。因此,我们可以通过在新游戏之前加载游戏来影响新游戏的随机性。这是不可取的。理想情况下,不同游戏的随机性是分开的,就像我们重新启动整个游戏一样。每次开始新游戏时,我们都可以通过播种新的随机序列来实现这一目标。

 

要选择一个新的种子值,我们必须使用随机性。我们可以使用Random.value,但是必须确保这些值来自它们自己的随机序列。为此,请向Game添加一个主随机状态字段。在游戏开始时,将其设置为Unity初始化的随机状态。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    Random.State mainRandomState;
  2.  
  3.  
    void Start () { mainRandomState = Random.state; }

 

 

当玩家开始新游戏时,第一步是恢复主要随机状态。然后,通过该Random.InitState方法,获取一个随机值并将其用作种子以初始化新的伪随机序列。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    void BeginNewGame () { Random.state = mainRandomState; int seed = Random.Range(0, int.MaxValue); Random.InitState(seed);
  2.  
    }

 

 

为了使种子更加不可预测,我们将其与当前播放时间混合,可通过访问Time.unscaledTime。按位异或运算符^对此很有用。

 

 

 

  •  
    int seed = Random.Range(0, int.MaxValue)^ (int)Time.unscaledTime; 

 

 

 

 

异或的作用是什么?

对于每个位,如果两个输入之一恰好为1,另一个为0,则结果为1。否则,结果为0。换句话说,输入是否不同。因为有点操纵,结果在数学上并不明显,就像加法一样。

 

为了跟踪主要随机序列的进展,请在获取下一个值之后存储状态,然后再为新游戏初始化状态。

 

 

 

  •  
  •  
  •  
  •  
    Random.state = mainRandomState;    int seed = Random.Range(0, int.MaxValue);    mainRandomState = Random.state;    Random.InitState(seed); 

 

 

现在正在加载游戏,您在每个游戏中所做的事情将不再影响同一会话中其他游戏的随机性。但是要确保此方法正确运行,我们还必须为每个会话的第一个游戏进行BeginNewGame调用。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    void Start () {
  2.  
    BeginNewGame(); StartCoroutine(LoadLevel(1)); }

 

 

 

 

 

支持两种方法

 

您可能不希望可重现的随机性,而是希望在加载后获得新结果。因此,我们通过给Game添加reseedOnLoad切换来支持这两种方法。

 

 

 

  •  
[SerializeField] bool reseedOnLoad;

 

 

 

 

我们需要更改的只是加载游戏时是否设置了随机状态。我们可以继续保存和加载它,因此保存文件始终支持这两个选项。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    public override void Load (GameDataReader reader) {
  2.  
    if (version >= 3) { //Random.state = reader.ReadRandomState(); Random.State state = reader.ReadRandomState(); if (!reseedOnLoad) { Random.state = state; } }
  3.  
    }

 

 

 

 

 

 

序列化数据

 

我们可以保存游戏中产生的形状,可以保存正在玩的关卡,还可以保存随机状态。我们可以使用相同的方法来保存可比较的数据,例如产生和破坏了多少个形状,或者在播放时可以创建其他东西。但是,如果我们想保存关卡中某些内容的状态怎么办?我们在关卡场景中放了些东西,但是在玩耍过程中有变化吗?为了支持这一点,我们也必须保存关卡的状态。

 

 

 

当前关卡而不是游戏单例

 

要保存关卡,Game保存自身时必须包括该关卡。这意味着它必须以某种方式获得对当前水平的参考。我们可以向其中添加一个属性,Game并让已加载的关卡为其分配自身,但是接下来,我们将与关卡相关的两件事的知识直接放入内部Game:关卡本身及其生成区域。这可能是一种有效的方法,但让我们扭转一下。不必依赖Game单例,而是可以全局访问当前关卡。

 

向GameLevel中添加静态Current属性。每个人都可以获取当前关卡,但是只有关卡本身可以设置它,启用后即会执行此操作。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    public static GameLevel Current { get; private set; }
  2.  
  3.  
    void OnEnable () { Current = this; }

 

 

 

现在,无需设置游戏的生成点,关卡就可以公开其生成点供游戏使用。实际上,我们可以走得更远,GameLevel直接提供一个SpawnPoint属性,将请求转发到其生成区域。因此,该关卡充当其生成区域的立面。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    public Vector3 SpawnPoint { get { return spawnZone.SpawnPoint; } }
  2.  
  3.  
    //void Start () { // Game.Instance.SpawnZoneOfLevel = spawnZone; //}

 

 

这意味着Game不再需要了解生成区。它只是要求当前水平。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  //public SpawnZone SpawnZoneOfLevel { get; set; }  void CreateShape () {    t.localPosition =GameLevel.Current.SpawnPoint;  } 

 

 

 

在这一点上,GameLevel不再需要Game引用。由于静态实例未在其他任何地方使用,因此将其删除。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    //public static Game Instance { get; private set; }
  2.  
  3.  
    //void OnEnable () { // Instance = this; //}
  4.  
    void Start () { mainRandomState = Random.state; //Instance = this; }

 

 

 

 

Game.Instance即使不使用,我们也不能保留吗?

您可以,但是将未使用的代码(称为无效代码)留在项目中会使维护变得更加困难。它是如此简单的代码,如果将来需要它,我们将再次添加它。

 

 

 

 

序列化游戏关卡

 

为了保存该关卡,请将其设置为PersistableObject。关卡对象本身的转换数据没有用,因此现在覆盖SaveLoad方法,使其暂时不执行任何操作。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    public class GameLevel :PersistableObject{
  2.  
  3.  
    public override void Save (GameDataWriter writer) {}
  4.  
    public override void Load (GameDataReader reader) {}}

 

 

在Game.Save中,有意义的是在播放时创建的所有内容之前写入关卡数据。让我们直接将其放在关卡构建索引之后。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  public override void Save (GameDataWriter writer) {    writer.Write(shapes.Count);    writer.Write(Random.state);    writer.Write(loadedLevelBuildIndex);    GameLevel.Current.Save(writer);    for (int i = 0; i < shapes.Count; i++) {    }  } 

 

 

 

 

 

加载水平数据

 

加载时,我们现在必须在读取关卡构建索引之后读取关卡数据。但是,我们只能在加载了关卡场景之后才能这样做,否则我们将其应用于将要卸载的关卡场景。因此,我们必须推迟读取其余保存文件,直到LoadLevel协程完成为止。为了实现这一点,让我们将整个加载过程变成协程。

 

确认支持保存版本后,启动新的LoadGame协程,然后Game.Load结束。此后曾经使用的代码成为新的LoadGame协程方法,该方法需要读者作为参数。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    public override void Load (GameDataReader reader) { int version = reader.Version; if (version > saveVersion) { Debug.LogError("Unsupported future save version " + version); return; } StartCoroutine(LoadGame(reader)); }
  2.  
    IEnumerator LoadGame (GameDataReader reader) { int version = reader.Version; int count = version <= 0 ? -version : reader.ReadInt();
  3.  
    if (version >= 3) { Random.State state = reader.ReadRandomState(); if (!reseedOnLoad) { Random.state = state; } }
  4.  
    StartCoroutine(LoadLevel(version < 2 ? 1 : reader.ReadInt()));
  5.  
    for (int i = 0; i < count; i++) { int shapeId = version > 0 ? reader.ReadInt() : 0; int materialId = version > 0 ? reader.ReadInt() : 0; Shape instance = shapeFactory.Get(shapeId, materialId); instance.Load(reader); shapes.Add(instance); } }

 

 

LoadGame,替换为LoadLevel而不是调用StartCoroutine。之后,我们可以调用GameLevel.Current.Load,假设我们具有文件版本3或更高版本。

 

 

 

  •  
  •  
  •  
  •  
  •  
    //StartCoroutine(LoadLevel(version < 2 ? 1 : reader.ReadInt()));    yield return LoadLevel(version < 2 ? 1 : reader.ReadInt());    if (version >= 3) {      GameLevel.Current.Load(reader);    }

 

 

 

不幸的是,我们在尝试加载游戏时会出现错误。

 

 

 

 

缓冲数据

 

我们得到的错误告诉我们我们正在尝试从一个封闭的BinaryReader实例读取数据。由于using里面的障碍物,它关闭了PersistentStorage.Load。它保证了该方法调用完成后,我们对文件的保留将被释放。我们现在试图稍后通过协程读取关卡数据,因此它失败了。

 

有两种方法可以解决此问题。第一种是取消该using块,而是稍后通过显式关闭阅读器来手动释放对保存文件的保留。这将要求我们仔细跟踪我们是否要紧握阅读器并确保将其关闭,即使我们在途中遇到错误也是如此。第二种方法是一次性读取整个文件,对其进行缓冲,然后再从缓冲区中读取。这意味着我们不必担心释放文件,而只需要将其全部内容存储在内存中一段时间。由于我们的保存文件很小,因此我们将使用缓冲区。

 

可以通过调用File.ReadAllBytes读取整个文件,这将为我们提供一个字节数组。这将是我们在PersistentStorage.Load的新方法。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  public void Load (PersistableObject o) {    //using (    //  var reader = new BinaryReader(File.Open(savePath, FileMode.Open))    //) {    //  o.Load(new GameDataReader(reader, -reader.ReadInt32()));    //}    byte[] data = File.ReadAllBytes(savePath);  } 

 

 

我们仍然必须使用BinaryReader需要流而不是数组的。我们可以创建一个MemoryStream包装数组的实例,并将其提供给reader 。然后GameDataReader像以前一样加载。

 

 

 

  •  
  •  
  •  
  •  
  •  
  public void Load (PersistableObject o) {    byte[] data = File.ReadAllBytes(savePath);    var reader = new BinaryReader(new MemoryStream(data));    o.Load(new GameDataReader(reader, -reader.ReadInt32()));  } 

 

 

 

 

 

 

水平状态

 

我们已经可以保存关卡数据,但是目前我们还没有什么可存储的。因此,让我们提出一些要保存的东西。

 

 

 

顺序复合生成区

 

到目前为止,我们拥有的最复杂的关卡结构是复合生成区域。它具有一组生成区域,每次需要新的生成点时都会使用一个元素。在实际操作中,您无法预测下一个使用的区域。形状的放置是任意的,不需要统一,尽管从长远来看,它将平均分布在所有区域中。

 

 

我们可以通过依次遍历生成区域来更改此设置。两种方法都是有效的,因此我们将同时支持这两种方法。添加一个切换选项CompositeSpawnZone以使其顺序。

 

 

 

  •  
  •  
[SerializeField]  bool sequential;

 

 

 

 

顺序生成需要我们跟踪下一步必须使用哪个区域索引。因此,如果我们处于顺序模式,请添加一个nextSequentialIndex字段并将其用于索引中的SpawnPoint。之后增加字段。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    int nextSequentialIndex;
  2.  
    public override Vector3 SpawnPoint { get { int index; if (sequential) { index = nextSequentialIndex++; } else { index =Random.Range(0, spawnZones.Length); } return spawnZones[index].SpawnPoint; } }

 

 

为了使其循环,当我们经过数组的末尾时,跳回到第一个索引。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
      if (sequential) {        index = nextSequentialIndex++;        if (nextSequentialIndex >= spawnZones.Length) {          nextSequentialIndex = 0;        }      } 

 

 

顺序生成区的行为与随机生成区明显不同。尽管它们在每个区域中的位置仍然是随机的,但其生成模式清晰,形状在区域之间均匀分布。

 

 

 

 

 

记住下一个索引

 

保存游戏时,现在必须保存顺序复合生成区域的状态,否则序列将在加载后重置。因此,它必须成为可持久的对象。它已经扩展了SpawnZone,所以我们必须进行SpawnZone扩展PersistableObject。这使得所有生成区域类型都可以保留其状态。

 

 

 

  •  
  •  
  •  
  •  
  1.  
    public abstract class SpawnZone :PersistableObject{
  2.  
    public abstract Vector3 SpawnPoint { get; }}

 

 

覆盖CompositeSpawnZone的SaveLoad方法,只需读和写nextSequentialIndex即可。无论区域是连续的还是无序的,我们都会这样做。我们还可以调用基本方法,以保存区域的转换数据,但让我们仅关注序列。该区域不会自行移动。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    public override void Save (GameDataWriter writer) { writer.Write(nextSequentialIndex); }
  2.  
    public override void Load (GameDataReader reader) { nextSequentialIndex = reader.ReadInt(); }

 

 

 

 

 

 

跟踪持久对象

 

生成区域现在可以持久保存,但尚未保存。GameLevel必须调用它们的SaveLoad方法。我们可以简单地使用该spawnZone字段,但是只允许保存一个生成区域。如果我们想将多个顺序的生成区域放置在一个关卡(复合区域层次结构的所有部分)中,该怎么办?

 

我们可以使复合区域负责保存和加载它包含的所有区域,但是如果我们在应该保存的关卡上添加其他内容,该怎么办?为了使其尽可能灵活,让我们添加一种方法来配置保存关卡时应该保留的对象。最简单的方法是GameLevel在设计关卡场景时添加可以填充的持久对象数组。

 

 

 

  •  
  •  
[SerializeField]  PersistableObject[] persistentObjects;

 

 

 

现在GameLevel可以保存多少个这样的对象,然后像保存Game其形状列表一样保存每个对象。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  public override void Save (GameDataWriter writer) {    writer.Write(persistentObjects.Length);    for (int i = 0; i < persistentObjects.Length; i++) {      persistentObjects[i].Save(writer);    }  } 

 

 

加载过程也是如此,但是由于关卡对象是场景的一部分,因此无需实例化任何内容。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  public override void Load (GameDataReader reader) {    int savedCount = reader.ReadInt();    for (int i = 0; i < savedCount; i++) {      persistentObjects[i].Load(reader);    }  } 

 

 

请注意,从现在开始,您必须确保放入该数组的内容保持在同一索引下,否则将破坏与较早保存文件的向后兼容性。但是,您将来可以添加更多内容。加载旧文件时,这些新对象将被跳过,保留它们在场景中的保存方式。

 

另一个重要的一点是,我们所有场景中的GameLevel实例都没有自动获得新的数组。您必须打开并保存所有关卡场景,否则在加载关卡时可能会出现空引用异常。另外,我们可以检查在播放中启用关卡对象时是否存在数组。如果没有,请创建一个。如果您有多个关卡,这是一种更方便的方法,如果第三方为您的游戏创建了您仍然希望支持的关卡,则这是唯一的选择。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  void OnEnable () {    Current = this;    if (persistentObjects == null) {      persistentObjects = new PersistableObject[0];    }  } 

 

 

现在,我们可以通过将顺序组合生成区域显式添加到关卡的持久对象中来最终保存它。

 

 

 

 

 

重新加载新游戏

 

现在,在加载关卡时可以恢复序列索引,但是当玩家在同一关卡中开始新游戏时,它目前不会重置。解决方案是在这种情况下也加载关卡,从而重置整个关卡状态。

 

 

 

  •  
  •  
  •  
  •  
    else if (Input.GetKeyDown(newGameKey)) {      BeginNewGame();      StartCoroutine(LoadLevel(loadedLevelBuildIndex));    } 

 

 

 

 

 

旋转物体

 

让我们添加另一种也必须存储状态的关卡对象。一个简单的旋转对象。这是一个具有可配置角速度的持久对象。使用3D向量,因此速度可以沿任何方向。要使其旋转,请为其Update提供一个调用其转换方法Rotate的方法,并使用以时间增量缩放的速度作为参数。

 

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    using UnityEngine;
  2.  
    public class RotatingObject : PersistableObject {
  3.  
    [SerializeField] Vector3 angularVelocity;
  4.  
    void Update () { transform.Rotate(angularVelocity * Time.deltaTime); }}

 

 

 

为了演示旋转的对象,我创建了第四个场景。在其中,有一个根对象绕Y轴以90的速度旋转。它的唯一子对象是另一个绕X轴以15的速度旋转的对象。更深一层的位置是一个顺序复合生成区域,其中有两个球形生成区域子级。两个球体的半径均为1,并且在沿Z轴的两个方向上距原点十个单位。

 

 

要持久化关卡状态,必须将旋转对象和复合生成区域都放入持久对象数组中。它们的顺序无关紧要,但以后不应更改。

 

 

这种配置会在较大球体的相对两侧创建两个小生成区,围绕它们旋转并上下移动。

 

 

通过自动创建速度而不是手动生成形状,最容易看到它的实际效果。然后,您还可以测试保存和加载,以验证关卡状态确实确实存在并已还原。但是,有时我们可以得到不同的生成结果。我们将在下一部分中处理。

 

 

 

 

 

创造与破坏

 

自动创建和销毁过程也是游戏状态的一部分。我们目前尚未保存,因此创建和销毁进度不受保存和加载的影响。这意味着当创建速度大于零时,加载游戏后,您可能不会获得完全相同的形状放置。形状破坏的时间也一样。我们应该确保时间安排完全相同。

 

 

 

保存和加载

 

保存进度仅是在Game.Save中写入两个值的问题。在写入随机状态之后,让我们这样做。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  public override void Save (GameDataWriter writer) {    writer.Write(shapes.Count);    writer.Write(Random.state);    writer.Write(creationProgress);    writer.Write(destructionProgress);    writer.Write(loadedLevelBuildIndex);    GameLevel.Current.Save(writer);  } 

 

 

加载时,请在适当的时候将它们读回。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    IEnumerator LoadGame (GameDataReader reader) { int version = reader.Version; int count = version <= 0 ? -version : reader.ReadInt();
  2.  
    if (version >= 3) { Random.State state = reader.ReadRandomState(); if (!reseedOnLoad) { Random.state = state; } creationProgress = reader.ReadFloat(); destructionProgress = reader.ReadFloat(); }
  3.  
    yield return LoadLevel(version < 2 ? 1 : reader.ReadInt()); }

 

 

 

 

 

 

确切时间

 

我们仍然没有完全相同的时机。那是因为我们游戏的帧频不是很稳定。每个帧的时间增量是可变的。如果框架花费的时间比以前更长,那么足以早于上一次生成一个形状就足够了。否则可能会在以后显示一帧。结合基于相同时间增量的移动生成区,形状可能会终止于其他位置。

 

通过使用固定的时间增量来更新创建和销毁进度,我们可以使计时准确。这是通过将相关代码从Update方法移至新FixedUpdate方法来完成的。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    void Update () { if (Input.GetKeyDown(createKey)) { CreateShape(); } else { } }
  2.  
    void FixedUpdate () { creationProgress += Time.deltaTime * CreationSpeed; while (creationProgress >= 1f) { creationProgress -= 1f; CreateShape(); }
  3.  
    destructionProgress += Time.deltaTime * DestructionSpeed; while (destructionProgress >= 1f) { destructionProgress -= 1f; DestroyShape(); } }

 

 

现在,形状的自动创建和销毁不再受可变帧速率的影响。但是旋转器仍然是。为了使其完美,我们也应该使用FixedUpdate旋转RotatingObject

 

 

 

  •  
  •  
  •  
  voidFixedUpdate() {    transform.Rotate(angularVelocity * Time.deltaTime);  } 

 

 

 

 

FixedUpdate什么时候被调用?

 

它在Update之后的每个帧都被调用。调用多少次取决于帧时间和固定时间步长,您可以通过Edit / Project Settings / Time进行配置。

 

默认的固定时间步长为0.02,即每秒50次。因此,如果您的游戏每秒精确运行10帧,则每帧FixedUpdate将被调用五次。而且,如果您的游戏以每秒超过50帧的速度运行,那么有时FixedUpdate在一帧内根本不会被调用。如果需要更多或更少的时间粒度,则可以使用不同的时间步长。

 

在使用FixedUpdate物理引擎时,或者在需要可靠的可重现定时时,可以使用本教程中的情况。

 

 

 

 

 

速度设定

 

除了进度外,我们还可以考虑将速度设置作为游戏状态的一部分。我们要做的就是在保存时也写入速度属性。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  public override void Save (GameDataWriter writer) {    writer.Write(shapes.Count);    writer.Write(Random.state);    writer.Write(CreationSpeed);    writer.Write(creationProgress);    writer.Write(DestructionSpeed);    writer.Write(destructionProgress);    writer.Write(loadedLevelBuildIndex);    GameLevel.Current.Save(writer);    for (int i = 0; i < shapes.Count; i++) {      writer.Write(shapes[i].ShapeId);      writer.Write(shapes[i].MaterialId);      shapes[i].Save(writer);    }  } 

 

 

并在加载时阅读它们。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
    if (version >= 3) {      Random.State state = reader.ReadRandomState();      if (!reseedOnLoad) {        Random.state = state;      }      CreationSpeed = reader.ReadFloat();      creationProgress = reader.ReadFloat();      DestructionSpeed = reader.ReadFloat();      destructionProgress = reader.ReadFloat();    } 

 

 

在开始新游戏时重置速度也很有意义。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    void BeginNewGame () { Random.InitState(seed);
  2.  
    CreationSpeed = 0; DestructionSpeed = 0;
  3.  
    }

 

 

 

 

 

更新标签

 

现在,速度设置已保存,并在我们加载游戏时恢复。但是UI并没有意识到这一点,因此如果我们碰巧加载了不同的速度,则不会改变。加载后,我们必须手动刷新滑块。为此,Game需要引用滑块,因此为它们添加两个配置字段。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    using UnityEngine.UI;
  2.  
    [DisallowMultipleComponent]public class Game : PersistableObject {
  3.  
  4.  
    [SerializeField] Slider creationSpeedSlider; [SerializeField] Slider destructionSpeedSlider; }

 

 

 

 

 

没有办法将UI绑定到属性吗?

没有内置的方法可以做到这一点。我们可以提出一个自定义解决方案,但这超出了本教程的范围。对于我们的简单情况,滑块引用就足够了。

 

重置速度时,我们现在还可以通过分配滑块的value属性来更新滑块。

 

 

 

  •  
  •  
  •  
  •  
    CreationSpeed = 0;    creationSpeedSlider.value = 0;    DestructionSpeed = 0;    destructionSpeedSlider.value = 0;

 

 

 

通过链接分配,可以使该代码更简洁。

 

 

 

  •  
  •  
  •  
  •  
    //CreationSpeed = 0;    creationSpeedSlider.value =CreationSpeed = 0;    //DestructionSpeed = 0;    destructionSpeedSlider.value =DestructionSpeed = 0; 

 

 

Load方法中执行相同的操作。

 

 

 

  •  
  •  
  •  
  •  
creationSpeedSlider.value =CreationSpeed = reader.ReadFloat();      creationProgress = reader.ReadFloat();      destructionSpeedSlider.value =DestructionSpeed = reader.ReadFloat();      destructionProgress = reader.ReadFloat(); 

 

 

现在,在加载或开始新游戏后,UI也会更新。

 

下一个教程是配置形状:各种随机

资源库(Repository)

https://bitbucket.org/catlikecodingunitytutorials/object-management-06-more-game-state


往期精选

Unity3D游戏开发中100+效果的实现和源码大全 - 收藏起来肯定用得着

S‍‍‍‍hader学习应该如何切入?

喵的Unity游戏开发之路 - 从入门到精通的学习线路和全教程‍‍‍‍


声明:发布此文是出于传递更多知识以供交流学习之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与我们联系,我们将及时更正、删除,谢谢。

原作者:Jasper Flick

原文:

https://catlikecoding.com/unity/tutorials/object-management/more-game-state/

翻译、编辑、整理:MarsZhou


More:【微信公众号】 u3dnotes

posted @ 2020-09-18 09:21  MarsZhou  阅读(250)  评论(0编辑  收藏  举报