

200+篇教程总入口,欢迎收藏:
放牛的星星:[教程汇总+持续更新]Unity从入门到入坟——收藏这一篇就够了zhuanlan.zhihu.com
本文重点内容:
1、通过加速区域创建跳板和浮空
2、制作一个多功能区域
3、不同材质的交互以及关闭或者激活对象
4、通过事件触发简单对象插值运动
这是关于控制角色移动的教程系列的第十期。它让环境可以以各种方式和对象运动产生交互。
本教程是CatLikeCoding系列的一部分,原文地址见文章底部。
本教程使用Unity 2019.4.4f1制作。它还使用ProBuilder软件包。

1 加速区域
一个活跃的环境比一个静态的环境更有意思,特别是它们还能对正在发生的行为做出反应的时候。这个行为表示可以对任何事情做出反应,也可以做任何事情,但是一个简单的例子是类似于跳板的东西:每当有东西落在跳板上时,它就会向上弹起。这可以是我们运动的球体,也可以是其他掉落或被推到跳板上的物体。因此,该行为在逻辑上属于跳板。其他物体不需要意识到它的存在,它们只是突然被弹飞起来了。
1.1 Zone 组件
描述跳板行为的最通用方法是,它是一个区域,可加速进入区域的任何物体。因此,我们将创建AccelerationZone组件类型,其可配置的速度不能为负。

区域可以通过添加一个带有触发器碰撞器的对象到场景中来创建,然后将 zone behavior 附加到它上。你也可以添加可视化的跳板对象,但是我只是用半透明的黄色材质使区域可见。

当具有刚体的物体进入区域时,我们应该对其进行加速。为此添加一个OnTriggerEnter方法,该方法将触发并调用新的Accelerate方法。进入该区域的所有物体都被执行,但是如果需要的话,可以使用Layer来防止检测到不需要的处理的物体。



1.2 阻止检测地面
这种简单的方法在发射常规物体时效果很好,但是我们的球体却没有正确发射。相反,它进入该区域时似乎获得了很大的前进速度。发生这种情况是因为我们将其压在了地面上。在这种情况下,可以通过降低“Max Snap Speed ”来解决,但这种方法不适用于设置为低速的加速区域。通常,为了防止被地面捕捉,我们必须指示MovingSphere暂时不要执行捕捉。为此,我们可以向其添加一个公共的PreventSnapToGround方法,该方法将stepsSinceLastJump设置为-1。

现在,如果物体具有MovingSphere组件,则AccelerationZone.Accelerate可以调用此方法,我们可以通过使用Sphere作为输出参数调用TryGetComponent来进行检查和检索。


请注意,这种方法不会重置跳跃阶段,因此在没有着陆的情况下弹跳跳板不会刷新空气跳跃。
1.3 持续加速
瞬时速度变化对于跳板很合适,但是我们也可以使用该区域创建其他连续的加速度现象,例如悬浮区域。我们可以通过简单地添加一个与OnTriggerEnter相同的OnTriggerStay方法来支持这个特性。

如果效果持续时间较长,那么通过适当的加速度来实现速度变化会更好一些,因此让我们向该区域添加一个可配置的加速度,最小还是为零。如果将其设置为零,我们将立即进行更改,否则将应用加速。



也可以施加力,这样质量较大的物体最终加速得较慢,但是固定的加速度使关卡设计变得更容易,因此我使用这个方式。
1.4 任意方向
最后,为了使其可以在任何方向上加速,请在“Accelerate”开始时将体速度转换为区域的局部空间,并在应用时将其转换回世界空间。使用InverseTransformDirection和TransformDirection进行此操作,以便区域的比例不会对其产生影响。现在可以通过旋转区域来控制加速度方向。


