您现在的位置是:首页 > 文章详情

喵的Unity游戏开发之路 - 互动环境(有影响的运动)

日期:2020-09-04点击:456



前言


        很多童鞋没有系统的Unity3D游戏开发基础,也不知道从何开始学。
为此我们精选了一套国外优秀的Unity3D游戏开发教程,翻译整理后放送给大家,教您从零开始一步一步掌握Unity3D游戏开发。 
本文不是广告,不是推广,是免费的纯干货!
本文全名:喵的Unity游戏开发之路 - 移动 - 互动环境  - 有影响的运动




互动环境
有影响的运动


  • 通过加速区创建跳板和悬浮力。

  • 制作一个多功能检测区。

  • 反应性地交换材料并激活或停用对象。

  • 通过事件触发的简单插值移动对象。

这是有关控制角色移动的教程系列的第十期。它使环境能够以各种方式对运动做出反应。

本教程使用Unity 2019.4.4f1制作。它还使用ProBuilder软件包。


效果之一


修正

我改进了轨道摄像机的1.4节“使焦点居中”,以便更好地实现焦点居中和焦点半径限制的相互作用。调整OrbitCamera.UpdateFocusPoint如下:


 void UpdateFocusPoint () { previousFocusPoint = focusPoint; Vector3 targetPoint = focus.position; if (focusRadius > 0f) { float distance = Vector3.Distance(targetPoint, focusPoint); float t = 1f; if (distance > 0.01f && focusCentering > 0f) { t = Mathf.Pow(1f - focusCentering, Time.unscaledDeltaTime); } if (distance > focusRadius) { t = Mathf.Min€(t, focusRadius / distance); } focusPoint = Vector3.Lerp(targetPoint, focusPoint, t); } else { focusPoint = targetPoint; } }

我还更改了“移动地面”部分2.3确定运动,因此忽略了质量较轻的连接物体。这样可以防止球体自动跟随其推开的轻物体。如下调整MovingSphere.UpdateState结尾


 if (connectedBody) { if (connectedBody.isKinematic || connectedBody.mass >= body.mass) { UpdateConnectionState(); } } 


最后,更改了“攀爬”第2.5节的“可选攀登”,以防止自动粘在动画的可攀爬表面上。这是通过在EvaluateCollision中调节desiresClimbing而不是在Climbing属性中完成的:


 bool Climbing =>climbContactCount > 0 && stepsSinceLastJump > 2;
void EvaluateCollision (Collision collision) { if ( desiresClimbing &&upDot >= minClimbDotProduct && (climbMask & (1 << layer)) != 0 ) { climbContactCount += 1; climbNormal += normal; lastClimbNormal = normal; connectedBody = collision.rigidbody; } }


(另一个)效果




和环境互动。


加速区



主动环境比静态环境有趣,尤其是当它对正在发生的事情做出反应时。这种行为可以对任何事情做出反应,也可以做任何事情,但是一个简单的例子就是跳垫:只要有东西落在垫上,它就会向上发射。这可能是我们的运动球或碰巧掉落或推到垫子上的任何其他物体。因此,该行为在逻辑上属于跳板。其他物体不必知道它的存在,它们只是突然结束飞行。


区域组成



描述跳板行为的最通用方法是,该区域可加速进入其的任何物体。因此,我们将创建一个AccelerationZone组件类型,其可配置的速度不能为负。

using UnityEngine;
public class AccelerationZone : MonoBehaviour {
[SerializeField, Min(0f)] float speed = 10f;}

可以通过将具有触发对撞器的对象添加到场景,然后将区域行为附加到场景来创建区域。您也可以添加可视化跳板的对象,但是我只是用半透明的黄色材料使该区域可见。

加速区组件。

当带Rigidbody的东西进入区域时,我们应该加速它。为此在OnTriggerEnter添加一个方法Accelerate,该方法以触发主体作为参数调用新方法。进入该区域的所有物体都会发生这种情况,但是如果需要,您可以使用图层来防止检测到某些物体。

void OnTriggerEnter (Collider other) { Rigidbody body = other.attachedRigidbody; if (body) { Accelerate(body); } }
void Accelerate(Rigidbody body) {}

Accelerate中只需使身体速度的Y分量等于配置的速度,除非它已经更大。其他速度分量不受影响。

