Unity基础教程系列(七)——可配置形状(Variety of Randomness)

2020-10-21 10:41:41 浏览数 (1)

本文重点: 1、让形状旋转和移动 2、集中控制游戏Update 3、每个生成区域可配置化 4、提高检视面板便捷度

这是有关 对象管理 的系列教程中的第七篇。它为形状增加了一些行为,并可以针对每个生成区域配置它们。

本教程是CatLikeCoding系列的一部分,原文地址见文章底部。原创标识仅表示原创翻译。如果因此导致其他人翻译不便之处可以联系本人开白,不显示来源。

本教程使用Unity 2017.4.12f1制作。

(每个生成区域都在做自己的事情)

1 形状旋转

我们可以创造出外观各异的形状,但它们只是原地不动,直到被销毁。现在让他们做点事情来增添趣味性。比如,让所有的形状旋转起来。

1.1 添加旋转

使对象旋转的最直接方法是调用其Transform组件的Rotate方法,就像我们对RotatingObject所做的那样。这个时候,我们需要向Shape添加一个FixedUpdate方法并调用它。首先先使用对象的本地的 forward 方向作为其旋转轴。

(旋转形状)

默认的时间步长是0.02,这意味着FixedUpdate每秒被调用50次。因此,我们最终得到了每秒旋转50°的形状。但是,通过将forward 矢量乘以50,再乘以时间增量,可以使转速明确。并且这会让旋转与时间步长无关。

1.2 随机旋转

下一步是给每个形状一个随机的角速度。添加一个公共AngularVelocity属性,使其可以进行配置,然后使用该属性来确定每次Update的旋转程度。

现在,游戏必须在CreateShape中设置形状的角速度。我们可以使用Random.onUnitSphere获取随机旋转轴。将其乘以50,所以我们再次以每秒50°的旋转结束。

(随机角速度)

为了使旋转速度也随机化,请将50替换为随机范围,例如每秒0到90度之间。

1.3 保存角速度

此时,我们还没有保存角速度。加载游戏还是会得到具有任意角速度的形状,因为回收的形状会保持其原有速度。因为保存角速度需要更改文件格式,所以请将保存版本增加到4。

在形状的颜色之后写下角速度。

保存的版本足够高时,还要加载时读取角速度。保存较早的游戏因为没有角速度,请使用零向量。

1.4 一次性更新所有形状

在添加形状旋转功能之前,我们不需要更新形状。但是现在,Unity必须调用所有活动形状的FixedUpdate方法。虽然常规方法调用并不会造成实际问题,但是FixedUpdate和其他特殊的Unity方法需要额外的开销,这可能会使对象变慢。当只有几个活动形状时,这不是问题,但是在处理许多形状时可能会成为性能瓶颈。

(Profiler 展示1000个独立的FixedUpdate调用)

与其将形状更新的责任留给Unity,不如我们自己进行管理。游戏已经包含所有活动形状的列表,正好可以用来更新它们。但是我们不能使用FixedUpdate,因为不管如何,Unity都会调用同名方法,所以必须将其重命名为其他名称。这里我们修改为GameUpdate,并将其公开,以便Game可以访问它。

在Game的FixedUpdate方法中,遍历形状列表并调用每个形状的新GameUpdate。在生成新形状之前,一开始就执行此操作。这样可以使行为与我们游戏的早期版本保持一致。

(所有的Update都统一起来了)

这种优化真的值得吗? 当你处理成千上万个相似的对象时,它们都需要更新,而你自己也已经对其进行了跟踪,那么这样做是值得的。你需要去了解性能会消耗多少,因为它随目标平台的不同而不同。优化在编辑器下可以获得最大收益。请注意,如果你发现自己正处于这种情况下时,可以关注下Unity 2018中引入的实体组件系统(ECS)是否更合适也是一个好主意,但本教程不会对此进行介绍。

2 形状运动

现在,我们的形状可以旋转了,但它们仍保留在它们产生的位置。让我们通过给每个形状一个随机的速度来改变它。

2.1 增加速度

就像我们做角速度一样,也给形状一个速度属性。

每个Update中,将速度乘以时间增量设置到形状的位置。我们可以使用局部位置而不是更昂贵的position属性,因为形状始终是根对象。

