Catlike Coding Unity教程系列 中文翻译 Object Management篇(四)Multiple Scenes

多重场景

加载关卡

原文地址:https://catlikecoding.com/unity/tutorials/object-management/multiple-scenes/

在播放模式下创建一个场景。
在场景之间移动物体。
使用多个场景。
支持游戏的关卡。

这是关于Object Management的系列教程的第4部分。它是关于将对象放在它们自己的场景中,同时处理多个场景,以及加载和卸载场景。

本教程使用Unity 2017.4.4f1制作。

请添加图片描述

不同的关卡,不同的时间。

1. 池场景

当在播放模式下实例化许多形状时,场景会迅速充满对象,层级窗口会变得非常混乱。这可能会使查找特定对象变得困难,也可能会降低编辑器的速度。

请添加图片描述

播放模式下的单场景。

我们可以通过折叠场景层次结构或移除层次窗口来防止潜在的编辑器减速,但这样我们就再也看不到对象了。理想情况下,我们可以将所有形状实例折叠为层次结构窗口中的单个条目,而其他所有东西都保持可见。有两种方法可以做到这一点。

第一个选项是创建一个根对象,并使该对象的所有形状都成为子对象。然后我们可以折叠根对象。不幸的是,当形状发生改变时,这可能会对我们的游戏性能产生负面影响。每当一个对象的活动状态或转换状态发生变化时,它的所有父对象都会被通知此更改。因此,最好避免使对象成为另一个对象的子对象,如果这不是严格必要的。

第二种选择是将所有形状放在一个单独的场景中。它们仍然是没有父对象的根对象,但成为额外场景的一部分,可以在层次窗口中折叠。场景并不关心对象的状态,所以这并不会降低游戏的速度。这是我们将要使用的选项。

1.1 在游戏中创造一个场景

我们需要一个专门的场景来包含形状实例。因为形状实例只存在于播放模式中,场景也只需要在我们处于播放模式时存在。我们将通过代码创建一个,而不是通过编辑器。

ShapeFactory负责创建、销毁和回收形状,因此它也应该负责保存它们的场景。为了直接处理场景,它需要从UnityEngine.SceneManagement命名空间访问代码,因此在ShapeFactory类文件中使用它。

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

[CreateAssetMenu]
public class ShapeFactory : ScriptableObject {}

我们将创建一个池场景,以包含所有可以回收的形状实例。所有工厂的形状都进入这个池,永远不应该从它删除。我们可以使用Scene字段来记录这个池场景。

	Scene poolScene;

我们只需要在启用回收时的场景。在不回收时,管理实例留给请求它们的人。所以我们只需要在需要池时创建一个场景。因此,在CreatePools的末尾调用SceneManager.CreateScene创建一个新场景并记录它。场景需要一个名称,我们可以简单地使用工厂的名称。如果你使用多个工厂,它们都会有自己的场景,所以确保给每个工厂一个唯一的名称。

	void CreatePools () {
		…
		poolScene = SceneManager.CreateScene(name);
	}

请添加图片描述

形状工厂池场景。

现在,当我们在播放模式中第一次创建一个形状时,就会出现一个Shape Factory场景,尽管形状还没有放入其中。当我们停止播放时,场景就消失了。

1.2 在池场景中放置对象

当一个游戏对象被实例化时,它被添加到活动场景中。在我们的例子中,活动场景是Scene,我们项目中唯一持久的场景。改变活动场景是可能的,但我们不希望工厂扰乱场景。相反,我们可以在创建形状之后,通过调用SceneManager.MoveGameObjectToScene将它们迁移到池场景中,使用游戏对象和场景作为参数。

	public Shape Get (int shapeId = 0, int materialId = 0) {
		Shape instance;
		if (recycle) {if (lastIndex >= 0) {}
			else {
				instance = Instantiate(prefabs[shapeId]);
				instance.ShapeId = shapeId;
				SceneManager.MoveGameObjectToScene(
					instance.gameObject, poolScene
				);
			}
		}}

请添加图片描述

形状放入池场景。

从现在开始,形状被整齐地放在Shape Factory场景中,你可以在层次窗口中折叠,或者在你想看的时候打开。

1.3 从重新编译中恢复

至少在构建或只要我们保持在游戏模式,工厂运行良好。不幸的是,在播放模式下重新编译会打乱我们的回收和池场景。

虽然Unity在编译时序列化MonoBehaviour类型的私有字段,但它不会对ScriptableObject类型这样做。这意味着在重新编译之后,池列表将丢失。这样做的结果是,CreatePools将在重新编译后再次被调用。

