Unity基础教程 您所在的位置:网站首页 unity如何多选 Unity基础教程

Unity基础教程

2023-04-10 10:31| 来源: 网络整理| 查看: 265

200+篇教程总入口,欢迎收藏:

本文重点:

1、在运行模式下创建场景2、在不同场景之间移动物体3、多场景同时工作4、支持游戏关卡

这是关于对象管理系列的第四篇教程。它是关于在如何在场景中放置对象,并同时与多个场景一起工作,以及加载和卸载场景等内容。

本教程是CatLikeCoding系列的一部分,原文地址见文章底部。“原创”标识意为原创翻译而非原创教程。

本教程使用Unity 2017.4.4f1编写。

不同的时间和不同的关卡

1 池场景

在游戏模式下,当越来越多形状被实例化时,场景会很快被对象填满,编辑器层次窗口会变得非常混乱。这可能会让查找特定对象变得十分困难,还可能使编辑器运行速度变慢。

运行模式下单个场景

一种可以减缓编辑器变慢的方式是,通过切换场景的层次窗口或者移除层次窗口,但是这样我们就看不到物体对象了。理想情况下,我们可以在hierarchy窗口中将所有的shape实例折叠为单个条目,而其他所有实例仍然可见。有两种方法可以做到这一点。

第一方法是是创建一个根对象,并使所有形状都是该对象的子对象,然后我们可以折叠根对象。但坏处是,当形状改变时,它会对我们的游戏性能产生负面影响。因为每当对象的active状态或transform状态发生变化时,它的所有父对象都会被通知此变化。因此,在没有必要的情况下,最好避免让对象成为另一个对象的子对象。

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

1.1 在运行时创建场景

我们需要一个专门的场景来收纳形状实例。因为形状实例只在游戏模式下存在,并且场景也只需要在游戏模式下存在。所以,我们将通过代码来创建一个。

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

我们将创建一个池场景来包含所有可以回收的形状实例。工厂创建所有的形状都进入了这个池子,永远不能从里面移走。我们可以使用Scene 变量来跟踪这个池场景。

我们只在开启recycling的时候才需要一个场景 。当不开启的时候,可以将实例的管理留给请求它们的人。所以我们只需要在需要池时再创建一个场景。因此,在CreatePools的末尾调用SceneManager。创建一个新的场景并追踪它。场景需要一个名称,我们只需使用工厂的名称即可。如果你使用多个工厂,它们都会有自己的场景,所以确保给每个工厂一个唯一的名字。

形状工厂的池场景

现在,当我们在游戏模式中第一次创建一个形状时,一个形状工厂场景就会出现,虽然这些形状还没有放到里面。但当我们停止运行时,场景就消失。

1.2 把对象放入场景池

当一个游戏对象被实例化时,它将被添加到活动场景中。在我们的案例中,活动场景是Scene,是项目中唯一呈现的场景。我们可以改变活动场景,但我们不希望工厂把场景弄乱。取而代之的是,通过调用SceneManager.MoveGameObjectToScene,在创建形状之后将它们迁移到池场景中,它需要传入游戏对象和场景作为参数。

形状放入了池场景

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

1.3 从重编译中恢复

工厂现在工作得很好,至少在同一次构建下或只要我们保持在游戏模式下。但问题是,在游戏模式下的重新编译就会打乱我们的回收和池场景。

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

我们不能仅将池标记为可序列化吗?这将使Unity保存池作为资产的一部分,在编辑器运气会话之间保存它,并在构建中包含它。但那不是我们想要的。

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

这似乎行不通。这是因为场景是一个结构体,而不是对实际场景的直接引用。因为它是不可序列化的,所以重新编译会将结构重新设置为默认值,默认值表示一个已卸载的场景。我们需要通过SceneManager.GetSceneByName方法请求一个新的连接。

现在没问题了,但是我们只需要在Unity编辑器而不是而不是在构建完成。可以通过

Application.isEditor 属性检查我们是否在编辑器中。