void Accelerate(Rigidbody body) { Vector3 velocity = body.velocity; if (velocity.y >= speed) { return; }
velocity.y = speed; body.velocity = velocity; }

将东西推入区域。


防止猛然掉地



当发射常规物体时,这种简单的方法效果很好,但是我们的球体没有正确发射。当它进入区域时,它似乎获得了很大的前进速度。发生这种情况是因为我们将其卡在了地上。在这种情况下,可以通过降低“ 最大捕捉速度”来解决,但不适用于设置为低速的加速区域。为了防止接地,一般来说,我们必须指示MovingSphere暂时不要执行接地。我们可以通过PreventSnapToGround向其添加设置stepsSinceLastJump为-1 的公共方法来做到这一点。

public void PreventSnapToGround () { stepsSinceLastJump = -1; }

现在AccelerationZone.Accelerate可以在主体具有MovingSphere组件的情况下调用此方法,我们可以通过调用TryGetComponent球体作为输出参数来进行检查和检索。

 void Accelerate(Rigidbody body) { if (body.TryGetComponent(out MovingSphere sphere)) { sphere.PreventSnapToGround(); } }

启动。

请注意,这种方法不会重置跳跃阶段,因此在没有降落的情况下弹跳跳板不会刷新空气跳跃。


持续加速



瞬时速度变化对于跳板很合适,但是我们也可以使用该区域创建其他连续的加速度现象,例如悬浮区域。我们可以通过简单地添加与OnTriggerStay相同的方法OnTriggerEnter来支持这一点。

void OnTriggerStay (Collider other) { Rigidbody body = other.attachedRigidbody; if (body) { Accelerate(body); } }

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

 [SerializeField, Min(0f)] floatacceleration = 10f,speed = 10f;

void Accelerate(Rigidbody body) {
if (acceleration > 0f) { velocity.y = Mathf.MoveTowards( velocity.y, speed, acceleration * Time.deltaTime ); } else { velocity.y = speed; } }

悬浮区 空气加速1。

也可以施加力,这样质量较大的物体最终的加速度会变慢,但是固定的加速度使水平设计更容易,因此我使用了这一点。


任意方向



最后,为了使其可以向任何方向加速,请在Accelerate开始时将人体速度转换为区域的局部空间,并在应用时将其转换回世界空间。通过InverseTransformDirection和TransformDirection这样做,因此区域的比例不会对其产生影响。现在可以通过旋转区域来控制加速方向。

 void Accelerate(Rigidbody body) { Vector3 velocity =transform.InverseTransformDirection(body.velocity);
body.velocity =transform.TransformDirection(velocity); }

在跳跃区之间跳动。


对存在做出反应



加速区只是如何创建具有特定行为的触发区的一个示例。如果您需要一个区域执行其他操作,则必须为其编写新代码。但是检测和响应某处某物的存在的简单行为是如此普遍,以至于我们理想情况下只编写一次。而且许多行为非常简单(例如激活对象),以至于无法为其创建专用的组件类型。而更复杂的行为通常只是一些简单动作的组合。如果关卡设计师可以通过简单地配置游戏对象并添加一些组件来创建它,而不必一直创建专门的代码,这将很方便。


检测区



让我们从创建一个DetectionZone组件开始,该组件检测在其区域中是否存在某些东西,并在有东西进入或退出时通知感兴趣的人。我们通过给它配置UnityEvent类型的字段onEnteronExit,从UnityEngine.Events命名空间。

using UnityEngine;using UnityEngine.Events;
public class DetectionZone : MonoBehaviour {
[SerializeField] UnityEvent onEnter = default, onExit = default;}
            
            
只需让它在OnTriggerEnter和OnTriggerExit中的适当事件上调用该方法。这将触发对事件注册的所有内容的方法调用。
 void OnTriggerEnter (Collider other) { onEnter.Invoke(); }
void OnTriggerExit (Collider other) { onExit.Invoke(); }

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

检测区域无事件。


材料选择器



为了演示这是如何工作的,我们将创建一个简单的MaterialSelector组件类型,该组件类型具有可配置的材料和MeshRenderer参考数组。它具有一个带有index参数的Select公共方法,该方法将有效的材质分配给渲染器(如果有效)。