难道我们不能将pools标记为Serializable吗?

这将使Unity保存池作为资产的一部分,在编辑器播放会话之间保存它,并在构建中包含它。这不是我们想要的。

第一个明显的问题是,我们试图再次创建池场景,这将失败,因为具有该名称的场景已经存在。我们可以通过使用Scene.isLoaded属性检查池场景是否已经加载来防止这种情况的发生。如果是,我们在创建场景之前中止。

	void CreatePools () {if (poolScene.isLoaded) {
			return;
		}

		poolScene = SceneManager.CreateScene(name);
	}

这似乎不起作用。这是因为Scene是一个结构体,而不是对实际场景的直接引用。因为它是不可序列化的,重新编译会将结构重置为它的默认值,这表示一个未加载的场景。我们必须通过SceneManager.GetSceneByName方法请求一个新的连接。

		poolScene = SceneManager.GetSceneByName(name);
		if (poolScene.isLoaded) {
			return;
		}
		
		poolScene = SceneManager.CreateScene(name);

这是有效的,但我们只需要在Unity编辑器中执行这一操作,而不是在构建中。我们可以通过Application.isEditor属性检查我们是否在编辑器中。

		if (Application.isEditor) {
			poolScene = SceneManager.GetSceneByName(name);
			if (poolScene.isLoaded) {
				return;
			}
		}

		poolScene = SceneManager.CreateScene(name);

第二个稍微不那么明显的问题是,在重新编译之前不活动的形状实例永远不会重用。那是因为我们丢失了记录他们的列表。我们可以通过重新填充列表来解决这个问题。首先,通过Scene.GetRootGameObjects方法检索一个包含池场景的所有根游戏对象的数组。

		if (Application.isEditor) {
			poolScene = SceneManager.GetSceneByName(name);
			if (poolScene.isLoaded) {
				GameObject[] rootObjects = poolScene.GetRootGameObjects();
				return; 
			}
		}

接下来,遍历所有对象并获取它们的形状组件。因为这是工厂场景,它应该只包含形状,所以我们总是得到一个组件。在此之后出现空引用错误将表明其他地方有问题。

			if (poolScene.isLoaded) {
				GameObject[] rootObjects = poolScene.GetRootGameObjects();
				for (int i = 0; i < rootObjects.Length; i++) {
					Shape pooledShape = rootObjects[i].GetComponent<Shape>();
				}
				return; 
			}

通过游戏对象的activeSelf属性检查形状是否激活。如果它不是活动的,那么我们有一个等待重用的形状,并且必须将它添加到适当的池列表中。

					Shape pooledShape = rootObjects[i].GetComponent<Shape>();
					if (!pooledShape.gameObject.activeSelf) {
						pools[pooledShape.ShapeId].Add(pooledShape);
					}
为什么不使用activeInHiarchy?

这是不需要的,因为我们处理的是根对象。

现在,我们的池在需要的时候重新构建自己而在重新编译后存活。

2.1 关卡1

场景不仅对在播放模式中分组对象有用。通常,项目被划分为多个场景。最明显的配置是每个游戏关卡一个场景。但游戏中的对象通常不属于某个关卡,而是属于整个游戏。比起在每个场景中添加这些对象的副本,它们也可以被放在自己的场景中。这允许你把你的项目分解成多个场景,但需要多个场景在编辑时同时打开。

2.1 多场景编辑

我们将把游戏分成两个场景。我们当前的场景是主要场景,所以将其重命名为Main Scene。然后通过File / New scene创建第二个场景,命名为Level 1。这个新场景代表了我们游戏的第一个关卡。

请添加图片描述

主场景和关卡1。

现在打开Level 1场景,同时保持Main Scene打开。这是通过将场景从项目窗口拖动到层次窗口来完成的。Level 1场景将添加到Main Scene之下,就像我们的池场景出现在游戏模式中一样。Main Scene以粗体文本显示,因为它仍然是活动场景。如果你现在进入游戏模式,你会看到三个场景:主场景、关卡和工厂池。

请添加图片描述

同一时刻的两个场景。

我们的想法是,主场景包含运行游戏所需的所有内容,无论我们玩的是哪个关卡。在我们的例子中,这是主摄像机、游戏对象、存储、画布和事件系统。但是我们会根据水平来调整灯光。所以删除Main Scene的光线和Level 1的摄像机。

请添加图片描述

相机和灯。

