本文重点: 1、创建一个生成区域并可以转置 2、使用Gizmos可视化生成区域 3、支持逐场景的不同生成区域 4、连接不同场景的对象 5、创建多个类型的生成区域
这是有关对象管理的系列教程中的第五篇。主要扩展了如何让对象以更多不同的模式生成,并且支持每个关卡的单独配置。
本教程是CatLikeCoding系列的一部分,原文地址见文章底部。“原创”标识意为原创翻译而非原创教程。
本教程使用Unity 2017.4.4f1制作。
(通过小的生成区域生成巨大的形状)
1 生成点
我们这个简单游戏玩法就是生成随机形状。每种形状的材质和颜色都是随机选择的,其位置,旋转和比例也是如此。尽管生成点是随机的,但它们被约束在以世界原点为中心的半径为5个单位的球形区域中。如果生成足够多的对象后,它们将形成可识别的球体。这其实是我们已经以硬编码形式在游戏中的产生的生成区域了。
我们不必限制只在一个单一的生成区域里生成,也可以让形状在不同的配置中生成。要实现该功能,需要用一个可配置的生成区替换我们的固定代码。
1.1 生成区域组件
创建一个新的SpawnZone组件类型。它的唯一目的是提供生成点,因此为其提供Vector3 SpawnPoint属性。这提供了一种获取点的方法,而无需设置它们,因此只需要Get即可。这使它成为仅具有getter或readonly属性。我们将首先返回半径为5个单位的球体内的随机点。
将Spawn Zone游戏对象添加到主场景并将新组件附加到主场景。现在,我们在游戏中有一个生成区域,但是现在还没有使用它。
(Spawn zone 对象)
1.2 使用区域
下一步是让游戏从分离的生成区内取回它的生成点。为此添加一个公共字段,并在CreateShape中使用它来获得生成点。
通过检视器器连接生成区域。尽管游戏的行为仍然没有改变,但它现在已经依赖于Spawn Zone对象了。
(Spawn zone 引用)
1.3 转置区域
因为生成区域是游戏对象的一部分,所以我们可以将其移动。要影响生成点,请将对象的位置添加到随机点。通过使用Transform组件的position属性而不是localPosition,可以使生成区域成为另一个对象的子级。这样,可以将生成区域附加到其他可能正在移动的区域。
我们可以更进一步,将游戏对象层次结构的整个transform应用于生成点。然后,我们还可以旋转和缩放区域。为此,请使用随机点作为参数调用区域的Transform组件的TransformPoint方法。现在,我们可以取消乘以五,并通过设置对象的比例来控制区域的半径。
(Spawn zone 缩放到5个单位)
通过使用不均匀的比例,也可以使球体变形。
(Spawn zone Z旋转45度,缩放为(10,2,5))
1.4 仅表面
我们不一定非要在球体半径范围内选择生成点。通过使用Random.onUnitSphere而不是Random.insideUnitSphere,也可以在球体的表面上获得一个点。通过将surfaceOnly切换字段添加到区域,使该选项成为一个选项。
(只在区域的表面生成)
仅在表面上生成才可以使球体的形状更加明显。
(表面和内部对比)
1.5 可视化区域
现在可以调整生成区域了,但如果可以不生成很多点就能看到其形状就会更好了。通过向SpawnZone添加一个无效的OnDrawGizmos方法,我们可以在场景视图中绘制视觉辅助。这是一种特殊的Unity方法,每次绘制场景窗口时都会调用该方法。
在OnDrawGizmos内,调用Gizmos.DrawWireSphere以绘制球体的线表示,该球体将渲染三个圆。我们需要为其提供位置和半径,我们将使用零向量和1来描述单位球面。
(辅助球体线)
我们还能在游戏窗口中看到Gizmos吗? 是的,在游戏窗口工具栏的右侧有一个Gizmos选项。这仅适用于编辑器,Gizmos不包含在构建中。
默认的Gizmo颜色是白色,但是可以通过更改Gizmos.color属性来使用其他颜色。这有助于将其与其他gizmo区分开。让我们将青色用于我们的生成区gizmo。
(Gizmos换色)
目前,我们的线球体是在原点绘制的,半径为1,与区域的transform无关。默认情况下,Gizmos在世界空间中绘制。要更改此设置,我们必须通过Gizmos.matrix属性指定应使用哪个转换矩阵。可以通过区域的Transform组件的localToWorldMatrix属性获得所需的矩阵。
(和生成区的transform关联)
我们是否需要重置Gizmo的颜色和矩阵? 不用,它们是自动重置的。
2 每个关卡一个区域
现在我们可以配置生成区域了,下一步是使每个关卡都有自己的生成区域。
2.1 迁移到不同场景
通过在层次结构窗口中拖拽,我们可以在打开的场景之间移动对象。使用Spawn Zone对象执行此操作,将其从Main Scene移到Level 1。
(生成区域转移到 Level 1)
该区域现在是关卡的一部分,但是Unity警告我们它检测到跨场景引用。问题是,由于场景可能不会同时打开,因此无法保存不同场景中对象之间的直接引用。当前,Game的生成区域参考指示场景不匹配,保存或播放后将清除它。
(场景不匹配)
游戏需要对生成区域的引用,但是由于我们现在将其存储在其他场景中,因此无法保存此类引用。然后,最简单的更改将是使用公共属性替换spawnZone字段。让我们显式命名为SpawnZoneOfLevel,以表明它不是主场景的一部分,而是关卡场景的一部分。
2.2 查找Game
有人需要设置SpawnZoneOfLevel属性。仅在加载关卡之后才能执行此操作。实际上,每次加载关卡时都必须执行此操作,因为每个关卡必须具有自己的生成区域。问题是谁应该对此负责。
尽管Game控制关卡的加载,但它不能直接访问关卡内容。它需要检索关卡场景的根对象,然后搜索正确的对象。另外,我们可以让该关卡负责在加载SpawnZoneOfLevel属性后对其进行设置。OK开始吧。
为了设置SpawnZoneOfLevel,关卡必须首先以某种方式获取对主场景中Game对象的引用。由于只有一个Game实例,因此我们可以将对它的引用存储在Game类的静态Instance属性中。每个人都可以获取此引用,但是只有Game可以设置它。这是单例设计模式的一个示例。
当我们的游戏实例唤醒时,它应该将自己分配给Instance属性。对象可以通过this关键字获得对自身的引用。
我们不应该强制只存在一个单例实例吗? 一般来说,这是个好主意。但是在我们的特定情况下,我们在主场景中只有一个Game组件实例,该实例仅被加载一次,而从未卸载。如果不是这种情况,那么我们要么在编辑场景时犯了一个错误,要么不只一次加载主场景。
虽然这在进入播放模式和构建时有效,但是static属性不会在编辑器中处于播放模式的编译之间持久存在,因为它不是Unity游戏状态的一部分。为了从重新编译中恢复过来,我们也可以在OnEnable方法中设置该属性。每次启用组件时,Unity都会调用该方法,每次重新编译后也会发生这种情况。
何时准确调用OnEnable? 每次启用一个已经禁用的组件时都会调用它。如果在游戏模式下进行重新编译,则首先会禁用所有活动组件,然后保存游戏状态,进行编译,恢复游戏状态,并再次启用先前的活动组件。你想的是对的,还有一个OnDisable方法,实际上它是在重新编译之前被调用的。
另外,除非组件以禁用状态保存,否则OnEnable会在组件的Awake方法之后立即调用。稍后我们将利用这个事实。
请注意,在关卡更改后也会调用OnEnable,因为在加载关卡时我们会暂时禁用Game。这不会造成问题,因为我们最终用相同的引用替换了旧的引用。
由于我们现在依靠其他代码来访问Game,因此正确隐藏其配置字段是一个好主意。与其使用公共字段,不如使用序列化的私有字段,就像我们已经对factory和spawn区域所做的那样。
我只显示了shapeFactory的更改,但对关键配置字段,存储和关卡计数进行了相同的更改。通常,属性放置在它们适用的任何内容之上,但是由于存在很多字段,因此在这种情况下,我将它们放在同一行上。
2.3 游戏关卡
要使关卡连接到生成区域,我们需要添加代码来执行此操作。虽然我们可以将此功能添加到SpawnZone,但理想情况下,该类应该专用于生成区域,而不负责其他任何事情。它不需要了解游戏的其余部分。因此,我们将创建一个新的GameLevel组件类型来进行设置。它需要知道要使用哪个生成区域,因此为其提供一个配置字段。然后,当它变为活动状态时,使其获取全局可用的Game.Instance属性。它可以用来设置Game的SpawnZoneOfLevel属性。
我们将在“Start”中进行连接,因此它会在加载关卡之后发生。另外,在编辑器中进入播放模式时,将首先加载当前活动的场景。之所以延迟到Start进行,是为了保证Game.OnEnable已经执行并设置Game.Instance,即使Main Scene不是活动场景也是如此。
将具有此组件的游戏对象添加到关卡场景并将其连接到生成区域。
(Game Level 对象)
这意味着Game Level对象保存了对Spawn Zone对象的引用,这是允许的,因为两者都存在于同一场景中。在游戏启动时,Game Level将通过Game.Instance来获取对Game的临时引用,该临时引用用于为Game提供对Spawn Zone的临时引用。因此,GameLevel可以进行连接,并且知道Game和SpawnZone。反过来,Game只知道SpawnZone。最后,SpawnZone根本不了解其他两个。
(对象引用,虚线只存在于运行时)
这是设计依赖项的最佳方法吗? 没有通用的最佳设计方法。在我们的案例中,我们改编了Game的现有spawnZone引用并将其设为属性,引入GameLevel对象来连接事物。我们还可以朝另一个方向发展,并通过静态属性使GameLevel可用,Game将使用该属性来到达生成区域。或者给Game一个GameLevel属性而不是SpawnZone属性,通过它可以间接访问生成区域。 但现在的方法效果很好,因为GameLevel的唯一目的是将生成区域连接到游戏。如果GameLevel获得更多的责任或联系,我们可能需要调整设计。此类代码更改是开发过程的一部分,因此我也将其包含在我的教程中。
同时为level2提供自己的Spawn Zone和Game Level对象。游戏将像以前一样运行,但是现在你可以按关卡调整生成区域。
3 区域类型
由于生成区域具有自己的类,因此现在可以对其进行扩展并创建其他区域类型。例如,除了球体区域,我们还可以添加对立方体区域的支持。
3.1 抽象Spawn Zone
无论特定的生成区域类型如何,它们的通用功能都是提供生成点。SpawnZone类定义了此基础。删除所有特定于球体区域的代码,仅保留SpawnPoint属性的默认定义。
这定义了生成区域的抽象功能。为了使之明确,请使用abstract关键字以及该属性标记该类。
SpawnZone现在是一种抽象类型,无法创建其实例的类。结果,Unity将报错说我们的生成区域组件已失效。我们需要将它们替换为特定的子类。
3.2 Sphere 区域
首先,我们将重新创建球形的生成区组件,但现在将其作为扩展SpawnZone的新SphereSpawnZone类型。与旧代码的唯一不同之处在于,我们必须指出它通过具体的实现覆盖了抽象的SpawnPoint属性。必须通过向其添加override关键字使其明确。
调整Level1场景的Spawn Zone对象,以使其使用此组件。同时恢复游戏关卡的引用,当SpawnZone成为无效组件时,该引用会丢失。level 2也需要修复。
(Sphere zone)
3.3 Cube Zone
接下来,还要创建一个名为CubeSpawnZone的立方体生成区域类型。从生成区域的最小功能开始,生成区域只是返回零向量的SpawnPoint属性。
立方体区域没有比较方便的随机函数,所以我们必须自己构造随机点。单位立方体以原点为中心,边长为一个单位。所以它的体积在每个维度的两个方向上都延长了半个单位。为了在这个空间内获得一个随机的点,我们可以分别为三个向量坐标调用Random.Range(-0.5f, 0.5f),然后转换得到的点。
有一个Gizmos.DrawWireCube方法,因此我们可以使用它来显示立方体区域的Gizmo。它的第一个参数是立方体的中心,而第二个参数是其边缘长度。
(在level2中缩放立方体生成区)
我们还要为立方体区域添加仅表面选项。启用后,我们需要调整生成点,使其最终出现在立方体的一个面上。我们可以通过在立方体内的一个随机点开始然后沿一个轴移动它直到与一个面对齐来做到这一点。轴的索引可以随机选择。
可以使用此索引访问Vector3值,就好像它是一个数组一样,获取或设置其对应的坐标。这样,我们可以使该分量与沿轴的正或负面对齐。我们可以使用原始坐标来决定要选择哪一侧。如果是负数,我们将其移至负数,否则移至正数。这会将点移动到两个面中最近的一个。
(Cube surface only)
3.4 复合区域
最后,让我们创建一个复合的生成区域类型,它由其他生成区域的集合定义。这样就可以创建更复杂的区域,该区域由多个单独的区域(可能是重叠的区域)组成。
添加一个CompositeSpawnZone类,使其扩展SpawnZone,并为其提供一个spawnZones数组字段。
它的SpawnPoint属性从zones数组中选择一个随机索引,然后使用该区域的属性获取生成点。
我们不应该检查数组是否为空吗? 你可以那样做。你还可以检查数组是否存在,因为如果在运行模式下创建组件,则该数组将为null。但是我们的想法是,我们在编辑模式下设计生成区域,并确保它们在进入播放模式或进行构建之前是正确的。因此,当复合生成区域为空时,我们不必担心该怎么办。保留一个null将会是一个错误,并且在尝试检索不存在的数组索引时,Unity将记录一个错误。
创建一个Level3场景,并在Game中增加关卡数,以尝试使用我们新的复合生成区域。确保它还具有一个Game Level对象,该对象获得对生成区域的引用,烘焙其照明并将其包括在构建设置中。
为了使复合区域正常工作,我们必须创建更多其他不同类型的区域。例如,创建两个球体区域和两个立方体区域,分别是一个实体和仅一个曲面版本,因此你可以同时看到它们。将这四个区域拖到复合区域的Spawn Zones 数组字段上。一种快速的方法是在选中复合区域时锁定检查器,方法是单击检查器窗口右上方的锁定图标。然后选择其他四个区域,并将整个选择拖动到数组上。之后,解锁检查器。
(复合区域,展示了所有支持的类型)
属于复合区域的区域可以在同一场景中的任何位置。它们不必是复合区域对象的子对象,但是如果进行转换,则复合区域将影响它们。
(复合区域作为其他区域的父节点)
甚至可以将多个生成区域组件添加到同一个游戏对象,但这样的话,你不能单独转换它们。
除了球形,立方体和复合区域外,你还可以创建更多的生成区域类型。我已经在本教程中包括了最直接的内容。此外,还有仅用于立方体和球体的gizmos 。你需要一些创造力才能显示其他形状的gizmos 。
下一章节,介绍更多的游戏状态。