2.2 保存速度

保存速度,直接在角速度之后写入速度。

并从旧文件读取时再次使用零向量加载它。

2.3 随机化速度

在CreateShape中创建新形状时,通过将Random.onUnitSphere与Random.Range相乘,例如以每秒0~2个单位的速度,给它一个随机的方向和速度。

(随机速度)

2.4 逐个生成区域的速度

给每个形状一个随机的运动方向会产生一个相当混乱的场景。相反,我们可以让所有形状都朝着同一个方向移动。但不是只使用单一的统一方向,我们可以给每个生成区使用一个独特的速度。这会让创建更精细的关卡成为可能。

当前,游戏会创建并配置每个新形状,并要求关卡提供生成点。如果我们也想让速度取决于生成区域,那么Game也必须能得到一个速度。但与其这样做,不如将整个形状配置责任从Game移到SpawnZone。

添加一个公共ConfigureSpawn方法到生成区,附带一个形状参数。从Game中复制代码。创建实例并将其添加到列表,第一行和最后一行除外。方法的参数替换了实例变量,现在可以直接访问SpawnPoint,而不必经过level了。

在GameLevel中,删除SpawnPoint属性,并添加一个ConfigureSpawn方法,该方法将直接转发到其生成区域的方法。

至此,所有对象仍然像以前一样工作,只是现在由SpawnZone配置形状了。

2.5 相对速度

现在我们已经在SpawnZone内配置了形状,我们可以访问该区域的transform数据了。我们可以利用这一点让形状的速度和区域的朝向产生关系,就像形状的位置也是相对的一样。使用区域的local forward方向,乘以一个随机速度试试。

(形状按照Forward方向运动)

对于球形和立方体区域,这可以按预期工作,但在使用复合生成区域时,则无法工作。因为它使用复合生成区域本身的forward方向,而不是其子区域的forward方向。为了使此工作有效,就像对SpawnPoint一样,CompositeSpawnZone必须重写ConfigureSpawn才能将调用转发到其各个子区域。可以从该属性复制代码,仅在最后更改其功能即可。

为了能够覆盖ConfigureSpawn,我们必须在SpawnZone中将其标记为virtual 。

(每个子区域相对运动)

3 逐个生成区域的配置

将形状配置的职责从Game迁移到SpawnZone不仅使设置相对移动方向变得容易。每个生成区还可以使用不同类型的移动。

3.1 移动方向

首先,让我们可以在向前或向上移动方向之间进行选择。要明确显示此选择,请创建SpawnMovementDirection枚举类型。由于此类型仅在每个生成区域的形状配置的上下文中才有意义,因此请在SpawnZone类中定义它,而不是将其放在自己的脚本文件中。然后为SpawnZone提供此类型的配置字段。

嵌套类型必须声明为public吗? 不是,但是也没有令人信服的理由对其进行保护。当直接与类外的枚举一起使用时,例如对于自定义编辑器,可能需要将其公开。在SpawnZone类及其扩展类之外,可以通过其完全限定的名称SpawnZone.SpawnMovementDirection访问枚举类型。

现在我们可以在ConfigureSpawn中检查移动方向是否设置为向上。如果是的话,请使用transform.up,否则请继续使用transform.forward。

(方向选择)

3.2 向外运动

除了选择一致的移动方向外,还可以使形状从生成区域的中心移开。为此,在枚举中添加一个“Outward ”选项。

向外移动的正确方向是通过从形状的位置减去区域的位置并将结果归一化来找到的。注意,我们必须使用transform.position,而不是本地位置,因为生成区域不需要是根对象。而且,这种关系在构造后不会持续,所以如果区域恰好移动,方向也不会改变。

3.3 随机运动

让我们也支持随机方向,这就是我们开始的方式。将随机添加到枚举。

并使用Random.onUnitSphere生成随机方向向量。

(随机运动)

3.4 速度范围

除了运动方向,我们还可以控制速度范围。我们需要的是最低速度和最高速度的配置字段。

(速度范围在1.5和2.5之间)

