Skip to content

Latest commit

 

History

History
748 lines (559 loc) · 30.2 KB

01-持久化对象.md

File metadata and controls

748 lines (559 loc) · 30.2 KB

Persisting Objects (创建、保存和加载)

原文链接:https://catlikecoding.com/unity/tutorials/object-management/persisting-objects/

  • 在按下一个键时生成随机的l立方体。
  • 使用泛型类型和虚拟方法。
  • 将数据写入文件并将其读取回来。
  • 保存游戏状态,以便以后加载。
  • 封装持久化数据的细节。

这是关于管理对象的系列教程的第一篇。它包括创建、跟踪、保存和加载简单的预制体实例。它建立在基础部分教程所奠定的基础上。 本教程使用Unity 2017.3.1p4制作。

这些立方体在游戏结束后保存了下来

  1. 按需创建对象 您可以在Unity编辑器中创建场景,并用对象实例填充它。这允许你为你的游戏设计固定的关卡。对象可以附加行为,这可以在游戏模式中改变场景的状态。通常,新的对象实例是在游戏过程中创建的。子弹被发射,敌人大量产生,随机的战利品出现,等等。玩家甚至可以在游戏中创建自定义关卡。 在游戏中创造新东西是一件事情。保存这一切,这样玩家就可以退出游戏,然后再回到游戏中去。Unity不会自动跟踪我们的游戏变化,我们必须自己去做。 在本教程中,我们将创建一个非常简单的游戏。它所做的就是在按下一个键时产生随机的立方体。一旦我们能够在游戏会话中跟踪立方体,我们就可以在以后的教程中增加游戏的复杂性。

1.1 游戏逻辑 因为我们的游戏非常简单,所以我们将使用一个游戏组件脚本来控制它。我们会用一个预制体来生成立方体。所以它应该包含一个公共字段来连接一个预制体实例。

using UnityEngine;
public class Game : MonoBehaviour {
	public Transform prefab;
}

添加一个游戏对象到场景中,并将此组件附加到场景中。然后创建一个默认的立方体,把它变成一个预制体,给游戏对象一个引用。

inspector

1.2 玩家输入 我们将会根据玩家的输入生成立方体,所以我们的游戏必须能够检测到这一点。我们将使用Unity的输入系统来检测按键。应该使用哪个键生成一个立方体?C键似乎是合适的,但我们可以通过检视面板将其配置为可配置的,方法是向Game中添加一个公共枚举字段。通过赋值定义字段时,使用C作为默认选项。

public KeyCode createKey = KeyCode.C;

设置c键

我们可以通过在Update方法中的查询静态输入类来检测键是否被按下。Input.GetKeyDown方法返回一个布尔值,它告诉我们当前帧中是否按下了某个特定的键。如果是,我们必须实例化我们的预制体。

void Update () {
		if (Input.GetKeyDown(createKey)) {
			Instantiate(prefab);
		}
	}

1.3 随机立方体 在游戏模式下,我们的游戏现在每次按下C键,或任何你配置它响应的键,都会生成一个立方体。但看起来我们只有一个立方体,因为它们都在同一个位置。让我们随机化我们创建的每个立方体的位置。 跟踪实例化的transform组件,这样我们就可以更改它的本地位置。使用Random.insideUnitSphere属性中获取一个随机点,将其放大到半径为5个单位,并将其作为最终位置。因为这比简单的实例化要复杂得多,所以将它的代码放在一个单独的CreateObject方法中,并在按下键时调用它。

void Update () {
		if (Input.GetKeyDown(createKey)) {
//			Instantiate(prefab);
			CreateObject();
		}
	}

	void CreateObject () {
		Transform t = Instantiate(prefab);
		t.localPosition = Random.insideUnitSphere * 5f;
	}

随机放置的立方体

立方体现在在一个球体内产生,而不是在完全相同的位置。它们仍然可以重叠,不过没关系。然而,它们都是对齐的,这看起来并不有趣。让我们给每个立方体一个随机旋转,我们可以用Random.rotation属性。