using UnityEngine;
public class MaterialSelector : MonoBehaviour {
[SerializeField] Material[] materials = default;
[SerializeField] MeshRenderer meshRenderer = default;
public void Select (int index) { if ( meshRenderer && materials != null && index >= 0 && index < materials.Length ) { meshRenderer.material = materials[index]; } }}

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

材料选择器。

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

检测区域设置为选择材料。

确保默认情况下,区域对象使用不活动的红色材料。然后以这种方式开始,但是一旦有物体进入区域,它将切换为活动的绿色材料。当有东西离开该区域时,它将再次变为红色。

与检测区域进行交互。


首次进入和最后退出



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

重命名的事件。

为了使这种行为成为可能,我们必须跟踪区域中当前的对撞机。我们将通过将DetectionZone命名空间中的List<Collider>字段初始化为System.Collections.Generic新列表来完成此操作。

using UnityEngine;using UnityEngine.Events;using System.Collections.Generic;
public class DetectionZone : MonoBehaviour {
[SerializeField] UnityEvent onFirstEnter = default, onLastExit = default;
List<Collider> colliders = new List<Collider>(); }


该列表如何工作?

请参阅“ 对象管理”系列的“ 持久对象”教程。


OnTriggerEnter中仅调用输入事件如果列表为空,则始终对撞机添加到列表中,以保持它的轨道。

 void OnTriggerEnter (Collider other) { if (colliders.Count == 0) { onFirstEnter.Invoke(); } colliders.Add(other); }

在这种情况下,我们将在OnTriggerExit中从列表删除对撞机,仅当列表为空时才调用exit事件。列表的Remove方法返回删除是否成功。应当总是这样,因为否则我们将无法跟踪对撞机,但是我们仍然可以对其进行检查。

 void OnTriggerExit (Collider other) { if (colliders.Remove(other) && colliders.Count == 0) { onLastExit.Invoke(); } }

只要区域中有东西,就活跃。


检测出现和消失的对象



不幸的是,OnTriggerExit它是不可靠的,因为在停用,禁用或销毁游戏对象或其对撞机时,不会调用它。不应该单独禁用碰撞器,因为那样会导致物体掉落到几何体中,因此我们将不支持此功能。但是我们应该能够处理整个游戏对象在区域内时被禁用或破坏的情况。

每个物理步骤,我们都必须检查区域中的对撞机是否仍然有效。添加一个在对撞机列表中循环的FixedUpdate方法。如果对撞机进行评估,false则意味着它或其游戏对象已被破坏。如果不是这种情况,我们必须检查其游戏对象是否已停用,我们可以通过activeInHierarchy其游戏对象的属性来查找。如果对撞机不再有效,请从列表中将其删除,并减少循环迭代器。如果列表为空,则调用exit事件。

void FixedUpdate () { for (int i = 0; i < colliders.Count; i++) { Collider collider = colliders[i]; if (!collider || !collider.gameObject.activeInHierarchy) { colliders.RemoveAt(i--); if (colliders.Count == 0) { onLastExit.Invoke(); } } } }

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

void Awake () { enabled = false; }
void FixedUpdate () { for (int i = 0; i < colliders.Count; i++) { Collider collider = colliders[i]; if (!collider || !collider.gameObject.activeInHierarchy) { colliders.RemoveAt(i--); if (colliders.Count == 0) { onLastExit.Invoke(); enabled = false; } } } }
void OnTriggerEnter (Collider other) { if (colliders.Count == 0) { onFirstEnter.Invoke(); enabled = true; } colliders.Add(other); }
void OnTriggerExit (Collider other) { if (colliders.Remove(other) && colliders.Count == 0) { onLastExit.Invoke(); enabled = false; } }

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

void OnDisable () { if (colliders.Count > 0) { colliders.Clear(); onLastExit.Invoke(); } }

请注意,检测区的组件不应由其他代码禁用,因为它可以管理自己的状态。一般规则是不要禁用检测区域组件,也不要禁用任何可能影响该区域的对撞机。这些游戏对象应全部停用或销毁。


热装