2 意识到存在
加速区域只是如何创建具有特定行为的触发区域的一个示例。如果你需要一个做其他事情的区域,你将不得不为它编写新的代码。但是,检测和响应某个地方出现的某些东西的简单行为是如此普遍,我们理想情况下只想编写一次。有很多行为非常简单,比如只是激活一个对象,就为它创建一个专用的组件类型可能就有些设计过渡了。更复杂的行为通常只是几个简单动作的组合。如果关卡设计师可以通过简单的对象来创建它,那会是非常方便的。
2.1 检测区域
让我们首先创建一个DetectionZone组件,该组件检测其区域中是否存在某些东西,并在有物体进入或退出时通知感兴趣的模块。我们通过从UnityEngine.Events命名空间为它提供类型为UnityEvent的onEnter和onExit字段进行配置来实现。

只需让它在OnTriggerEnter和OnTriggerExit中的适当事件上调用Invoke方法。这将触发对事件注册的所有内容的方法调用。

检查器会将组件的事件作为名为On Enter()和On Exit()的列表公开,这些列表最初是空的。名称后面的括号中没有任何内容,表示这些事件没有参数。

2.2 材质选择
为了演示其工作原理,我们将创建一个简单的MaterialSelector组件类型,该组件类型具有可配置的材质数组和MeshRenderer参考。它具有一个带有索引参数的公共Select方法,该方法将有效的材质分配给渲染器(如果有效的话)。

创建一个带有红色非活动区域和绿色活动区域的材质选择器组件,这将用于更改检测区域的可视化。虽然不需要将其添加到受影响的游戏对象中,但这仍然是有意义的。

现在,通过按项目的+按钮将其添加到检测区域组件的输入事件列表中。通过材质选择器的左下角字段将游戏对象链接到该项目。之后,可以选择MaterialSelector.Select方法。由于此方法具有整数参数,因此其值将显示在方法名称下方。默认情况下,它设置为零,表示不活动状态,因此将其设置为1。然后对退出事件执行相同的操作,这次将参数保留为零。

区域对象默认使用不活动的红色材质。只要有物体进入区域,将切换材质到绿色。当有东西离开这个区域时,它又会变成红色。

2.3 最开始进入和最后退出
该检测区域可以工作,并确实可以完成其编程的目的,即每次进入时调用一次进入,每次离开时调用一次退出。因此,我们可以混合使用enter和exit事件(例如enter,enter,exit,enter,exit,exit),并且当其中仍然有东西时,最终会出现视觉上无效的区域。在区域中保持活动状态时,使区域保持活动状态更加直观。使用保证进入和退出事件将严格交替的区域进行设计也更加容易。因此,它仅应在第一件东西进入时和最后一件东西离开时发出信号。重构事件重命名为onFirstEnter和onLastExit可以使这一点变得清晰,这将需要再次连接事件。

为了使这种行为成为可能,我们必须跟踪区域中当前的碰撞体。通过为DetectionZone提供一个List

该列表如何工作?
请参阅“对象管理”系列的“持久对象”教程。
在OnTriggerEnter中,只有在列表为空时才调用enter事件,然后始终将碰撞器添加到列表中以跟踪它。

在OnTriggerExit中,我们从列表中移除碰撞器,并且只有在列表为空时才调用退出事件 列表的Remove方法返回删除是否成功 这应该总是这样的,因为否则我们就无法追踪碰撞器。


2.4 检测突然出现和消失的物体
不幸的是,OnTriggerExit不可靠,因为在停用,禁用或销毁游戏对象或其碰撞器时便不会再调用它。不应该单独禁用碰撞器,因为那样会导致物体掉落到几何体中,因此我们将不支持这种方法。但是我们应该能够处理整个游戏对象在区域内时被禁用或销毁的情况。
在每一个物理步长中,我们都要检查区域内的碰撞器是否仍然有效。添加一个在碰撞器列表中循环的FixedUpdate方法。如果一个碰撞器计算为false,这意味着它或它的游戏对象已经被销毁。如果不是的话,我们就需要检查它的游戏对象是否被禁用了,这一点我们可以通过它的游戏对象的active属性来发现。如果碰撞器不再有效,则将其从列表中删除并递减循环迭代器。如果列表为空,则调用退出事件。