第二个不太明显的问题是,在重新编译之前处于非活动状态的形状实例永远不会再被重用了。那是因为我们丢失了追踪它们的列表。可以通过重新填充列表来解决此问题。首先,通过Scene.GetRootGameObjects方法检索包含池场景的所有根游戏对象的数组。

这不是创建一个临时数组吗?是的。还有一个使用list参数的变体,可用于避免使用临时数组。但是我们在重新编译后就进入了编辑器,因此,这里真的不必担心内存效率。

接下来,循环所有对象并获取它们的形状组件。因为这是工厂场景,它应该只包含形状,所以我们总是会得到一个组件。如果有空引用错误,则表明在其他地方有问题的,需要排查。

通过其游戏对象的activeSelf属性检查该形状是否处于活动状态。如果它不是活动的,那么我们就有一个等待复用的形状,并且需要将其添加到适当的池列表中。

可以使用activeInHierarchy吗?不需要,因为我们正在处理根对象。

现在,我们的池在重新编译时通过重建自己存活下来。

2 关卡1

场景不仅对在播放模式下可以对对象进行分组。通常,项目会分为多个场景。最明显的配置是每个游戏关卡会是一个场景。但是游戏通常具有的对象不只属于单个关卡,而是属于整个游戏。除了将这些对象的副本放置在每个场景中之外,还可以将它们放置在自己的场景中。这使得你可以将项目分解为多个场景,但是需要在编辑时同时打开多个场景。

2.1 多场景编辑

我们将把游戏分为两个场景。我们当前的场景是主场景,因此将其重命名为“Main Scene”。然后通过“File/ New Scene”创建一个名为“级Level1”的第二场景。该新场景代表了我们游戏的第一关。

Main 场景 和 Level 1场景

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

同时拥有2个场景

无论我们运行的是哪个关卡,主要场景都包含了运行游戏所需的一切。在我们的例子中,这是主摄像机、游戏对象、存储、画布和事件系统。但我们会根据不同的关卡设置光照。所以删除主场景的灯光和Level 1的相机。

一个场景和一个灯光

2.2 场景灯光

我们唯一更改的是将灯光放在一个单独的场景中,该场景也是开放的。按道理来说游戏应该像以前一样运行。但是,事实上已经导致了区别。如下所示,环境照明已经变得非常暗了。

环境光太暗

除了是游戏对象的集合之外,场景也有自己的灯光设置。环境光变化,是因为主场景中不再有灯光了,导致环境照明变暗。现在的这个结果是因为使用了活动场景的灯光设置。

level场景中有一个灯光,与环境照明相匹配。所以为了修正灯光,我们必须将Level设置为活动场景。这可以通过层次窗口中每个场景的下拉菜单中的设置活动场景选项来完成。

把level1设置为活动场景

2.3 在构建中包含多场景

在level1作为活动场景的情况下,我们的游戏至少在编辑器中可以按预期工作了。为了使它在构建中也能正常工作,我们必须确保两个场景都包括在内。转到“File / Build Settings… ”,并通过单击“Add Open Scenes ”或将它们拖到“Scenes In Build list”列表中,确保添加了两个场景。确保主场景的索引为0,级别1的索引为1。

2个场景都放入构建中

从现在开始,两个场景都被添加到构建中,即使它们在构建时不是打开的也是如此。你可以通过其下拉菜单中的“Unload Scene”选项来卸载场景。这会将其保留在层次结构窗口中,但被禁用。

level1没有被加载

你也可以使用移除场景选项。从层次结构窗口卸载并删除它。它不会从项目中删除它。

level1从场景层次中移除

2.4 加载场景

即使两个场景都包含在构建中,运行游戏构建时也只会加载第一个场景(索引为0)。这与进入运行模式时仅在编辑器中打开主场景相同。为了确保同时加载两个关卡,我们必须手动加载level1。

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

我们的游戏没有启动画面,logo简介或主菜单,因此在唤醒时会立即加载关卡。

这并没有达到预期的效果。Unity卸载所有当前打开的场景,然后加载请求的场景。结果就是我们最后除了光照的物体什么也没有。这相当于在编辑器中双击一个场景。