2.2 场景照明

我们唯一改变的是,我们把光放在一个独立的场景,这也是开放的。游戏应该像之前那样运行。然而,这是有区别的。环境照明已经变得非常黑暗。

请添加图片描述

环境照明太暗。

除了是游戏对象的集合,场景也有自己的灯光设置。环境照明发生了变化,因为主场景中不再有灯光,其结果是环境照明变暗了。我们得到这个结果是因为使用了活动场景的灯光设置。

关卡场景中有一个灯光,以及与之匹配的环境照明。因此,为了修复灯光,我们必须将Level 1设置为活动场景。这可以通过层级窗口中每个场景下拉菜单中的Set Active Scene选项来实现。

请添加图片描述
请添加图片描述

Level 1为活动场景。

2.3 在构建中包括多个场景

Level 1作为活动场景,我们的游戏就像预期的那样运行,至少在编辑器中是这样。为了让它在构建中正确工作,我们必须确保两个场景都包含在内。点击File / Build Settings…,确保两个场景都被添加,点击Add Open Scenes或者将它们拖放到Scenes In Build列表中。确保Main Scene的索引为0,Level 1的索引为1。

请添加图片描述

建立两个场景。

从现在开始,这两个场景都被添加到构建中,即使在构建时它们没有打开。你可以通过下拉菜单中的Unload Scene选项卸载一个场景。这将使它保持在层次结构窗口中,但是被禁用。

请添加图片描述

Level 1未加载。

你也可以使用Remove Scene选项。这将从层次结构窗口卸载并删除它,不会从项目中删除它。

请添加图片描述

从层次结构中移除Level 1。

2.4 加载一个场景

尽管这两个场景都包含在构建中,但当游戏构建运行时,只有索引0的第一个场景会被加载。这与进入游戏模式时只在编辑器中打开主场景是一样的。为了确保两个关卡都被加载,我们必须手动加载Level 1

在游戏中添加LoadLevel方法。在其中,调用SceneManager.LoadScene,将我们的关卡名称作为参数。

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class Game : PersistableObject {void LoadLevel () {
		SceneManager.LoadScene("Level 1");
	}}

我们的游戏没有启动画面、logo或主菜单,所以在关卡唤醒后立即加载关卡。

	void Awake () {
		shapes = new List<Shape>();
		LoadLevel();
	}

这并没有达到预期的效果。Unity没有加载所有当前打开的场景,然后加载请求的场景。结果就是我们最终得到的只有灯光。这相当于在编辑器中双击一个场景。

我们想要加载的是除了已经加载的内容之外的关卡场景,就像我们之前在编辑器中所做的那样。这可以通过LoadSceneMode.Additive,SceneManager.LoadScene作为附加参数来完成。

	void LoadLevel () {
		SceneManager.LoadScene("Level 1", LoadSceneMode.Additive);
	}

在编辑器中尝试一下,不要加载Level 1。这是可行的,但不幸的是环境照明再次不正确,虽然这一次很难发现。它有点太暗了。

请添加图片描述

不正确的环境照明。

再一次地,我们必须确保Level 1是活动场景,这一次是通过代码。这是通过调用SceneManager.SetActiveScene,带有Scene参数来完成的。我们可以通过SceneManager.GetSceneByName获取所需的场景数据。

	void LoadLevel () {
		SceneManager.LoadScene("Level 1", LoadSceneMode.Additive);
		SceneManager.SetActiveScene(SceneManager.GetSceneByName("Level 1"));
	}

不幸的是,这导致了一个错误。SceneManage.SetActiveScene只适用于加载的场景,显然它不是,即使我们刚刚调用了LoadScene。这是因为加载场景需要一些时间。场景只有在下一帧才完全加载。

2.5 等待一帧

因为加载的场景不会立即完全加载,我们必须等到下一帧才会使其成为活动场景。最简单的方法是将LoadLevel转换为协程。然后我们可以在调用LoadScene和SetActiveScene之间产生一次,添加一个帧的延迟。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class Game : PersistableObject {void Awake () {
		shapes = new List<Shape>();
		StartCoroutine(LoadLevel());
	}IEnumerator LoadLevel () {
		SceneManager.LoadScene("Level 1", LoadSceneMode.Additive);
		yield return null;
		SceneManager.SetActiveScene(SceneManager.GetSceneByName("Level 1"));
	}}
2.6 烘焙照明