必须使用两个字段来控制单个范围是不方便的,尤其是如果我们以后要添加更多范围的时候。Unity没有浮点数的范围类型,所以让我们自己做一个。使用公共最小和最大浮点字段创建一个名为FloatRange的结构类型。本质上,它是一个Vector2,具有适当命名的字段,并且没有与矢量相关的功能。而是给它提供一个方便的RandomValueInRange属性,该属性负责对Random.Range的调用。

请注意,FloatRange并非特定于形状配置,而是像往常一样在其自己的脚本文件中定义。

要使Unity保存浮动范围值,请使用Serializable属性标记类型。该属性存在于System名称空间中,但是该名称空间还包含一个Random类型,该类型与Unity的版本冲突。为了避免这种情况,只需编写System.Serializable而不是使用名称空间。

现在,我们可以在SpawnZone中使用一个FloatRange字段。

(速度范围)

3.5 隔离配置

我们还可以创建一个类型,以包含所有用于生成的配置选项。这样可以将它们整齐地分组在一起,从而使我们不必在所有字段前都添加spawn。因此,在SpawnZone中定义一个可序列化的SpawnConfiguration结构类型,并将相关字段以及枚举类型放入其中,并删除其前缀。然后,SpawnZone仅需要单个生成配置字段。

SpawnConfiguration不应该是一个类吗? 关键点是将数据分组在一起,同时将其保留在SpawnZone对象中,这正是结构类型所做的事情。作为一个类,数据将作为其自己的对象存在于内存中的其他位置,而spawnConfig将是对该对象的引用。如果我们要传递配置,那么一个类将是适当的,但是我们不会这样做。

调整ConfigureSpawn中的引用以使其匹配。此时,由于移动方向名称变得很长,因此可以方便地用开关块替换if-else序列。

(配置字段)

Switch如何工作? Switch块是一种基于单个变量或字段进行分支的古老方法。它使用标签来控制执行流程。每个标签均由大小写定义,后跟一个值和一个冒号。如果用于切换的值与标签匹配,则代码执行将跳至该标签之后。还有一个特殊的默认标签,当其他标签都不匹配时使用。

它必须使用break或return语句结束相关的代码段,而不是针对每种情况使用代码块。

在功能上与

一样。

除此之外,还可以一起声明多个标签,如case1:case2:DoAB();break; 等于if(x == 1 || x == 2){DoAB(); }。也可以使用goto跳转到另一种情况。但是这种用例很少见。我在这里只使用它来使代码行更短,而不必重复spawnConfig.movementDirection。

3.6 重写符合区域

请注意,现在所有的生成区域类型都有生成配置选项,因此也有复合生成区域。我们可以使用它来覆盖其子区域的配置。将开关添加到CompositeSpawnZone以使其可选。如果需要覆盖它,则让它调用ConfigureSpawn的基本实现,而不是将其转发到子区域之一。然后它将使用自己的配置,同时仍从子区域中选择一个生成点。

(一次性给所有区域配置)

4 高级配置

现在,我们已经创建了一种配置每个区域的生成运动的方法,我们可以扩展这种方法。当然还有更多可以控制的事情,可以进一步改善这些选项的表示方式。

4.1 角速度和缩放

要配置的其他候选对象是形状的旋转速度和比例。将两者的FloatRange字段添加到SpawnConfiguration并在ConfigureSpawn中使用它们。

(占用了很多空间)

我们还可以添加更多选项,比如控制角度旋转轴的方法,但问题是配置的检查器很快就会变得又大又笨拙。当展开时,每个浮动范围会占用三行,这是有点浪费空间。如果每个浮动范围都能容纳在一行中就更好了。

4.2 Custom Property Drawer

通过为其创建自定义属性Drawer,我们可以覆盖Unity绘制FloatRange值的默认方法。为此添加一个FloatRangeDrawer类。与编辑器打交道时,其文件应放在“Editor”文件夹中。这告诉Unity将其与所有其他与编辑器有关的代码进行编译和组合,并使其脱离构建。

(一个编辑器脚本)

编辑器类依赖于来自UnityEditor名称空间的东西,所以除了使用UnityEngine之外,还要使用它。要使类成为属性折叠项,它必须继承自PropertyDrawer类。

除此之外,我们必须告诉Unity我们想为什么类型创建一个Custom Property Drawer。这是通过向我们的类添加CustomPropertyDrawer属性完成的。我们需要将相关类型作为参数提供给它,可以通过typeof来指定它。