我们想要的是在已经加载的场景之外加载关卡场景,就像我们之前在编辑器中做的那样。这可以通过提供LoadSceneMode来实现。作为SceneManager.LoadScene的附加参数。

现在,它可以生效了,但是不幸的是,环境照明仍然不正确,虽然这次很难发现,但仔细看还是有点黑了。

不正确的环境光照

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

这么写还是会导致错误。SceneManager.SetActiveScene仅适用于已加载的场景,虽然我们已经调用LoadScene,但加载场景需要一些时间。场景仅在下一帧才能完全加载。

2.5 等待下一帧

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

2.6 烘焙环境光

尽管level1现在可以正确地变为活动场景,但我们仍然无法获得正确的环境照明。至少不在编辑器中是这样的。构建是没问题的,因为可以正确包含所有光照数据。但是在编辑器中,以播放模式加载场景时,自动生成的照明数据无法正常工作。为了确保编辑器中的照明正确,我们需要关闭自动设置选项,该选项位于照明设置的底部,可能会存在Unity版本差别,但基本都可以通过Window/ Lighting Settings or Window Rendering / Lighting Settings 打开。

手动管理光照数据生成

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

生成level1的照明数据

这些设置是针对单个场景的。你只需要手动烘焙level1即可。我们不使用主场景的照明数据,因此你可以将其保持在自动生成模式下。

2.7 异步加载

加载场景需要多长时间取决于它包含多少内容。在我们的例子中,它是单个光源,因此加载非常快。但总的来说,加载可能需要一段时间,这会冻结游戏直到加载完成。为避免这种情况,可以通过SceneManager.LoadSceneAsync异步加载场景。这意味着开始加载场景,会返回AsyncOperation对象引用,该引用可用于检查场景是否已完成加载。或者,它可用于产生协程。我们按照现在的方式修订一下,而不是只像上面那样延迟一帧。

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

在更复杂的游戏中,你还可以在这里显示和隐藏一个加载屏幕。

2.8 阻止双重加载

游戏开始时加载关卡工作正常,但是如果我们已经在编辑器中打开关卡场景,则在进入游戏模式时会再次加载它。

level1 加载了2次

因为我们的关卡场景包含了一个灯光,等于是我们最终使用了两个灯光,会导致了一个过亮的结果。

两次灯光

同样,这只是在编辑器中工作时的问题。但这是我们应该处理的事情,因为你通常都会使用这种方式启动进行测试。当然你也不希望加载2次关卡。

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

但是这一次仍然行不通。因为场景尚未标记为已加载。在Awake中尝试还为时过早,但是如果我们稍稍延迟一下,改用Start方法,它会生效。对于场景中的所有游戏对象都是如此。加载场景时,将立即调用Awake,但尚未算作已加载。Start和Update的调用在正式加载场景之后进行。

仅当我们在编辑器中时,这些才是必需的。

3 更多的关卡

有些游戏只有一个关卡,但大部分时候有多个关卡。因此,我们再来添加另一个关卡,并使其能够在它们之间进行切换。

3.1 level2

要创建level2,可以复制level1场景并将其命名为level2。要使它们在视觉上可区分,请打开新场景并调整其灯光。例如,将其X旋转设置为1而不是50,表示太阳正好位于地平线上方。然后烘焙level2的照明。

两个关卡场景

再向构建添加level 2,并为其分配构建索引2。

两个关卡场景和1个主场景

3.2 检查加载的关卡

虽然可以同时打开两个关卡场景,但只使用一个关卡才是有意义的。打开多个关卡以复制或移动内容可能很方便,但这只应该是暂时的。进入播放模式时,除了主场景外,我们希望没有或只有一个关卡打开。当第level1打开时,此方法工作正常,但如果level2打开的话,现在的情况是,进入播放模式时,level1也会加载。

两个关卡都被加载了

为了防止这种情况的发生,我们必须在Game.Start中调整关卡检测。无需显式检查级level1,需要检查任何关卡。当前,我们有两个关卡,但是我们应该支持检查不限数量的关卡。

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