大多数情况下,检测区域中没有物体。为了避免不必要地连续调用FixedUpdate,我们可以在组件唤醒时和最后一个碰撞器退出后禁用该组件。然后我们只有在有东西进入后才启用它。之所以这样有效,是因为无论是否启用行为,总是会触发触发器方法。

接下来,我们还应该处理区域对象自身被停用或销毁的情况,因为当事件仍在区域中时发生时,调用退出事件是有意义的。我们都可以通过添加一个OnDisable方法来完成这两项工作,该方法清除列表并在列表不为空时调用exit事件。

请注意,检测区的组件不应由其他代码禁用,因为它可以管理自己的状态。一般规则是不要禁用检测区域组件,也不要禁用任何可能影响该区域的碰撞器。这些游戏对象应全部停用或销毁。
2.5 热重载
因为热重载(在编辑器播放模式下重新编译)将调用OnDisable,所以它违反了我们刚刚声明的规则。这将导致退出事件被调用以响应热重载,此后已经在区域中的对象会被忽略。幸运的是,我们可以在OnDisable中检测到热重载。如果同时启用了该组件并且游戏对象处于活动状态,则我们将进行热重载,并且什么也不做。当游戏对象没有被销毁而组件被销毁时,情况也是如此,但是我们仍然什么都不做。
我们只需要在编辑器中播放时进行检查,就可以将代码包装在#if UNITY_EDITOR和#endif中。

OnDisable中有哪些相关状态组合?
如果禁用了该组件,仅仅是禁用或反激活游戏对象,则应该继续进行。否则,如果游戏对象未处于活动状态,则该游戏对象将被停用或销毁,应该继续。否则,要么是热重载,要么是仅组件被销毁,则将其忽略。
2.6 更复杂的行为
这只是通过事件可以完成的简单演示。你可以通过将更多条目添加到事件列表来创建更复杂的行为。甚至不必为此创建新方法,直接使用现有方法。而限制则是它必须是与事件的参数列表匹配的无效方法或属性设置器,或者最多具有一个可序列化的参数。例如,我进行了一些设置,以便在更改检测区域本身的可视化效果的同时,在检测区域内有东西时关闭悬浮区域。


您必总是对所有事件都响应。有时候可能只有在进入或退出时才触发某些事件。例如,在进入区域时激活某些内容。然后退出并不会取消激活它,而重新进入则会再次激活它,虽然二级激活实际上没有任何用处。
这种基于事件的方法可以用于整个游戏吗?
从理论上讲,是的,它对于快速原型制作非常有用,但是却很麻烦。一旦发现自己重复了复杂的模式,便有必要为其创建专用的方法或行为,这种方法或方法应该更容易使用,并在以后必要时进行优化。
3 简单运动
我们将在本教程中介绍的最后一种情况是移动环境对象。复杂的运动可以通过动画来完成,可以通过检测区域触发。但是通常两点之间的简单线性插值就足够了,例如,对于门,电梯或浮动平台。现在,让我们添加对此的支持。
3.1 自动滑动条
无论插值什么,它在概念上都由从0到1的滑块控制。如何更改值是与插值本身不同的问题。保持滑块分离还可以将其用于多个插值。因此,我们将创建一个专用于该值的AutomaticSlider组件。它的可配置持续时间必须为正。当我们使用它为物理对象设置动画时,我们将使其在FixedUpdate方法中增加其值,并确保它不会溢出。一旦值达到1,我们就可以完成并可以禁用滑块。

再一次,我们将使用Unity事件使它能够附加行为到滑动条。在本例中,我们需要一个随值变化的事件,我们将使用它来传递滑块的当前值。所以我们的事件需要一个浮点参数,可以使用UnityEvent