虽然Level 1现在正确地成为活动场景,我们仍然没有得到正确的环境照明。至少,不是在编辑器里。构建很好,因为所有的照明都得到了适当的包含。但是在编辑器中,自动生成的灯光数据在播放模式加载场景时不能正常工作。为了确保编辑器中正确的照明,我们必须关闭在照明设置底部的Auto Generate选项,根据Unity版本通过Window / Lighting / Settings or Window / Rendering / Lighting Settings打开。

请添加图片描述

手动设置照明生成。

打开Level 1场景,确保它是活动场景,然后单击Generate Lighting。Unity将烘焙照明数据,并保存在场景资产旁边的文件夹中。

请添加图片描述

Level 1场景照明数据。

这些设置是每个场景的。你只需要手动烘焙Level 1。我们不使用主场景的照明数据,所以你可以把它留在自动生成模式。

2.7 异步加载

加载一个场景所需的时间取决于它包含了多少内容。在我们的例子中,它是一个单一的灯光,所以它加载非常快。但通常情况下,加载需要一段时间,这将导致游戏冻结直到完成。为了避免这种情况,可以通过SceneManager.LoadSceneAsync异步加载场景。这将开始加载场景的过程,并返回一个AsyncOperation对象引用,用于检查场景是否已经完成加载。或者,它可以用于在协程中yield。让我们这样做代替只是一帧中yield。

	IEnumerator LoadLevel () {
		//SceneManager.LoadScene("Level 1", LoadSceneMode.Additive);
		//yield return null;
		yield return SceneManager.LoadSceneAsync(
			"Level 1", LoadSceneMode.Additive
		);
		SceneManager.SetActiveScene(SceneManager.GetSceneByName("Level 1"));
	}

现在我们的游戏不会在加载关卡时冻结。这意味着我们的游戏更新方法有可能在关卡加载并成为活动场景之前被任意次数调用。这是一个问题,因为这让玩家能够在加载关卡前发出命令。为了防止这种情况,游戏组件必须在加载过程开始前禁用自己,并在加载完成后再次启用自己。

	IEnumerator LoadLevel () {
		enabled = false;
		yield return SceneManager.LoadSceneAsync(
			"Level 1", LoadSceneMode.Additive
		);
		SceneManager.SetActiveScene(SceneManager.GetSceneByName("Level 1"));
		enabled = true;
	}

在更复杂的游戏中,你也需要在这些点上显示和隐藏加载屏幕。

2.8 防止双重加载

在游戏开始时加载关卡效果很好,但如果我们已经在编辑器中打开了关卡场景,我们就会在进入游戏模式时再次加载它。
请添加图片描述

Level1加载两次。

因为我们的关卡场景包含一盏灯,所以我们最终使用了两盏灯,从而导致了过于明亮的结果。

请添加图片描述

双重光照。

同样,这只是在编辑器中工作时的问题。但这是我们应该处理的内容,因为你通常会面对一个开放关卡场景,并进入游戏模式进行测试。你肯定不希望关卡再次加载。

为了防止重复加载场景,在Awake中调用LoadLevel之前检查它是否已经加载。如果它已加载,确保它是活动场景,然后中止。

	void Awake () {
		shapes = new List<Shape>();

		Scene loadedLevel = SceneManager.GetSceneByName("Level 1");
		if (loadedLevel.isLoaded) {
			SceneManager.SetActiveScene(loadedLevel);
			return;
		}

		StartCoroutine(LoadLevel());
	}

这不能工作,因为场景还没有被标记为加载。在Awake中尝试还为时过早,但如果我们延迟一点,并使用Start方法,它就会工作。这适用于作为场景一部分的所有游戏物体。当场景加载时,Awake会立即被调用,但还不算作加载。当场景正式加载时,会发生Start和之后的Update调用。

	//void Awake () {
	void Start () {
		shapes = new List<Shape>();}

所有这些只有在编辑器中才有必要。

	void Start () {
		shapes = new List<Shape>();

		if (Application.isEditor) {
			Scene loadedLevel = SceneManager.GetSceneByName("Level 1");
			if (loadedLevel.isLoaded) {
				SceneManager.SetActiveScene(loadedLevel);
				return;
			}
		}

		StartCoroutine(LoadLevel());
	}

3. 更多的关卡

有些游戏只有一个关卡,但通常有多个关卡。所以让我们添加另一个关卡,让它们之间的切换成为可能。

3.1 关卡2

要制作第二个关卡,你可以复制Level 1场景,并将其命名为Level 2。为了使它们在视觉上可区分,打开新场景并调整其光线。例如,将它的X旋转设置为1而不是50,表示太阳就在地平线上。然后烘烤关卡2的照明。