void CreateObject () {
		Transform t = Instantiate(prefab);
		t.localPosition = Random.insideUnitSphere * 5f;
		t.localRotation = Random.rotation;
	}

随机旋转

最后,我们还可以改变立方体的大小。我们用的是等比例的立方体,所以它们总是完美的立方体,只是大小不同。Random.Range可以用来获得在一定范围内的随机浮点数。让我们从小的0.1个立方体到一般大小的1个立方体。要将此值用于刻度的所有三个维度,只需乘以Vector3.one。然后分配结果到本地缩放。

void CreateObject () {
		Transform t = Instantiate(prefab);
		t.localPosition = Random.insideUnitSphere * 5f;
		t.localRotation = Random.rotation;
		t.localScale = Vector3.one * Random.Range(0.1f, 1f);
	}

随机均匀缩放

1.4 开始新游戏 如果我们想开始一个新游戏,我们必须退出游戏模式,然后再次进入。但这只可能在Unity编辑器中实现。玩家需要退出我们的应用程序并重新启动它才能玩一款新游戏。如果我们能在保持在运行时同时开始新游戏,那就更好了。 我们可以通过重新加载场景来开始新游戏,但这并不是必须的。我们可以销毁所有生成的数据集。我们使用另一个可配置的键,使用N作为默认值。

把开启新游戏按键设置为N

检查这个键是否在update中被按下,如果是,调用一个新的BeginNewGame方法。我们应该一次只处理一个键,所以如果没有按下C键,只检查N键。

	void Update () {
		if (Input.GetKeyDown(createKey)) {
			CreateObject();
		}
		else if (Input.GetKey(newGameKey)) {
			BeginNewGame();
		}
	}

	void BeginNewGame () {}

1.5 跟踪对象 我们的游戏可以产生任意数量的随机方块,这些方块都被添加到场景中。但游戏没有它所生成对象的记录。为了摧毁立方体,我们首先需要找到它们。为了实现这一点,我们将让Game脚本跟踪它实例化的对象的引用列表。 我们可以添加一个数组字段到游戏中,并用引用填充它,但是我们不知道会创建多少个立方体。幸运的是System.Collections.Generic命名空间包含一个我们可以使用的列表类。它的工作方式类似于一个数组,只是它的大小不是固定的。

using System.Collections.Generic;
using UnityEngine;

public class Game : MonoBehaviour {List objects;}

但我们不想要一个泛型的列表。我们特别需要transform引用的列表。事实上,List要求我们指定其内容的类型。List是泛型类型,这意味着它充当特定List类的模板,每个List类对应一个具体的内容类型。语法是List,其中模板类型T被添加到泛型类型中,位于尖括号之间。在我们的例子中,正确的类型是List。

List<Transform> objects;

与数组一样,在使用list对象实例之前,我们必须确保它是一个对象实例。我们将通过在Awake方法中创建新实例来实现这一点。对于数组,我们必须使用new Transform[]。但是因为我们使用的是一个列表,所以我们必须使用new list()。这将调用list类的特殊构造函数方法,该方法可以有参数,这就是为什么我们必须在类型名之后附加圆括号。

void Awake () {
		objects = new List<Transform>();
	}

接下来,在每次实例化一个新的transform引用时,通过list的add方法将transform引用添加到我们的列表中。

void CreateObject () {
		Transform t = Instantiate(prefab);
		t.localPosition = Random.insideUnitSphere * 5f;
		t.localRotation = Random.rotation;
		t.localScale = Vector3.one * Random.Range(0.1f, 1f);
		objects.Add(t);
	}

1.6 清空列表 现在我们可以BeginNewGame中循环遍历列表并销毁所有实例化的游戏对象。这与数组的工作原理相同,不同之处在于列表的长度是通过它的Count属性找到的。