要遍历所有已加载的场景,我们可以使用SceneManager.sceneCount属性获取计数,并使用SceneManager.GetSceneAt方法获取特定索引处的场景。

现在,如果碰巧在编辑器中将其打开,则游戏会保持加载level2。这使得玩家可以直接游玩任何关卡,而无需进行游戏内关卡的选择。

只有关卡2

3.3 加载特殊的关卡

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

默认情况下,我们在Start中加载具有构建索引1的关卡。

我们如何处理多关卡?如果游戏具有多个关卡,那么将它们放在单独的AssetBundle中(可能可以按需下载)更加实用。这也使得以后可以更新或添加游戏关卡。AssetBundle不在本教程中讲解。

3.4 选择关卡

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

level count 设置为2

现在我们必须检查玩家是否按下数字键之一来加载关卡。我们可以通过遍历所有有效的构建索引来做到这一点。相应的键代码是KeyCode.Alpha0加上索引。如果按下该键,则开始加载该关卡,然后跳过其余的Update方法。

我们不能以这种方式支持十个级别吗?可以,需要对第十项进行特殊检查,比如绑定到零键。

3.5 卸载关卡

现在,我们可以在播放模式下在两个关卡之间切换,但是每次加载关卡时,我们都会获得一个新的场景,并将其添加到当前打开的关卡中,而不是替换它们。发生这种情况是因为我们使用了additively的方式。

太多啦

我们可以通过追踪当前已加载关卡的构建索引来防止这种情况。因此,为其添加一个字段。

如果我们以已加载的关卡场景开始,请在“Start”中初始化索引。否则,它将保持其默认值零。

完成加载关卡后,还更新此索引。

现在,我们可以在开始加载关卡之前检查索引是否为非零。如果是这样的话,则表明已经存在一个关卡场景了。我们需要首先卸载此场景,这是通过使用旧索引调用SceneManager.UnloadSceneAsync来异步完成的。在继续加载下一个关卡之前,要先延迟卸载当前关卡。

最后,将关卡的加载视为新游戏的开始是有道理的,它需要卸载所有已生成的对象。因此,在加载另一个关卡之前,请调用BeginNewGame。

如果我们要再次加载同一关卡,是否可以跳过加载关卡?假设当前已加载关卡2,并且玩家按下2按钮。然后开始新的游戏,卸载关卡2,然后加载关卡2。我们可以只开始一个新游戏,而不跳过同一场景的卸载和加载吗?就我而言,目前的答案是肯定的。场景仅包含一个灯光。运行过程中什么都没有改变。但大部分时候,答案通常是“否”,因为场景的状态可能会发生很大变化。可以将关卡重置为初始状态,而不是重新加载它,但是问题是这个方案是否值得一试。由于我们的关卡加载非常快,因此我们只需重新加载即可。

4 记住关卡

此时,我们可以在游戏过程中在关卡之间切换,但是保存和加载游戏仍然会忽略关卡。结果,我们可以将形状保存在一个关卡中,然后将其加载到另一个关卡中。所以,我们需要确保游戏记住已保存的关卡。

4.1 保存关卡

保存关卡要求我们向保存文件中添加其他数据,从而使其与我们游戏的较早版本不兼容。因此,将保存版本从1增加到2。

当游戏保存自身时,现在还要编写当前打开关卡的构建索引。让我们在形状计数之后,写入形状之前执行此操作。

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

通过这种方法,我们开始加载关卡,同时仍然需要读取和创建所有形状。由于关卡加载是异步的,因此当我们读取和创建形状时,当前关卡场景仍然是加载的场景。直到以后,才加载正确的关卡并激活场景。这没有问题,因为我们将所有形状放置在单独的工厂场景中,并且它们不依赖于关卡中的任何内容。再未来这可能会改变,从而使此过程更加复杂。我们将在需要时处理该问题。

下一篇,介绍 生成区域。

本文翻译自 Jasper Flick的系列教程

原文地址:

https://catlikecoding.com/unity/tutorials



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有