请添加图片描述

两个关卡的场景。

3.2 检测一个已载入的关卡

虽然可以同时打开两个关卡场景,但通常只处理一个关卡是有意义的。也许打开多个关卡来复制或移动内容很方便,但这应该是暂时的。当进入游戏模式时,我们希望除了主场景之外没有或只有一个关卡是开放的。当第1关打开时,这种方法很有效,但如果第2关打开,那么当我们进入游戏模式时,第1关也会被加载。

请添加图片描述

两个关卡都被加载。

为了防止这种情况发生,我们必须调整Game.Start中的关卡检测。我们必须检查任何级别,而不是显式地检查Level 1。目前我们有两个级别,但我们至少应该支持更多的。

我们所能做的就是要求所有关卡场景的名称都包含*Level *一词,后面加上一个空格。然后我们可以循环所有当前加载的场景,检查一个场景的名称是否包含所需的字符串,如果包含,则使其成为活动场景。

为了循环遍历所有加载的场景,我们可以使用SceneManager.sceneCount属性来获取计数和SceneManager.GetSceneAt方法获取特定索引的场景。

	void Start () {
		shapes = new List<Shape>();

		if (Application.isEditor) {
			//Scene loadedLevel = SceneManager.GetSceneByName("Level 1");
			//if (loadedLevel.isLoaded) {
			//	SceneManager.SetActiveScene(loadedLevel);
			//	return;
			//}
			for (int i = 0; i < SceneManager.sceneCount; i++) {
				Scene loadedScene = SceneManager.GetSceneAt(i);
				if (loadedScene.name.Contains("Level ")) {
					SceneManager.SetActiveScene(loadedScene);
					return;
				}
			}
		}

		StartCoroutine(LoadLevel());
	}

现在,如果Level 2恰好在编辑器中打开,游戏就会停留在Level 2上。这使得玩家可以直接玩任何关卡,而无需经过游戏内的关卡选择。

请添加图片描述
请添加图片描述

只有Level 2。

3.3 加载特定关卡

为了加载游戏中的特定关卡,我们必须调整LoadLevel。因为我们只有几个级别,所以我们可以手动地将它们分配给构建,赋予Level 1构建索引1,Level 2索引2,以此类推。为了加载其中一个关卡,我们必须添加关卡的构建索引作为参数。然后我们在加载场景时使用该索引,并使用GetSceneByBuildIndex而不是GetSceneByName。

	IEnumerator LoadLevel (int levelBuildIndex) {
		enabled = false;
		yield return SceneManager.LoadSceneAsync(
			levelBuildIndex, LoadSceneMode.Additive
		);
		SceneManager.SetActiveScene(
			SceneManager.GetSceneByBuildIndex(levelBuildIndex)
		);
		enabled = true;
	}

默认情况下,我们在Start中加载第一个关卡,它的构建索引为1。

	void Start () {StartCoroutine(LoadLevel(1));
	}
我们如何处理多个关卡?

如果一款游戏有许多关卡,那么将它们放在独立的资产包中会更实用,因为玩家可以按需下载这些资产包。这也让我们能够在之后更新或添加关卡到游戏中。本教程不涉及资产包。

3.4 选择关卡

对于我们这款简单的小游戏,我们将使用最直接的方法去选择关卡。只需按一个数字键加载相应的关卡。这种方法最多适用于九个关卡。为了方便调整我们支持的关卡数量,在游戏中添加一个关卡计数字段,然后通过检查器将其设置为2。

	public int levelCount;

请添加图片描述

关卡计数设置为2。

现在我们必须检查玩家是否按下了一个数字键来加载关卡。我们可以通过遍历所有有效的构建索引来实现这一点。对应的密钥代码为KeyCode.Alpha0加上索引。如果玩家按下了按键,那就开始加载该关卡并跳过Update方法的其余部分。

	void Update () {else if (Input.GetKeyDown(loadKey)) {
			BeginNewGame();
			storage.Load(this);
		}
		else {
			for (int i = 1; i <= levelCount; i++) {
				if (Input.GetKeyDown(KeyCode.Alpha0 + i)) {
					StartCoroutine(LoadLevel(i));
					return;
				}
			}
		}}
我们能用这种方式支持第十个关卡吗?

是的,你可以对第十关做一个特殊的检查,将其绑定到0键。

3.5 卸载关卡