因为热重载(在编辑器播放模式下重新编译)OnDisable将被调用,因此它违反了我们刚刚声明的规则。这将导致调用退出事件以响应热重载,此后已存在于该区域中的对象将被忽略。幸运的是,我们可以检测到OnDisable中的热重装。如果同时启用了该组件并且游戏对象处于活动状态,则我们将进行热重载,并且什么也不做。当游戏对象没有被销毁而组件被销毁时,情况也是如此,但是我们裁定不应该这样做。

我们只需要在编辑器中播放时进行检查,就可以将代码包装在#if UNITY_EDITOR和中#endif

 void OnDisable () {#if UNITY_EDITOR if (enabled && gameObject.activeInHierarchy) { return; }#endif if (colliders.Count > 0) { colliders.Clear(); onLastExit.Invoke(); } }


OnDisable中相关的状态组合是什么?

如果禁用了该组件,则将其禁用或禁用游戏对象,然后我们继续进行。否则,如果游戏对象未处于活动状态,则该游戏对象将被停用或销毁,然后我们继续进行。否则,它要么是热装,要么是仅组件被破坏,我们将其忽略。



更复杂的行为



这只是通过事件可以完成的简单演示。您可以通过向事件列表中添加更多条目来创建更复杂的行为。您不必为此创建新方法,您可以使用现有方法。限制是它必须是与事件的参数列表匹配的void方法或属性设置器,或者最多具有一个可序列化的参数。例如,我进行了一些设置,以便在检测区域内有东西的同时关闭悬浮区域,除了更改区域本身的可视化效果之外。

切换悬浮区。

您不必总是对所有事件都响应。您可能只有在进入或退出时才触发某些事件。例如,在进入区域时激活某些内容。然后退出并不会取消激活它,而重新进入则会再次激活它,这无济于事。


这种基于事件的方法可以用于整个游戏吗?

从理论上讲,是的,这对于快速制作原型非常有用,但是却很麻烦。一旦发现自己重复了一个复杂的模式,就可以为其创建专用的方法或行为,这应该更容易使用,并在以后必要时进行优化。



简单运动



我们将在本教程中介绍的最后一种情况是移动环境对象。复杂的运动可以通过动画来完成,可以通过检测区域触发。但是通常两点之间的简单线性插值就足够了,例如,对于门,电梯或浮动平台。因此,让我们添加对此的支持。


自动滑块



无论插值什么,它在概念上都由从0到1的滑块控制。如何更改值是与插值本身不同的问题。保持滑块分离还可以将其用于多个插值。因此,我们将创建一个AutomaticSlider专用于此值的组件。它的可配置持续时间必须为正。当我们使用它为物理对象设置动画时,我们将使其在FixedUpdate方法中增加其值,并确保它不会过冲。一旦值达到1,我们就可以完成并可以禁用滑块。

using UnityEngine;
public class AutomaticSlider : MonoBehaviour {
[SerializeField, Min(0.01f)] float duration = 1f;
float value;
void FixedUpdate () { value += Time.deltaTime / duration; if (value >= 1f) { value = 1f; enabled = false; } }}

再一次,我们将使用Unity事件来将行为附加到滑块。在这种情况下,我们需要一个on-value-changed事件,该事件将用于传递滑块的当前值。因此,我们的事件需要一个float参数,我们可以为其使用UnityEvent<float>类型。在FixedUpdate结束时调用事件。

using UnityEngine;using UnityEngine.Events;
public class AutomaticSlider : MonoBehaviour { [SerializeField] UnityEvent<float> onValueChanged = default;
float value;
void FixedUpdate () { onValueChanged.Invoke(value); }}

但是,Unity无法序列化通用事件类型,因此该事件不会显示在检查器中。我们必须创建自己的具体可序列化事件类型,该事件类型可以简单地扩展UnityEvent<float>。此类型特定于我们的滑块,因此通过在类内部以及事件字段本身进行声明将其设置为嵌套类型。

[System.Serializable] public class OnValueChangedEvent : UnityEvent<float> { }
[SerializeField] OnValueChangedEventonValueChanged = default;

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

具有值更改事件的禁用滑块。

请注意,在这种情况下,事件的名称后跟(Single),表示它具有一个参数。Single是指float类型,它是单精度浮点数。


位置插补器



接下来,创建一个PositionInterpolator组件类型,该组件类型Rigidbody通过带有float参数的公共方法Interpolate在两个可配置位置之间插值可配置位置。请使用Vector3.LerpUnclamped以便提供的值不会受到限制,而将其留给调用者。我们必须通过其MovePosition方法更改身体的位置,以便将其解释为运动,否则将成为隐形传送。

using UnityEngine;
public class PositionInterpolator : MonoBehaviour {
[SerializeField] Rigidbody body = default; [SerializeField] Vector3 from = default, to = default; public void Interpolate (float t) { body.MovePosition(Vector3.LerpUnclamped(from, to, t)); }}

位置插补器连接到滑块。

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

启用移动平台。


自动倒车



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

[SerializeField] bool autoReverse = false;

bool reversed;
void FixedUpdate () { float delta = Time.deltaTime / duration; if (reversed) { value -= delta; if (value <= 0f) { if (autoReverse) { value = Mathf.Min€(1f, -value); reversed = false; } else { value = 0f; enabled = false; } } } else { value +=delta; if (value >= 1f) { if (autoReverse) { value = Mathf.Max(0f, 2f - value); reversed = true; } else { value = 1f; enabled = false; } } } onValueChanged.Invoke(value); }

启用了自动反向的平台。


平稳步伐



线性插值的运动是刚性的,反转时速度会突然变化。通过将值的平滑变体传递给事件,我们可以使其加速和减速。我们通过应用smoothstep功能给它,这是3V 2 - 2V 3。使它成为可配置的选项。

线性且平滑。
 [SerializeField] bool autoReverse = false, smoothstep = false;
float SmoothedValue => 3f * value * value - 2f * value * value * value; void FixedUpdate () { onValueChanged.Invoke(smoothstep ? SmoothedValue :value); }

启用了平滑步骤的平台。


更多控制



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

 //bool reversed; public bool Reversed { get; set; }

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

public bool AutoReverse { get => autoReverse; set => autoReverse = value; }

复杂的平台控制。

请注意,方向反转是突然的,因为它仍然是简单的插值。如果要在任何时候平稳停止和反转,则需要创建使用加速度和速度的更复杂的逻辑。


碰撞碰撞



移动风景的危险是,身体最终可能会陷入两个接近的对撞机之间。当对撞机之间的缝隙关闭时,身体要么被弹出,要么最终被推入对撞机或通过对撞机。如果碰撞表面成一定角度,则存在清晰的逃生路径,身体将朝该方向被推动。如果不是这样,或者如果没有足够的时间逃脱,则身体最终会被压碎,从而穿透对撞机。如果一个物体卡在两个足够厚的简单对撞机之间,那么它可以留在它们内部,一旦有一条清晰的道路就会弹出。否则会掉下去。

被推入地面。

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

平台具有成角度的对撞机和隐藏在地板上的盒子。


局部插值



世界空间中的配置可能会带来不便,因为它无法在多个位置用于同一动画。因此,我们通过给PositionInterpolator添加一个本地空间选项来包装一下。为此,我们添加了一个可选的可配置的相对于插值发生位置的Transform。通常用插值器引用对象,但这不是必需的。

[SerializeField] Transform relativeTo = default;
public void Interpolate (float t) { Vector3 p; if (relativeTo) { p = Vector3.LerpUnclamped( relativeTo.TransformPoint(from), relativeTo.TransformPoint(to), t ); } else { p = Vector3.LerpUnclamped(from, to, t); } body.MovePosition(p); }

相对插值使重用成为可能。

想知道下一个教程何时发布吗?关注微信公众号(u3dnotes)吧!

资源库(Repository)

https://bitbucket.org/catlikecodingunitytutorials/movement-10-reactive-environment/



往期精选

Unity3D游戏开发中100+效果的实现和源码大全 - 收藏起来肯定用得着

Shader学习应该如何切入?


UE4 开发从入门到入土



声明:发布此文是出于传递更多知识以供交流学习之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与我们联系,我们将及时更正、删除,谢谢。

原作者:Jasper Flick

原文:

https://catlikecoding.com/unity/tutorials/movement/reactive-environment/

翻译、编辑、整理:MarsZhou


More:【微信公众号】 u3dnotes


本文分享自微信公众号 - Unity3D游戏开发精华教程干货(u3dnotes)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

原文链接:https://my.oschina.net/u/4589456/blog/4544356
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章