现在,Unity每次必须显示FloatRange值的UI时,都会调用PropertyDrawe的OnGUI方法。我们需要重写该方法才能创建自己的UI。它具有三个参数:一个Rect定义要插入的区域,一个SerializedProperty表示浮动范围值,以及一个GUIContent,其中包含要使用的默认标签。一开始可以将方法留空。

位置不应该命名为area,rect或类似名称吗?

那会更有意义,因为它实际上描述的是矩形UI区域,而不仅仅是位置。但是Unity一直使用Position,因此我也会这样做。

(空行)

因为我们没有在OnGUI中做任何事情,所以什么也没画。但是默认属性为其自身保留了一行,因此我们的生成配置的检查器已经缩小到所需的大小。

首先,通过调用带有与OnGUI相同参数的EditorGUI.BeginProperty来告诉Unity编辑器我们正在为属性创建UI,仅交换标签和属性。完成后,我们将调用EditorGUI.EndProperty。 我们将在这些调用之间创建UI。 尽管它似乎啥也没做,但这可以确保编辑器将能够处理预制件和预制件的替代品。

我们的浮动范围属性由两个子属性组成,即最小和最大浮动。可以通过在属性上调用FindPropertyRelative来访问它们,并使用适当的名称作为字符串参数。这再次给了我们一个SerializedProperty实例。显示此类属性的UI的最简单方法是使用位置和属性作为参数来调用EditorGUI.PropertyField。这样做是为了获得最小值。

(只有最小值)

我们最终得到每个范围的最小值,它可以编辑。我们也使用相同的方法添加最大值。

(最大值和最小值叠加了)

最小值和最大值字段的UI最终彼此绘制在一起,因为我们对两者使用了相同的位置设置。绘制属性时,Unity为我们提供了一个要绘制的矩形区域,因此我们必须自己进行布局。现在,我们可以简单地将区域的宽度减半,然后将第二个字段的水平坐标增加至它的宽度。

(最小和最大值 靠在一起了)

接下来,我们需要为范围添加标签。这是通过调用带有给我们的位置和标签的EditorGUI.PrefixLabel来完成的。当标签占用空间时,该方法返回一个修改后的区域,该区域为我们提供了其余UI的剩余空间。

(增加前面的描述文字)

这会弄乱我们的布局,因为Unity使用固定宽度的标签,它对于我们的min和max字段来说太宽了。我们可以通过设置EditorGUIUtility.labelWidth属性来覆盖该宽度。让将其设置为每个字段使用的宽度的一半。

(重新设置标签大小)

看起来已经不错了,但这仅是因为我们的范围字段最后缩进了一步。Unity全局跟踪UI缩进,但是我们可以通过设置EditorGUI.indentLevel属性来覆盖它。确保将其设置为1,这样会将标签文本向右推动一步。

(选中的属性标签也高亮显示)

请注意,选择输入字段后,相应的标签变为蓝色。但是,当选择最小字段时,其范围的标签也会变为蓝色。这是因为它们最终具有相同的UI控件ID。我们可以通过在调用PrefixLabel时添加特定的控件ID作为参数来避免这种情况。选择整个范围是没有意义的,因此请使用GUIUtility.GetControlID(FocusType.Passive)。这样可以防止它变成蓝色,并在你使用Tab键在编辑器中逐步浏览UI控件时可以将其跳过。

(现在只会高亮选中框了)

最后,完成后,我们应该将缩进级别和标签宽度恢复为其原始值。这里其实不恢复也可以,因为Unity的默认编辑器会为我们恢复值,但是我们通常不应该依赖它。

4.3 配置颜色

我们可以配置的另一件事是允许的随机颜色范围。到目前为止,该功能已经完成了,但是我们可以使用刚完成的整洁的浮动范围来对其进行配置。实际上,我们可以创建一个专用的ColorRangeHSV结构以包含这些范围,并提供便利的属性以从中获得随机颜色。再次像FloatRange一样,此结构独立存在,并不特定于生成配置。

现在将颜色配置添加到SpawnConfiguration只需向其添加ColorRangeHSV字段即可。