我们现在可以在游戏模式中切换两个关卡,但每次加载关卡时,我们就会获得一个更多的开放场景,添加到当前开放的关卡中,而不是替换它们。这是因为我们把它们叠加地加载了。

请添加图片描述

太多加载的关卡。

我们可以通过记录当前加载关卡的构建索引来避免这种情况。所以为它添加一个字段。

	int loadedLevelBuildIndex;

如果我们从一个已经加载的关卡场景开始,那么在Start中初始化这个索引。否则,它将保持默认值0。

				if (loadedScene.name.Contains("Level ")) {
					SceneManager.SetActiveScene(loadedScene);
					loadedLevelBuildIndex = loadedScene.buildIndex;
					return;
				}

在我们完成加载关卡后,也要更新这个索引。

	IEnumerator LoadLevel (int levelBuildIndex) {
		…
		loadedLevelBuildIndex = levelBuildIndex;
		enabled = true;
	}

现在我们可以在开始加载关卡之前检查索引是否为非零。如果是这样,那么就已经有一个关卡场景开放了。我们必须首先卸载这个场景,这是通过调用SceneManager.UnloadSceneAsync用旧的索引作为参数异步完成的。在加载下一个关卡前先完成卸载。

	IEnumerator LoadLevel (int levelBuildIndex) {
		enabled = false;
		if (loadedLevelBuildIndex > 0) {
			yield return SceneManager.UnloadSceneAsync(loadedLevelBuildIndex);
		}
		yield return SceneManager.LoadSceneAsync(
			levelBuildIndex, LoadSceneMode.Additive
		);
		SceneManager.SetActiveScene(
			SceneManager.GetSceneByBuildIndex(levelBuildIndex)
		);
		loadedLevelBuildIndex = levelBuildIndex;
		enabled = true;
	}

最后,我们可以将关卡的加载视为新游戏的开始,删除所有生成的对象。所以在加载另一个关卡之前调用BeginNewGame。

				if (Input.GetKeyDown(KeyCode.Alpha0 + i)) {
					BeginNewGame();
					StartCoroutine(LoadLevel(i));
					return;
				}
如果我们再次加载相同的关卡,我们是否可以跳过加载?

假设Level 2当前已加载,玩家按下2按钮。然后新游戏开始,Level 2被卸载,然后Level 2被加载。我们是否能够跳过相同场景的卸载和加载而开始一款新游戏?

就我们的情况而言,目前的答案是肯定的。这些场景只包含一盏灯。游戏过程中没有任何改变。一般来说,答案通常是否定的,因为场景的状态可以改变很多。我们可以将关卡重置为初始状态而不是重新加载它,但这是否值得我们去实现却值得怀疑。因为我们的关卡加载非常快,所以我们只是简单地重载它们。

4. 记录关卡

此时我们可以在游戏过程中切换关卡,但保存和加载游戏仍然会忽略关卡。因此,我们可以将形状保存在一个关卡中,并在另一个关卡中加载它们。我们必须确保游戏能够记住保存了哪个关卡。

4.1 保存关卡

保存关卡需要我们在保存文件中添加额外的数据,这使得它与我们的旧版本游戏不兼容。因此,将保存版本从1增加到2。

	const int saveVersion = 2;

当游戏保存自己时,现在也写当前打开关卡的建造索引。让我们在形状计数之后做这个,但在写形状之前。

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

这种方法依赖于关卡的特定构建索引,所以在此之后我们不能在不破坏向后兼容性的情况下更改它们,就像我们不能更改工厂的形状预制件的顺序一样

4.2 加载关卡

加载时,与往常一样,我们首先处理形状计数的特殊情况。然后读取关卡构建索引,除非我们有更早的保存文件,在这种情况下我们总是加载关卡1。然后立即开始加载关卡。

	public override void Load (GameDataReader reader) {int count = version <= 0 ? -version : reader.ReadInt();
		StartCoroutine(LoadLevel(version < 2 ? 1 : reader.ReadInt()));
		for (int i = 0; i < count; i++) {}
	}

通过这种方法,我们开始加载关卡,同时还需要读取和创建所有形状。因为关卡加载是异步的,所以当我们读取和创建形状时,当前的关卡场景仍然是加载场景。只有稍后才会加载正确的关卡并激活场景。这不是问题,因为我们将所有形状放在一个独立的工厂场景中,它们不依赖于关卡中的任何内容。这种情况在未来可能会改变,使这个过程更加复杂。需要的时候我们会处理的。

下一个教程是生成区域。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值