但是,Unity无法序列化通用事件类型,因此该事件不会显示在检查器中。我们必须创建自己的具体可序列化事件类型,该事件类型只是扩展UnityEvent

进入播放模式时,滑块将立即开始增加。如果你不希望这样做,请在默认情况下将其禁用。然后,你可以将其连接到检??测区域,以在以后启用它。

请注意,在这种情况下,事件的名称后跟(Single),表示它具有一个参数。单精度是指浮点类型,它是单精度浮点数。
3.2 位置插值
接下来,创建一个PositionInterpolator组件类型,该类型通过带有float参数的公共Interpolate方法在两个可配置位置之间插值可配置刚体的位置。使用Vector3.LerpUnclamped,以使提供的值不会被钳位,而是由调用者决定。我们需要通过其MovePosition方法更改身体的位置,以便将其解释为运动,否则将成为闪现。


通过将sider和interpolator都添加到同一平台对象,我创建了一个简单的移动平台。插值器的Interpolate方法的动态版本绑定到滑块的事件,这就是为什么其值没有字段的原因。然后,我将滑块连接到检测区域,以便在有物体进入该区域时激活平台。请注意,插值点在世界空间中。

3.3 自动倒置
我们可以通过向AutomaticSlider添加可配置的自动反向切换来使插值来回移动。这需要我们跟踪它是否反转,并在FixedUpdate中加倍代码,同时必须支持双向。同样,当自动反转激活时,我们必须跳动而不是钳制该值。在持续时间极短的情况下,这可能会导致溢出,因此反弹后我们仍然会钳住。



3.4 平滑步长
线性插值的运动是刚性的,反转时速度会突然变化。通过将值的平滑变体传递给事件,可以使其加速和减速。通过对其应用smoothstep函数来实现。并使它成为可配置的选项。




3.5 更多控制
可以通过检测区域事件,并禁用滑块组件来暂停动画,但让我们也可以控制其方向。最简单的方法是通过公共属性提供其反转状态。将反向字段替换为自动反向属性,调整其他代码的大小写以使其匹配。

让我们对自动反转选项执行相同的操作。在这种情况下,我们必须保留序列化字段,因此添加一个显式属性。



请注意,方向反转是突然的,因为它仍然是简单的插值。如果要在任何时候平稳停止和反转,则需要创建使用加速度和速度的更复杂的逻辑。
3.6 压碎的碰撞体
移动场景的危险在于,物体最终可能会陷入两个接近的碰撞器之间。当碰撞器之间的缝隙关闭时,身体要么被弹出,要么最终被压入碰撞器或穿过碰撞器。如果碰撞表面成一定角度,则存在清晰的逃生路径,物体将朝该方向被推动。如果不是这样,或者如果没有足够的时间逃脱,则物体最终会被压碎,穿透碰撞体。如果一个物体卡在两个足够厚的简单碰撞器之间,那么它可以留在它们内部,一旦有一条清晰的道路就弹出。否则会掉下去。

如果碰撞表面成一定角度,则物体会被推到一边,并且很有可能逃脱。因此,通过在表面之间留出足够的空间或通过引入倾斜的碰撞器(无论是否可见)来设计这样的配置是一个好主意。此外,将box碰撞器隐藏在地板上可以使它更牢固,以免物体被推入。或者,添加一个区域,在适当的时候触发该区域的销毁,表示它被压碎了。


3.7 局部插值
世界空间中的配置可能会带来不便,因为它无法在多个位置用于同一动画。因此,让我们通过在PositionInterpolator中添加一个局部空间选项进行总结。为此,我们添加了一个可选的可配置的Transform,该插值相对于应该发生的插值。通常用插值器引用对象,但这不是必需的。



下一章节,滚动。
欢迎扫描二维码,查看更多精彩内容。点击 阅读原文 可以跳转原教程。
本文翻译自 Jasper Flick的系列教程