喵的Unity游戏开发之路 - 多场景:场景加载
如果丢失格式、图片或视频,请查看原文:https://mp.weixin.qq.com/s/RDVMg6l41uc2IHBsscc0cQ
很多童鞋没有系统的Unity3D游戏开发基础,也不知道从何开始学。为此我们精选了一套国外优秀的Unity3D游戏开发教程,翻译整理后放送给大家,教您从零开始一步一步掌握Unity3D游戏开发。 本文不是广告,不是推广,是免费的纯干货!本文全名:喵的Unity游戏开发之路 - 对象管理 - 多场景:场景加载
在播放模式下创建场景。
在场景之间移动对象。
处理多个场景。
支持游戏关卡。
这是有关对象管理的系列教程中的第四篇。这是关于将对象放置在自己的场景中,一次处理多个场景,以及加载和卸载场景。
本教程使用Unity 2017.4.4f1制作。
缓存场景
在播放模式下实例化许多形状时,场景会迅速充满对象,并且层次结构窗口可能会变得很混乱。这可能使查找特定对象变得困难,并且还可能降低编辑器的速度。
可以通过折叠场景层次结构或删除层次结构窗口来防止潜在的编辑器变慢,但是之后我们就看不到对象了。理想情况下,我们可以将所有形状实例折叠到层次结构窗口中的单个条目中,而其他所有内容保持可见。有两种方法可以做到这一点。
第一种选择是创建一个根对象,并使该对象的所有形状成为子对象。然后,我们可以折叠根对象。不幸的是,这会在形状改变时对我们的游戏性能产生负面影响。每当对象的活动状态或变换状态发生更改时,都会将该更改通知其所有父对象。因此,在并非绝对必要时,最好避免使对象成为另一个对象的子对象。
第二种选择是将所有形状放置在单独的场景中。它们仍然是没有父级的根对象,但成为额外场景的一部分,可以在层次结构窗口中折叠。场景不在乎对象的状态,因此不会降低游戏的速度。这是我们将要使用的选项。
播放时创建场景
我们想要一个包含形状实例的专用场景。由于形状实例仅在播放模式下存在,因此场景也仅在我们处于播放模式时才需要存在。因此,我们将通过代码而不是通过编辑器来创建一个。
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);
}
现在,虽然我们尚未将形状工厂放进去,但是在播放模式下我们第一次创建形状时,会出现形状工厂场景。当我们停止播放时,场景消失了。
将对象放入缓存场景
实例化游戏对象时,会将其添加到活动场景。在我们的例子中,活动场景是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
);
}
}
…
}
从现在开始,将形状整齐地放置在“ 形状工厂”场景中,您可以在层次结构窗口中将其折叠,也可以在要查看时保持打开状态。
从重新编译中恢复
至少在构建中或只要我们保持游戏模式,工厂就可以正常工作。不幸的是,在游戏模式下重新编译会弄乱我们的回收和池场景。
虽然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;
}
}
这不是创建一个临时数组吗?
是。还有一个使用list参数的变体,可用于避免使用临时数组。但是我们在重新编译后就进入了编辑器,因此,这里真的不需要担心内存效率。
接下来,遍历所有对象并获取其形状组件。由于这是工厂场景,因此只应包含形状,因此我们总是得到一个组件。在此之后,如果引用错误为空,则表明其他地方存在问题。
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);
}
我们不应该使用activeInHierarchy吗?
不需要,因为我们正在处理根对象。
现在,我们的池可以通过在需要时进行自我重建来生存。
关卡1
场景不仅对在播放模式下对对象进行分组有用。通常,项目会分为多个场景。最明显的配置是每个游戏关卡一个场景。但是游戏通常具有的对象不属于单个关卡,而是属于整个游戏。除了将这些对象的副本放置在每个场景中之外,还可以将它们放置在自己的场景中。这使您可以将项目分解为多个场景,但是需要在编辑时同时打开多个场景。
多场景编辑
我们将把游戏分为两个场景。我们当前的场景是主场景,因此将其重命名为Main Scene。然后通过File / New Scene创建另一个名为Level 1的场景。这个新场景代表了我们游戏的第一层次。
现在打开关卡1场景,同时也保持主场景打开。这是通过将场景从项目窗口拖动到层次结构窗口中来完成的。在关卡1的场景将被添加以下主场景,就像我们的缓存一幕出现在播放模式。主场景以粗体显示,因为它仍然是活动场景。如果现在进入播放模式,则最终会出现三个场景:主场景,关卡和工厂场景。
这个想法是,无论我们在哪个关卡玩游戏,主场景都包含运行游戏所需的一切。在我们的案例中,这就是主摄像机,游戏对象,存储,画布和事件系统。但是,我们将使照明取决于关卡水平。因此,请从“ 主要场景”中删除灯光,并从“ 关卡1”中删除相机。
场景照明
我们唯一更改的是将灯光放在一个单独的场景中,该场景也是开放的。游戏应该像以前一样运行。但是,有区别。事实证明,环境照明已经变得非常暗。
除了作为游戏对象的集合外,场景还具有自己的照明设置。环境照明发生了变化,因为主场景中不再有灯光,因此其环境照明变暗了。由于使用了活动场景的照明设置,因此我们得到了此结果。
关卡场景中有一个灯光,并带有匹配的环境照明。因此,要固定照明,我们必须使“ 关卡1”成为活动场景。这可以通过层次结构窗口中每个场景的下拉菜单中的“ 设置活动场景”选项来完成。
在构建中包括多个场景
在关卡1作为活动场景的情况下,我们的游戏至少在编辑器中可以按预期工作。为了使它在构建中也能正常工作,我们必须确保两个场景都包括在内。转到“ 文件/构建设置”,并通过单击“ 添加打开的场景”或将它们拖到“ 构建中的场景”列表中,确保添加了两个场景。确保主场景的索引为0,关卡1的索引为1。
从现在开始,两个场景都被添加到构建中,即使它们在构建时未打开也是如此。您可以通过其下拉菜单中的“ 卸载场景”选项来卸载场景。这会将其保留在层次结构窗口中,但被禁用。
您也可以使用“ 删除场景”选项。它将卸载并将其从层次结构窗口中删除。它不会将其从项目中删除。
加载场景
即使两个场景都包含在构建中,运行游戏构建时也只会加载第一个场景(索引为0)。这与进入播放模式时仅在编辑器中打开主场景相同。为了确保同时加载两个关卡,我们必须手动加载Level 1。
向Game添加LoadLevel方法。在其中,以关卡名称作为参数进行调用SceneManager.LoadScene。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class Game : PersistableObject {
…
void LoadLevel () {
SceneManager.LoadScene("Level 1");
}
…
}
我们的游戏没有启动画面,徽标简介或主菜单,因此在唤醒时立即加载关卡。
void Awake () {
shapes = new List<Shape>();
LoadLevel();
}
这没有理想的效果。Unity卸载所有当前打开的场景,然后加载请求的场景。结果是我们最终只剩下了轻的物体。这等效于在编辑器中双击场景。
我们想要的是除了已经加载的场景外,还加载关卡场景,就像我们之前在编辑器中所做的一样。可以通过提供SceneManager.LoadScene额外的参数LoadSceneMode.Additive来完成此操作。
void LoadLevel () {
SceneManager.LoadScene("Level 1", LoadSceneMode.Additive);
}
在没有加载关卡1的情况下,在编辑器中尝试一下。它可以工作,但是不幸的是,环境照明仍然不正确,尽管这次很难发现。有点太黑了。
再次,我们必须通过代码确保关卡1是活动场景。通过调用带有Scene参数的SceneManager.SetActiveScene来完成。我们可以通过SceneManager.GetSceneByName获取所需的场景数据。
void LoadLevel () {
SceneManager.LoadScene("Level 1", LoadSceneMode.Additive);
SceneManager.SetActiveScene(SceneManager.GetSceneByName("Level 1"));
}
不幸的是,这会导致错误。SceneManager.SetActiveScene仅适用于已加载的场景,即使我们刚刚调用LoadScene,它显然也不适用。那是因为加载场景需要一些时间。场景仅在下一帧上完全加载。
等待帧
由于加载的场景不会立即完全加载,因此我们必须等到下一帧才能将其设为活动场景。最简单的方法是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());
}
…
IEnumeratorLoadLevel () {
SceneManager.LoadScene("Level 1", LoadSceneMode.Additive);
yield return null;
SceneManager.SetActiveScene(SceneManager.GetSceneByName("Level 1"));
}
…
}
烘烤灯
尽管关卡1现在可以正确地变为活动场景,但我们仍然无法获得正确的环境照明。至少不是在编辑器中。构造很好,因为可以正确包含所有照明。但是在编辑器中,以播放模式加载场景时,自动生成的照明数据无法正常工作。为了确保编辑器中的照明正确,我们必须关闭自动设置选项,该选项位于照明设置的底部,根据Unity版本,通过窗口/照明/设置或窗口/渲染/照明设置打开。
打开关卡1场景,确保它是活动场景,然后单击生成照明。Unity将烘焙照明数据并将其保存在场景资产旁边的文件夹中。
这些设置是针对每个场景的。您只需要手动烘焙关卡1即可。我们不使用主场景的照明数据,因此您可以将其保留为自动生成模式。
异步加载
加载场景需要多长时间取决于它包含多少内容。在我们的例子中,它是单个光源,因此加载非常快。但总的来说,加载可能需要一段时间,这会冻结游戏直到完成。为避免这种情况,可以通过SceneManager.LoadSceneAsync异步加载场景。这将开始加载场景的过程,并返回AsyncOperation对象引用,该对象引用可用于检查场景是否已完成加载。或者,它可用于产生协程。让我们这样做,而不是只产生一帧。
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"));
}
现在我们的游戏在加载关卡时不会冻结。这意味着Update在加载关卡并成为活动场景之前,我们的游戏方法有可能被任意调用了几次。这是一个问题,因为它使玩家可以在加载关卡之前发出命令。为避免这种情况,Game组件必须在开始加载过程之前禁用自身,并在加载完成后再次启用自身。
IEnumerator LoadLevel () {
enabled = false;
yield return SceneManager.LoadSceneAsync(
"Level 1", LoadSceneMode.Additive
);
SceneManager.SetActiveScene(SceneManager.GetSceneByName("Level 1"));
enabled = true;
}
在更复杂的游戏中,您还将在这些位置显示和隐藏加载屏幕。
防止双重装载
游戏开始时加载关卡工作正常,但是如果我们已经在编辑器中打开关卡场景,则在进入游戏模式时最终会再次加载关卡场景。
因为我们的关卡场景包含一盏灯,所以我们最终只能使用两盏灯,从而导致亮度过高。
同样,这只是在编辑器中工作时的问题。但这是我们应该处理的事情,因为您通常使用开放关卡场景并进入播放模式进行测试。您不希望第二次加载关卡。
为防止双重加载场景,请在Awake调用LoadLevel中检查它是否已加载。如果已加载,请确保它是活动场景,然后中止。
void Awake () {
shapes = new List<Shape>();
Scene loadedLevel = SceneManager.GetSceneByName("Level 1");
if (loadedLevel.isLoaded) {
SceneManager.SetActiveScene(loadedLevel);
return;
}
StartCoroutine(LoadLevel());
}
再次,这是行不通的,因为尚未将场景标记为已加载。尝试尚为时过早,但是如果我们稍稍延迟一下,而是改用一种方法Start,它会奏效。对于场景中的所有游戏对象都是如此。场景加载后Awake立即被调用,但尚未算作加载。Starth和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());
}
更多关卡
有些游戏只有一个关卡,但通常有多个关卡。因此,让我们添加另一个关卡,并使其能够在它们之间进行切换。
关卡2
要创建关卡2,您可以复制关卡1场景并将其命名为关卡2。为了使它们在视觉上可区分,请打开新场景并调整其光线。例如,将其X旋转设置为1而不是50,表示太阳正好位于地平线上方。然后烘烤关卡2照明。
还将关卡2添加到构建中,为其分配构建索引2。
检测场景加载
虽然可以同时打开两个关卡场景,但只使用一个关卡是很有意义的。打开多个关卡以复制或移动内容可能很方便,但这应该是暂时的。进入播放模式时,除了主要场景外,我们希望没有或只有一个关卡打开。当关卡1打开时,此方法工作正常,但如果关卡2打开,则当我们进入播放模式时,关卡1也将加载。
为了防止这种情况的发生,我们必须在中调整电平检测Game.Start。无需显式检查关卡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());
}
现在,如果碰巧在编辑器中将其打开,则游戏将保持关卡2不变。这使得直接玩任何关卡成为可能,而无需进行游戏内关卡选择。
加载特定关卡
为了在游戏中加载特定关卡,我们必须进行调整LoadLevel。因为我们只有几个关卡,所以我们可以手动将它们分配给构建,从而赋予关卡1构建索引1、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));
}
我们该如何应对许多层次?
如果游戏具有多个关卡,那么将它们放在单独的资产捆绑包中(可能可以按需下载)更加实用。这也使得以后可以更新或添加游戏关卡。资产捆绑包不在本教程中。
选择关卡
对于简单的小型游戏,我们将使用最直接的方法来选择一个关卡。只需按数字键即可加载相应的电平。这最多可用于九个关卡。为了轻松调整我们支持的关卡,请在Game上添加一个关卡计数字段,然后通过检查器将其设置为2。
public int levelCount;
现在我们必须检查玩家是否按下数字键之一来加载关卡。我们可以通过遍历所有有效的构建索引来做到这一点。相应的键代码是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;
}
}
}
…
}
我们不能以这种方式支持十个关卡吗?
是的,如果对第十项进行特殊检查,则绑定到零键。
卸载关卡
现在,我们可以在播放模式下在两个关卡之间切换,但是每次加载关卡时,我们都会获得一个开放的场景,添加到当前打开的关卡中,而不是替换它们。发生这种情况是因为我们加了场景。
我们可以通过跟踪当前已加载关卡的构建索引来防止这种情况。因此,为其添加一个字段。
int loadedLevelBuildIndex;
如果我们以已加载的关卡场景开始,则Start初始化中的索引。否则,它将保持其默认值零。
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;
}
如果我们要再次加载同一关卡,是否可以跳过加载关卡?
假设当前已加载关卡2,并且玩家按下2按钮。然后,一个新的游戏开始,关卡2被卸载,然后关卡2被加载。我们可以只开始一个新游戏,而不必跳过同一场景的卸载和加载吗?
就我们而言,目前的答案是肯定的。场景仅包含一个灯光。比赛过程中什么都没有改变。通常,答案通常是“否”,因为场景的状态可能会发生很大变化。可以将关卡重置为初始状态,而不是重新加载它,但是值得一试的是值得怀疑的。由于我们的关卡加载非常快,因此我们只需重新加载即可。
记住关卡
此时,我们可以在游戏过程中在关卡之间切换,但是保存和加载游戏仍然会忽略关卡。结果,我们可以将形状保存在一个关卡中,然后将其加载到另一个关卡中。我们必须确保游戏记住已保存的关卡。
保存关卡
保存关卡要求我们将其他数据添加到保存文件中,从而使其与我们的游戏的旧版本不兼容。因此,将保存版本从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++) {
…
}
}
这种方法依赖于我们关卡的特定构建索引,因此在此之后我们不能在不破坏向后兼容性的情况下更改它们,就像我们无法更改工厂预制件的顺序一样。
加载关卡
加载时,我们像往常一样首先处理形状计数的特殊情况。然后读取关卡构建索引,除非我们有一个较旧的保存文件,在这种情况下,我们总是加载关卡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++) {
…
}
}
通过这种方法,我们开始加载关卡,同时仍然需要读取和创建所有形状。由于关卡加载是异步的,因此当我们读取和创建形状时,当前关卡场景仍然是加载的场景。直到稍后,正确的关卡才会被加载并激活场景。这不是问题,因为我们将所有形状放置在单独的工厂场景中,并且它们不依赖于关卡中的任何内容。将来这可能会改变,从而使此过程更加复杂。我们将在需要时处理该问题。
下一个教程是 生成区:各色场景。
资源库(Repository)
https://bitbucket.org/catlikecodingunitytutorials/object-management-04-multiple-scenes
往期精选
Unity3D游戏开发中100+效果的实现和源码大全 - 收藏起来肯定用得着
Shader学习应该如何切入?
喵的Unity游戏开发之路 - 从入门到精通的学习线路和全教程
声明:发布此文是出于传递更多知识以供交流学习之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与我们联系,我们将及时更正、删除,谢谢。
原作者:Jasper Flick
原文:
https://catlikecoding.com/unity/tutorials/object-management/multiple-scenes/
翻译、编辑、整理:MarsZhou
More:【微信公众号】 u3dnotes