void BeginNewGame () {
		for (int i = 0; i < objects.Count; i++) {
			Destroy(objects[i].gameObject);
		}
	}

这给我们留下了一个被销毁对象的引用列表。我们还必须通过调用它的Clear方法清空列表来消除这些问题。

void BeginNewGame () {
		for (int i = 0; i < objects.Count; i++) {
			Destroy(objects[i].gameObject);
		}
		objects.Clear();
	}
  1. 保存和加载

为了支持在单个运行会话期间保存加载,在内存中保存transform数据列表就足够了。复制保存时所有数据集的位置、旋转和比例,重置游戏并使用加载时记住的数据生成数据集。然而,真正的保存系统能够记住游戏状态,即使游戏已经终止。这需要将游戏状态持久化到游戏之外的某个地方。最直接的方法是将数据存储在文件中。

2.1 保存路径 游戏文件应该存储在哪里取决于文件系统。Unity为我们处理了这些差异,通过使用[Application](http://docs.unity3d.com/Documentation/ScriptReference/Application.html).persistentDataPath使我们使用的文件夹路径可用的。我们可以从这个属性获取文本字符串并将其存储在Awake中的savePath字段中,因此我们只需要检索一次。

string savePath;

	void Awake () {
		savePath = Application.persistentDataPath;
	}

这给了我们一个文件夹的路径,而不是一个文件。我们必须在路径后附加一个文件名。让我们使用saveFile,不需要文件扩展名。我们应该使用正斜杠还是反斜杠来将文件名与路径的其余部分分开,这同样取决于操作系统。我们可以利用Path.Combine方法为我们处理具体问题。Path是System.IO命名空间的一部分。

using System.Collections.Generic;
using System.IO;
using UnityEngine;

public class Game : MonoBehaviour {void Awake () {
		savePath = Path.Combine(Application.persistentDataPath, "saveFile");
	}}

2.2 打开文件进行写入

为了能够将数据写入我们的保存文件,我们首先必须打开它。这是通过File.Open方法完成的,为其提供一个路径参数。它还需要知道我们为什么要打开文件。我们希望向它写入数据,如果文件不存在,则创建该文件,或者替换已经存在的文件。我们通过提供FileMode来指定这一点。创建第二个参数。在新的Save方法中执行此操作。

void Save () {
		File.Open(savePath, FileMode.Create);
	}

File.Open返回一个文件流,它本身并不有用。我们需要一个可以写入数据的数据流。这些数据必须是某种格式的。我们将使用最紧凑的未压缩格式,即原始二进制数据。这个System.IO命名空间有BinaryWriter类来实现这一点。使用其构造函数方法创建这个类的新实例,并提供文件流作为参数。我们不需要保持对文件流的引用,所以我们可以直接使用该文件。作为参数的开放调用。我们确实需要保持对写的引用,所以将它分配给一个变量。

void Save () {
		BinaryWriter writer =
			new BinaryWriter(File.Open(savePath, FileMode.Create));
	}

现在,我们有一个名为writer的binary writer变量,它引用一个新的binary writer。那就是在一个表达中三次使用“writer”这个词,这有点过分了。当我们显式地创建一个新的BinaryWriter时,显式地声明变量的类型也是多余的。相反,我们可以使用var关键字。这将隐式声明变量的类型,以匹配立即分配给它的任何值,在本例中,编译器可以推断出这些值。

void Save () {
		var writer = new BinaryWriter(File.Open(savePath, FileMode.Create));
	}

现在,我们有一个引用新binary writer的binary writer变量。它的类型很明显。

2.3 关闭文件

如果我们打开一个文件,我们必须确保我们会关闭它。这是可能的,通过一个Close方法,但这是不安全的。如果在打开和关闭文件之间出现错误,可能会引发异常,并在关闭文件之前终止方法的执行。我们必须小心处理异常,以确保文件总是关闭的。有一种语法糖使这一切变得容易。将writer变量的声明和赋值放在圆括号内,将using关键字放在它前面,在它后面放一个代码块。该变量在该块中可用,就像标准for循环的迭代器变量i一样。

void Save () {
		using (
			var writer = new BinaryWriter(File.Open(savePath, FileMode.Create))
		) {}
	}

这将确保在代码执行之后,无论如何,都会正确地处理任何writer引用。这适用于特殊disposable类型,即writer和流。

如果没有语法糖,那么怎么做?

var writer = new BinaryWriter(File.Open(savePath, FileMode.Create);
try {}
finally {
	if (writer != null) {
		((IDisposable)writer).Dispose();
	}
}

2.4 写数据

我们可以通过调用writer的Write方法将数据写入文件。可以编写简单的值,如布尔值、整数等,一次一个。让我们从我们实例化了多少对象开始。

void Save () {
		using (
			var writer = new BinaryWriter(File.Open(savePath, FileMode.Create))
		) {
			writer.Write(objects.Count);
		}
	}

要实际保存这些数据,我们必须调用save方法。我们将再次通过键来控制它,在本例中使用S作为默认值。

public KeyCode createKey = KeyCode.C;
	public KeyCode saveKey = KeyCode.S;void Update () {
		if (Input.GetKeyDown(createKey)) {
			CreateObject();
		}
		else if (Input.GetKey(newGameKey)) {
			BeginNewGame();
		}
		else if (Input.GetKeyDown(saveKey)) {
			Save();
		}
	}

将S键设置为保存

进入游戏运行模式,创建几个立方体,然后按下S键保存游戏。这将在您的文件系统上创建一个saveFile文件。如果您不确定它的位置,您可以使用Debug.Log将文件的路径写入Unity控制台。 您将发现该文件包含四个字节的数据。在文本编辑器中打开文件不会显示任何有用信息,因为数据是二进制的。它可能什么也不显示,或者可能将数据解释为奇怪的字符。有四个字节,因为这是一个整数的大小。 除了编写我们有多少个立方体之外,我们还必须存储每个立方体的transform数据。我们通过遍历对象并写入它们的数据来实现这一点,每次写入一个数字。现在,我们只讨论他们的位置。所以把每个立方体位置的X, Y, Z分量按这个顺序写出来。

writer.Write(objects.Count);
			for (int i = 0; i < objects.Count; i++) {
				Transform t = objects[i];
				writer.Write(t.localPosition.x);
				writer.Write(t.localPosition.y);
				writer.Write(t.localPosition.z);
			}

包含7个位置的文件,以4字节块表示

2.5 加载数据

要加载我们刚刚保存的数据,我们必须再次打开文件,这次是使用FileMode .Open作为第二个参数。我们必须使用BinaryReader,而不是BinaryWriter。在新的Load方法中执行此操作,同样使用using语句。

void Load () {
		using (
			var reader = new BinaryReader(File.Open(savePath, FileMode.Open))
		) {}
	}

我们首先写入文件的是列表的count属性,所以这也是我们首先要读取的内容。我们使用reader的ReadInt32方法来做这件事。我们所读的内容必须是显式的,因为没有参数可以清楚地说明这一点。后缀32指的是整数的大小,它是4个字节,因此是32位。也有较大和较小的整数变体,但我们不使用它们。

using (
			var reader = new BinaryReader(File.Open(savePath, FileMode.Open))
		) {
			int count = reader.ReadInt32();
		}

在读取计数之后,我们知道保存了多少对象。我们必须从文件中读取许多位置。用一个循环来做这个操作,每个迭代读取三个浮点数,用于位置向量的X、Y和Z分量。使用ReadSingle方法读取单精度浮点数。使用ReadDouble方法读取双精度双精度表。

int count = reader.ReadInt32();
			for (int i = 0; i < count; i++) {
				Vector3 p;
				p.x = reader.ReadSingle();
				p.y = reader.ReadSingle();
				p.z = reader.ReadSingle();
			}

使用向量来设置新实例化的多维数立方体的位置,并将其添加到列表中。

for (int i = 0; i < count; i++) {
				Vector3 p;
				p.x = reader.ReadSingle();
				p.y = reader.ReadSingle();
				p.z = reader.ReadSingle();
				Transform t = Instantiate(prefab);
				t.localPosition = p;
				objects.Add(t);
			}

此时,我们可以重新创建我们保存的所有立方体,但是它们会被添加到场景中已经存在的立方体中。要正确加载之前保存的游戏,我们必须在重新创建游戏之前重置游戏。我们可以通过在加载数据之前调用BeginNewGame来做到这一点。

void Load () {
		BeginNewGame();
		using (
			var reader = new BinaryReader(File.Open(savePath, FileMode.Open))
		) {}
	}

当按下一个键时,使用L作为默认值,让游戏调用加载。

public KeyCode createKey = KeyCode.C;
	public KeyCode saveKey = KeyCode.S;
	public KeyCode loadKey = KeyCode.L;void Update () {else if (Input.GetKeyDown(saveKey)) {
			Save();
		}
		else if (Input.GetKeyDown(loadKey)) {
			Load();
		}
	}

将L键设置为加载

现在,玩家可以保存他们的多维立方体,并在以后加载它们,无论是在同一个游戏运行会话还是另一个。但是因为我们只存储位置数据,所以不存储立方体的旋转和比例。因此,加载的多维数据集都以预制块的默认旋转和缩放结束。

  1. 抽象存储

尽管我们需要知道读写二进制数据的细节,但这是相当底层的。写入单个3D向量需要三次写操作。在保存和加载对象时,如果我们能够在稍微高一点的层次上工作,使用一个方法调用读取或写入整个3D向量,就会更加方便。另外,如果我们可以只使用ReadInt和ReadFloat就好了,而不必担心我们不使用的所有不同的变量。最后,不管数据是以二进制、纯文本、base-64还是其他编码方法存储的。游戏不需要知道这些细节。

3.1 Game Data Writer和Reader

为了隐藏读写数据的细节,我们将创建自己的reader和writer类。让我们从writer开始,将其命名为GameDataWriter。 GameDataWriter不会继承MonoBehaviour,因为我们不会将它附加到游戏对象上。它将充当BinaryWriter的封装器,因此给它一个writer字段。

using System.IO;
using UnityEngine;

public class GameDataWriter {

	BinaryWriter writer;
}

可以通过new GameDataWriter()创建自定义writer类型的新对象实例。但这只有在我们有writer要写的时候才有意义。因此,使用BinaryWriter参数创建一个自定义构造函数方法。这是一个将其类的类型名作为自身名称的方法,它也充当其返回类型。它替换了隐式默认构造函数方法。

public GameDataWriter (BinaryWriter writer) {
	}

尽管调用构造函数方法会产生一个新的对象实例,但是这样的方法不会显式地返回任何东西。对象在调用构造函数之前创建,然后构造函数可以处理任何所需的初始化。在我们的例子中,这只是将writer参数分配给对象的字段。由于我对两个对象使用了相同的名称,所以必须使用this关键字来显式地表示我引用的是对象的字段而不是参数。

public GameDataWriter (BinaryWriter writer) {
		this.writer = writer;
	}

最基本的功能是编写一个浮点数或整型值。为此添加公共写方法,只需将调用转发给实际的writer。

public void Write (float value) {
		writer.Write(value);
	}

	public void Write (int value) {
		writer.Write(value);
	}

除此之外,还添加了一些方法来编写四元数(用于旋转)和Vector3。这些方法必须编写其参数的所有组件。对于四元数来说,它有四个分量。

    public void Write (Quaternion value) {
		writer.Write(value.x);
		writer.Write(value.y);
		writer.Write(value.z);
		writer.Write(value.w);
	}
	
	public void Write (Vector3 value) {
		writer.Write(value.x);
		writer.Write(value.y);
		writer.Write(value.z);
	}

接下来,使用与writer相同的方法创建一个新的GameDataReader类。在本例中,我们封装了一个BinaryReader。

using System.IO;
using UnityEngine;

public class GameDataReader {

	BinaryReader reader;

	public GameDataReader (BinaryReader reader) {
		this.reader = reader;
	}
}

给它简单命名为ReadFloat和ReadInt的方法,将调用转发给ReadSingle和ReadInt32。

    public float ReadFloat () {
		return reader.ReadSingle();
	}

	public int ReadInt () {
		return reader.ReadInt32();
	}

还创建ReadQuaternion和ReadVector3方法。读取它们的组件的顺序与我们写入它们的顺序相同。

	public Quaternion ReadQuaternion () {
		Quaternion value;
		value.x = reader.ReadSingle();
		value.y = reader.ReadSingle();
		value.z = reader.ReadSingle();
		value.w = reader.ReadSingle();
		return value;
	}

	public Vector3 ReadVector3 () {
		Vector3 value;
		value.x = reader.ReadSingle();
		value.y = reader.ReadSingle();
		value.z = reader.ReadSingle();
		return value;
	}

3.2 可持久化的对象

现在,在游戏中编写立方体的transform数据要简单得多。但我们可以更进一步。如果游戏可以简单地调用writer.Write(objects[i])呢?这将非常方便,但需要GameDataWriter了解编写游戏对象的细节。但最好保持writer的简单性,仅限于原始值和简单结构。 我们可以改变这种推理。游戏不需要知道如何保存游戏对象,那是对象本身的责任。对象需要的只是一个写入器来保存自己。然后游戏可以使用对象[i]. save (writer)。 我们的多个立方体是简单的对象,没有任何自定义组件。唯一要保存的是transform组件。让我们创建一个知道如何保存和加载数据的PersistableObject组件脚本。它只是扩展了MonoBehaviour,并具有一个带有GameDataWriter或GameDataReader参数的公共保存和加载方法。让它保存transform的位置、旋转和缩放,并以相同的顺序加载它们。

using UnityEngine;

    public class PersistableObject : MonoBehaviour {

	public void Save (GameDataWriter writer) {
		writer.Write(transform.localPosition);
		writer.Write(transform.localRotation);
		writer.Write(transform.localScale);
	}

	public void Load (GameDataReader reader) {
		transform.localPosition = reader.ReadVector3();
		transform.localRotation = reader.ReadQuaternion();
		transform.localScale = reader.ReadVector3();
	}
}

其思想是,一个可以持久化的游戏对象只有一个可持久化对象组件。拥有多个这样的组件毫无意义。我们可以通过向类添加DisallowMultipleComponent属性来实现这一点。

[DisallowMultipleComponent]
public class PersistableObject : MonoBehaviour {}

将这个组件添加到我们的立方体预制体中。

可持久化的预制体

3.3 持久存储

现在我们有了一个持久化对象类型,让我们创建一个PersistentStorage类来保存这样一个对象。它包含与Game相同的保存和加载逻辑,只是它只保存和加载一个持久对象实例,该实例通过一个参数提供给公共保存和加载方法。让它成为一个MonoBehaviour,这样我们就可以把它附加到一个游戏对象上,这样它就可以初始化它的保存路径。

using System.IO;
using UnityEngine;

public class PersistentStorage : MonoBehaviour {

	string savePath;

	void Awake () {
		savePath = Path.Combine(Application.persistentDataPath, "saveFile");
	}

	public void Save (PersistableObject o) {
		using (
			var writer = new BinaryWriter(File.Open(savePath, FileMode.Create))
		) {
			o.Save(new GameDataWriter(writer));
		}
	}

	public void Load (PersistableObject o) {
		using (
			var reader = new BinaryReader(File.Open(savePath, FileMode.Open))
		) {
			o.Load(new GameDataReader(reader));
		}
	}
}

添加一个新的带有此组件的游戏对象到场景中。它代表了游戏的持久存储。理论上,我们可以有多个这样的存储对象,用来存储不同的东西,或者提供对不同存储类型的访问。但是在本教程中,我们只使用这个文件存储对象。

存储对象

3.4 可持久化的Game

为了使用新的持久对象方法,我们必须重写Game。将prefab和objects内容类型更改为PersistableObject。调整CreateObject,使它能够处理这种类型的更改。然后删除所有特定于文件读写的代码。

using System.Collections.Generic;
//using System.IO;
using UnityEngine;

public class Game : MonoBehaviour {

	public PersistableObject prefab;List<PersistableObject> objects;

//	string savePath;

	void Awake () {
		objects = new List<PersistableObject>();
//		savePath = Path.Combine(Application.persistentDataPath, "saveFile");
	}

	void Update () {else if (Input.GetKeyDown(saveKey)) {
//			Save();
		}
		else if (Input.GetKeyDown(loadKey)) {
//			Load();
		}
	}void CreateObject () {
		PersistableObject o = Instantiate(prefab);
		Transform t = o.transform;
		…
		objects.Add(o);
	}

//	void Save () {
//		…
//	}

//	void Load () {
//		…
//	}
}

我们将让游戏依赖于一个持久化存储实例来处理存储数据的细节。添加一个这种类型的公共存储字段,这样我们就可以给游戏一个对我们存储对象的引用。为了再次保存和加载游戏状态,我们让游戏本身扩展了PersistableObject。然后,它可以加载和保存自己,使用存储。

public class Game : PersistableObject {public PersistentStorage storage;void Update () {
		if (Input.GetKeyDown(createKey)) {
			CreateObject();
		}
		else if (Input.GetKeyDown(saveKey)) {
			storage.Save(this);
		}
		else if (Input.GetKeyDown(loadKey)) {
			BeginNewGame();
			storage.Load(this);
		}
	}}

通过检检视面板连接存储器。还要重新连接预制块,因为字段的类型更改导致其引用丢失。

Game脚本连接的预制体和storage

3.5 重载方法

当我们现在保存并加载游戏时,我们最终会写入并读取我们的主游戏对象的transform数据。这是无用的。相反,我们必须保存并加载它的对象列表。

public void Save (GameDataWriter writer) {
		writer.Write(objects.Count);
		for (int i = 0; i < objects.Count; i++) {
			objects[i].Save(writer);
		}
	}

这还不足以让它起作用。编译器会报错那个游戏。Save隐藏继承的成员PersistableObject.Save。虽然游戏可以使用自己的保存版本,但PersistentStorage只知道PersistableObject.Save。它会调用这个方法,而不是Game中的那个。为了确保调用了正确的Save方法,我们必须显式地声明重写从PersistableObject继承的Game方法。这是通过向方法声明中添加override关键字来实现的。

public override void Save (GameDataWriter writer) {}

但是,我们不能重载任何我们喜欢的方法。默认情况下,我们不允许这样做。我们必须显式地启用它,方法是将virtual关键字添加到PersistableObject中的Save和Load方法声明中。

public virtual void Save (GameDataWriter writer) {
		writer.Write(transform.localPosition);
		writer.Write(transform.localRotation);
		writer.Write(transform.localScale);
	}

	public virtual void Load (GameDataReader reader) {
		transform.localPosition = reader.ReadVector3();
		transform.localRotation = reader.ReadQuaternion();
		transform.localScale = reader.ReadVector3();
	}

PersistentStorage现在会调用我们的Game.Save方法,即使它作为一个PersistableObject参数传递给它。也有Game重载的Load方法。

public override void Load (GameDataReader reader) {
		int count = reader.ReadInt();
		for (int i = 0; i < count; i++) {
			PersistableObject o = Instantiate(prefab);
			o.Load(reader);
			objects.Add(o);
		}
	}

包含两个transform的文件

参考文档

  1. MSDN中关于virtual的说明
  2. MSDN 中关于override关键字的说明