现在,ConfigureSpawn可以使用new属性,而不必担心创建随机颜色的细节。

(现在拥有颜色的选择项了)

4.4 范围滑动条

色相,饱和度和值都必须介于0到1之间,因此不允许使用任何其他值。如果它们是简单的float字段,那么我们可以使用Range属性在编辑器中强制执行此操作,将输入字段转换为滑块。

(范围的属性没有生效)

但这没有生效,因为Range仅适用于float或int。因此,让我们创建我们一个自己的属性。这是通过定义扩展PropertyAttribute的类来完成的。约定是将Attribute作为后缀添加,因此我们将其命名为FloatRangeSliderAttribute。尽管我们仅在编辑器中使用此元数据,但不得将其脚本文件放置在Editor文件夹中,因为我们将在ColorRangeHSV中使用此类型。

该属性只是最小和最大两个属性的容器。它们应该是公共可读的,但仅由属性本身设置即可。

添加具有最小值和最大值作为参数的构造方法,以初始化属性。为了使范围合理,请强制最大值不能小于最小值。

现在,我们可以使用自己的属性代替Range。作为属性,我们可以将其称为FloatRangeSlider,而忽略属性后缀。

这本身并不会改变浮动范围的绘制方式,因为我们所做的只是将一些元数据附加到字段定义中。我们必须创建另一个

custom property drawer,这次是为FloatRangeSliderAttribute而不是为FloatRange。再次从basic drawer开始,让UI保留空白。

在绘制属性之前,Unity编辑器会检查是否存在适用于附加到其上的的drawer。如果是这样,它将使用那个。否则,它将检查是否存在适用于属性类型的drawer并使用该drawer。如果没有,它将使用其默认drawer。因此属性优先,而我们再次以空结尾。

我们仍然需要访问min和max属性,但是这次我们要绘制一个滑块来指示一个范围,而不是两个单独的float字段。因此,请保留变量。

我们可以通过floatValue属性访问min和max的float值。首先,我们必须得到它们,然后在显示了范围滑块之后,我们必须对其进行设置,以防它们被更改。Unity将负责检测更改并为我们支持撤消和重做。

接下来,我们需要知道要显示的滑块的限制,该限制存储在属性中。我们可以通过PropertyDrawer的attribute属性访问它。属性的类型是PropertyAttribute,因此我们必须通过将属性编写为FloatRangeSliderAttribute来将其强制转换为我们自己的类型。

现在,通过调用EditorGUI.MinMaxSlider,我们具有绘制滑块范围所需的全部功能。作为参数,我们将使用位置和标签,然后是最小值和最大值,最后是最小值和最大值限制。因为最小值和最大值可以通过滑块更改,所以我们必须通过在它们前面放置ref来提供它们作为参考参数。这就使它们成为对变量的引用(就像它们是对象而不是浮点数一样),因此MinMaxSlider可以更改它们。这是必需的,因为方法不能返回两个值。

(滑块的范围设置为0~1)

4.5 滑块值

尽管滑块不错,但无法指定确切的值(极值除外)。这可能不是问题,因为颜色不需要精确,但是它使得无法检查要复制的一个滑块的值以用于其他地方。因此,我们也为最小值和最大值添加常规输入字段。

首先,我们将从滑块上删除标签,这使得可以将其放置在两个float字段之间。只需从MinMaxSlider的调用中删除label参数。

(没有标签的滑动块)

接下来,我们必须像以前一样使用PrefixLabel分别绘制标签。另外,我们不希望缩进级别与布局混淆,因此在标签后将其设置为零,并在完成后将其重置。

我们将从在三个部分之间平均分配剩余空间开始。首先使用EditorGUI.FloatField绘制一个最小的float输入字段,不带标签。它返回可能更改的值。之后是滑块,然后是最大输入字段。

(滑动块 带有值域)

我们可以通过将滑块的一半宽度专用于滑动块,使滑动字段各占四分之一来改善布局。另外,如果在滑块和浮点之间添加一些填充,则效果会更好。为此,请从浮动字段的宽度中减去四个像素,然后移动水平位置进行补偿。

(更好的布局)

最后,我们强制要求直接输入字段不能超出限制,并且max永远不会小于min。

下一个章节,更多的工厂。

0 人点赞