喵的Unity游戏开发之路 - 复杂重力
转载(拷贝)自微信公众号(u3dnotes),图片和视频请查看原文:
前言
很多童鞋没有系统的Unity3D游戏开发基础,也不知道从何开始学。为此我们精选了一套国外优秀的Unity3D游戏开发教程,翻译整理后放送给大家,教您从零开始一步一步掌握Unity3D游戏开发。 本文不是广告,不是推广,是免费的纯干货!本文全名:喵的Unity游戏开发之路 - 移动 - 复杂重力 - 重力平面,球体和盒子
支持多种重力源。
限制重力范围。
使重力随距离减小。
创建平面,球形和盒形重力源。
这是有关控制角色移动的教程系列的第六部分。它通过支持多个重力源(包括平面,球体和盒子)扩展了我们的自定义重力。
本教程使用Unity 2019.2.21f1制作。它还使用ProBuilder软件包。
效果之一
多重引力源
如果您只需要相同的重力源,那么上一教程中介绍的方法就足够了。但是,如果您需要使用不同种类的重力(每个场景使用不同的重力,或者同一场景中使用多个重力),则需要一种更通用的方法。
默认重力源
为了在一个场景中支持多个重力源,我们将创建一个新的GravitySource
组件类型,使用类似于CustomGravity中的一个具有单个位置参数的公共方法GetGravity,只是在这种情况下它不是静态的。最简单的实现是只返回Physics.gravity
,因此我们将这样做。可以将其用作默认重力源,可以将其添加到任何场景中,这将使我们的球体在标准物理重力下工作。
using UnityEngine;
public class GravitySource : MonoBehaviour {
public Vector3 GetGravity (Vector3 position) {
return Physics.gravity;
}
}
重力来源清单
为了每个场景支持多个重力源,CustomGravity
必须跟踪所有活动源。我们将为此提供一个静态的来源列表。
using UnityEngine;
using System.Collections.Generic;
public static class CustomGravity {
static List<GravitySource> sources = new List<GravitySource>();
…
}
该列表如何工作?
请参阅“ 持久性对象”教程的1.5节中的通用列表说明。
调整GetGravity
仅具有位置参数的方法,以使其遍历光源并累积其重力。
public static Vector3 GetGravity (Vector3 position) {
Vector3 g = Vector3.zero;
for (int i = 0; i < sources.Count; i++) {
g += sources[i].GetGravity(position);
}
return g;
}
在另一种方法中执行相同的GetGravity操作,该方法也提供向上轴。现在我们别无选择,只能通过归一化和求反最终重力矢量来得出轴。
public static Vector3 GetGravity (Vector3 position, out Vector3 upAxis) {
Vector3 g = Vector3.zero;
for (int i = 0; i < sources.Count; i++) {
g += sources[i].GetGravity(position);
}
upAxis = -g.normalized;
return g;
}
当只有一个来源时,我们不能优化它吗?
是的,但是我们不会对来源数量做任何假设。如果您一次只使用一个信号源,则根本不需要循环。
GetUpAxis
必须以相同的方式进行调整。
public static Vector3 GetUpAxis (Vector3 position) {
Vector3 g = Vector3.zero;
for (int i = 0; i < sources.Count; i++) {
g += sources[i].GetGravity(position);
}
return -g.normalized;
}
使用列表是最好的方法吗?
如果同时有几个重力源处于活动状态,则列表是最简单的方法,也是最好的方法。只有在有许多资源发挥作用时,才开始考虑更智能的空间分区策略(例如,限制体积层次结构)会变得有益。一种替代方法是使用对撞机和触发器来确定哪些对象受哪些重力源影响,但这会增加大量开销。还有一种方法是禁用离玩家太远而不会影响游戏体验的音源。但是本教程将使其保持尽可能简单。
还请记住,一个区域具有多个彼此靠近的强重力源,这是不直观的。我们没有类似的经验。少数场景可能很有趣,但是具有许多重力源的空间可能使导航变得令人沮丧。
注册和注销源
为了能够改变主动重力源,增加了public的 Register
和Unregister
方法。他们只是在列表中添加或删除给定的重力源。
public static void Register (GravitySource source) {
sources.Add(source);
}
public static void Unregister (GravitySource source) {
sources.Remove(source);
}
这个想法是单个来源只能注册一次,否则其效果将成倍增加。同样,仅取消注销先前已注册的源才有意义。可以多次注销和注册同一源,这是很好的。我们可以添加断言以通过调用Debug.Assert来验证这一点,以捕获编程错误。
public static void Register (GravitySource source) {
Debug.Assert(
!sources.Contains(source),
"Duplicate registration of gravity source!", source
);
sources.Add(source);
}
public static void Unregister (GravitySource source) {
Debug.Assert(
sources.Contains(source),
"Unregistration of unknown gravity source!", source
);
sources.Remove(source);
}
Debug.Assert是做什么的?
如果第一个参数是false,它将使用第二个参数消息(如果提供)记录断言错误。第三个参数是如果在控制台中选择了消息,则在编辑器中突出显示的内容。此调用仅包含在开发版本中,不包含在发行版本中。好像Debug.Assert(…);从未编写过代码。因此,这是在开发过程中添加不会影响最终版本的检查的好方法。
我们可以通过分别在OnEnable
和OnDisable
方法中进行GravitySource的注册和注销自己。这样,它在创建,销毁,激活,停用,启用,禁用源代码以及跨编辑器热加载时都有效。
void OnEnable () {
CustomGravity.Register(this);
}
void OnDisable () {
CustomGravity.Unregister(this);
}
现在,我们可以将所有场景调整为再次使用默认重力,只需GravitySource
向每个场景添加带有组件的游戏对象即可。
允许扩展
这个想法是GravitySource是
其他重力源的基础,就像我们创建的所有自定义组件类型的基础MonoBehaviour一样。新的重力源类型将通过GetGravity
用自己的实现覆盖方法来完成其工作。为了使之成为可能,我们必须将此方法声明为virtual
。
publicvirtualVector3 GetGravity (Vector3 position) {
return Physics.gravity;
}
什么是virtual关键字?
请参阅“ 持久对象”教程末尾的说明。
重力平面
默认的物理重力仅定义一个代表普遍向下拉动的向量。该想法的最简单扩展是定义重力平面。这样做完全相同,只是平面还将空间分成上下两部分。我们可以用它来限制平面重力的范围。
新重力源类型
创建一个GravityPlane
扩展的新组件类型GravitySource
。给它一个可配置的重力场。这是它应用于其范围内的所有物体的加速度,将其拉低。因此,正值表示通过重力的法向吸引力,而负值表示排斥,表示反重力。
重写GetGravity
以使其返回指向正向上的矢量,该矢量由取反的配置重力缩放。必须通过向其添加override
关键字来显式地覆盖方法。
using UnityEngine;
public class GravityPlane : GravitySource {
[SerializeField]
float gravity = 9.81f;
public override Vector3 GetGravity (Vector3 position) {
Vector3 up = Vector3.up;
return -gravity * up;
}
}
现在,我们可以创建一个重力平面,如果将其配置为指向下方,则其工作原理与默认物理重力矢量相同。
我们也可以通过使用游戏对象转换的向上矢量来支持任何方向的平面。
Vector3 up =transform.up;
可视化平面
为了更容易查看平面的位置,我们将使用一些小物件将其可视化。如果启用了切换选项,它们将在场景窗口以及编辑器的游戏窗口中绘制。尽管平面是无限的,但我们必须使用有限的可视化效果,因此我们将使用正方形。
通过添加OnDrawGizmos
方法并使用类Gizmos的各种静态方法和属性来完成绘制小控件。首先将其设置Gizmos.color
为黄色,然后通过Gizmos.DrawWireCube绘制线框立方体 ,该立方体位于原点,其大小设置为(1,0,1),因此将其展平为一个正方形。
void OnDrawGizmos () {
Gizmos.color = Color.yellow;
Gizmos.DrawWireCube(Vector3.zero, new Vector3(1f, 0f, 1f));
}
默认情况下,小物件在世界空间中绘制。为了正确定位和旋转正方形,我们必须使用平面的变换矩阵(将其localToWorldMatrix分配给Gizmos.matrix)。这也使我们能够缩放平面对象,以便更容易看到正方形,即使这不影响平面的重力。
Gizmos.matrix = transform.localToWorldMatrix;
Gizmos.color = Color.yellow;
Gizmos.DrawWireCube(Vector3.zero, new Vector3(1f, 0f, 1f));
例如,我制作了一个小场景,其中包含两个相对的20×20矩形区域以及相应的重力平面,它们位于它们的上方和下方。底部是正常区域,顶部是倒置区域。顶面旋转,因此它也上下颠倒,这意味着其重力已翻转。
我们是否必须重置其中的颜色和矩阵OnDrawGizmos?
不,那是自动发生的。
重力范围
当两个具有相同重力的平面朝相反的方向拉动时,它们会相互抵消,因此根本不会产生重力。如果我们想要一个重力在一个地方向下而在另一个地方向上的场景,则我们必须限制每个平面的影响。我们将通过添加可配置范围来实现GravityPlane
。它表示相对于平面本身有效的重力,因此最小范围为零。平面的影响力没有极限,可以一直持续下去。
我们可以在GetGravity中
通过获取平面上矢量和该位置减去平面位置的点积来找到位置的距离。如果距离大于该范围,则合成重力应为零。
[SerializeField, Min(0f)]
float range = 1f;
public override Vector3 GetGravity (Vector3 position) {
Vector3 up = transform.up;
float distance = Vector3.Dot(up, position - transform.position);
if (distance > range) {
return Vector3.zero;
}
return -gravity * up;
}
我们不能只切断平面上方的重力吗?
是的,但是稍后我们将使用该范围逐渐减小重力,而不是使其变为二进制。
我们可以通过绘制第二个正方形(最初是一个单位)来可视化此范围。让我们给它一个青色。
void OnDrawGizmos () {
Gizmos.matrix = transform.localToWorldMatrix;
Vector3 size = new Vector3(1f, 0f, 1f);
Gizmos.color = Color.yellow;
Gizmos.DrawWireCube(Vector3.zero,size);
Gizmos.color = Color.cyan;
Gizmos.DrawWireCube(Vector3.up, size);
}
但是在这种情况下,我们要使用偏移量的范围,而不用飞机游戏对象的比例来修改。为此,我们可以通过自己构造一个矩阵Matrix4x4.TRS
,然后将其位置,旋转和比例传递给它。我们只需要用范围替换对象局部比例尺的Y分量。
Vector3 scale = transform.localScale;
scale.y = range;
Gizmos.matrix =
Matrix4x4.TRS(transform.position, transform.rotation, scale);
我对这些平面进行了配置,以使它们的范围平方最终位于完全相同的位置。因此,它显示了重力翻转的平面。
如果范围是零,那么我们就不用费心画青色正方形了。
if (range > 0f) {
Gizmos.color = Color.cyan;
Gizmos.DrawWireCube(Vector3.up, size);
}
相机对准速度
假设我们可以跳得足够高,我们现在可以在场景的一侧行走并跳到另一侧。当我们这样做时,重力会翻转,这也会突然使相机翻转,这会迷失方向。我们可以通过减慢OrbitCamera的重新排列来改善重力突然变化的体验。
添加可配置的向上对齐速度,以限制相机调整其向上矢量的速度,以每秒度数表示。让我们使用360°作为默认值,这样完整的重力翻转需要半秒钟才能完成。
[SerializeField, Min(0f)]
float upAlignmentSpeed = 360f;
现在,我们必须调整如何调整重力对齐四元数。首先将代码移至单独的UpdateGravityAlignment
方法,然后使用变量跟踪当前和所需的向上矢量。
void LateUpdate () {
//gravityAlignment =
// Quaternion.FromToRotation(
// gravityAlignment * Vector3.up,
// CustomGravity.GetUpAxis(focusPoint)
// ) * gravityAlignment;
UpdateGravityAlignment();
UpdateFocusPoint();
…
}
void UpdateGravityAlignment () {
Vector3 fromUp = gravityAlignment * Vector3.up;
Vector3 toUp = CustomGravity.GetUpAxis(focusPoint);
Quaternion newAlignment =
Quaternion.FromToRotation(fromUp, toUp) * gravityAlignment;
gravityAlignment = newAlignment;
}
我们可以通过取上矢量的点积,然后通过Mathf.Acos
乘以Mathf.Rad2Deg,将结果转换为度,从而找到上矢量之间的夹角。最大允许角度是由时间增量缩放的上对齐速度。
Vector3 fromUp = gravityAlignment * Vector3.up;
Vector3 toUp = CustomGravity.GetUpAxis(focusPoint);
float dot = Vector3.Dot(fromUp, toUp);
float angle = Mathf.Acos(dot) * Mathf.Rad2Deg;
float maxAngle = upAlignmentSpeed * Time.deltaTime;
arccos函数仅对落在-1到1范围内的输入产生有效的结果。由于精度限制,点积最终可能会超出该范围,从而产生非数字NaN值。为了防止这种情况Mathf.Clamp
来清理点积。
float dot =Mathf.Clamp(Vector3.Dot(fromUp, toUp), -1f, 1f);
如果角度足够小,那么我们可以照常直接使用新的对齐方式。否则,我们必须在当前旋转和所需旋转之间进行插补,最大角度除以所需角度作为插补器。为此,我们执行球面插值Quaternion.Slerp,以便获得适当的旋转。
Quaternion newAlignment =
Quaternion.FromToRotation(fromUp, toUp) * gravityAlignment;
if (angle <= maxAngle) {
gravityAlignment = newAlignment;
}
else {
gravityAlignment = Quaternion.Slerp(
gravityAlignment, newAlignment, maxAngle / angle
);
}
由于我们已经确保仅在必要时才进行插值,因此保证插值器位于0–1范围内,因此我们可以使用它SlerpUnclamped
来避免不必要的额外钳位。
gravityAlignment = Quaternion.SlerpUnclamped(
gravityAlignment, newAlignment, maxAngle / angle
);
插值采用最短路径,但是在180°翻转的情况下,插值可以沿任何方向进行。数学偏爱某个方向,因此即使看起来很奇怪,它也总是以相同的方式进行。
重力衰减
该平面范围的点是逐渐减小其重力,而不是突然将其切断。为了演示此方法的用处,我回到了重力盒的场景,并在盒子的六个侧面都添加了一个重力平面。我限制了它们的范围,因此盒子中间的大部分开放空间都没有重力。那里漂浮的任何东西要么保持静止不动,要么保持它已经拥有的动力。
由于每个平面的重力的二值截止,在立方体的边缘和角落附近发生了奇怪的事情。重力突然以陡峭的角度变化,很难导航。我们可以通过GravityPlane
随着距离从零到最大范围线性地减小的重力来改善这一点。如果位置位于平面上方,则只需将其乘以1减去距离除以范围即可完成此操作。
public override Vector3 GetGravity (Vector3 position) {
…
if (distance > range) {
return Vector3.zero;
}
float g = -gravity;
if (distance > 0f) {
g *= 1f - distance / range;
}
returng* up;
}
现在,当我们接近盒子的边缘或角落时,重力会更平滑地过渡。但是它仍然很奇怪,并且可能很难从角落中逃脱,因为三个平面被拉到那里,导致重力增加。稍后我们将提出一个更好的解决方案。
引力不应该除以平方距离吗?
实际上,实际重力会根据距离的平方减小。但是,这假定了从其质心开始测量的大致球形重力源。这对于我们的重力平面没有意义,因为它是平坦的,无限的并且没有质心。您可以对衰减进行平方,但由于没有最大的重力范围,因此无法实现。该范围用于创建不真实的场景,其中不同区域仅受某些重力源的影响。衰减通常是线性还是二次无关紧要。Linear更易于设计,这就是我所使用的。
重力球
现在我们有了一个功能性重力平面,让我们对球体应用相同的方法。
半径和衰减
创建具有可配置重力,外半径和外衰减半径的GravitySphere。我使用术语“外部半径”而不是“半径”,因为它表示重力达到最大强度时的距离。这不必与球体的表面半径匹配。实际上,将其扩展到足够远的位置是一个好主意,以便常规游戏区域能够承受恒定强度的重力。这使得设计起来容易得多。否则,您会发现在略微升高的位置进行常规跳跃可能已经将您带入太空。
在这种情况下,我们可以用Gizmos.DrawWireSphere来可视化重力,再次将黄色用作第一个阈值,将青色用作第二个阈值。如果衰减球大于外球,我们只需要显示它即可。
using UnityEngine;
public class GravitySphere : GravitySource {
[SerializeField]
float gravity = 9.81f;
[SerializeField, Min(0f)]
float outerRadius = 10f, outerFalloffRadius = 15f;
void OnDrawGizmos () {
Vector3 p = transform.position;
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(p, outerRadius);
if (outerFalloffRadius > outerRadius) {
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(p, outerFalloffRadius);
}
}
}
衰减半径小于外半径没有意义,因此请强制使其始终至少与 Awake
和OnValidate中的
方法 一样大。
void Awake () {
OnValidate();
}
void OnValidate () {
outerFalloffRadius = Mathf.Max(outerFalloffRadius, outerRadius);
}
应用重力
对于球体,GetGravity
可以通过找到从位置指向球体中心的向量来工作。距离是矢量的大小。如果超出外部衰减半径,则没有重力。否则,它是按配置的重力缩放的归一化向量。请注意,再次使用正重力表示标准拉动,而负重力会将对象推开。
public override Vector3 GetGravity (Vector3 position) {
Vector3 vector = transform.position - position;
float distance = vector.magnitude;
if (distance > outerFalloffRadius) {
return Vector3.zero;
}
float g = gravity;
return g * vector.normalized;
}
我们已经计算了向量的长度,因此我们可以将配置的重力除以该长度来说明向量的长度,而不必对其进行归一化。
float g = gravity/ distance;
return g *vector;
我们不能从比较平方距离开始吗?
是的,这将是一种优化,当位置最终超出范围时,避免平方根运算。您必须在方法中平方配置的半径或将其存储在字段中。
在外半径和外衰减半径之间线性减小重力的作用类似于减小平面。我们只需要更改数学即可使其落在正确的范围内。因此,我们乘以一减去外半径的距离再除以衰减范围。该范围等于衰减半径减去外半径。我将最后一位隔离在单独的衰减因子中,并将其存储在字段中,并在OnValidate中对其进行了初始化。这避免了一些数学运算,但是我主要是这样做的,因为它使GetGravity代码保持较短。
float outerFalloffFactor;
public override Vector3 GetGravity (Vector3 position) {
…
float g = gravity / distance;
if (distance > outerRadius) {
g *= 1f - (distance - outerRadius) * outerFalloffFactor;
}
return g * vector;
}
…
void OnValidate () {
outerFalloffRadius = Mathf.Max(outerFalloffRadius, outerRadius);
outerFalloffFactor = 1f / (outerFalloffRadius - outerRadius);
}
通过衰减操作,现在可以使用多个具有重叠衰减区域的重力球,从而在它们之间进行平滑过渡。请注意,存在一个抵消区域,物体可能最终在两个球体之间绕圆轨道运行,但是当进入具有动量的区域时,很少会陷入其中。
从一个球体跳到另一个球体可能需要一些练习。特别是当强重力场在大面积上重叠时,您可能会陷入相当长的不稳定轨道。同样,当一个球体的一部分受到另一个重力源的强烈影响时,重力会变得有些奇怪。
倒球
我们还可以支持倒置重力球。仅仅抵消重力是不够的,因为我们可能希望在球体中心有一个重力死区。这是球体的重力抵消其自身的区域,这对于正常行星和反向行星都是如此。它还可以在内部放置不受更大重力影响的另一个重力源。添加可配置的内部衰减半径和内部半径以实现此目的。让我们再次用青色和黄色可视化它们,但前提是它们大于零。
[SerializeField, Min(0f)]
float innerFalloffRadius = 1f, innerRadius = 5f;
…
void OnDrawGizmos () {
Vector3 p = transform.position;
if (innerFalloffRadius > 0f && innerFalloffRadius < innerRadius) {
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(p, innerFalloffRadius);
}
Gizmos.color = Color.yellow;
if (innerRadius > 0f && innerRadius < outerRadius) {
Gizmos.DrawWireSphere(p, innerRadius);
}
Gizmos.DrawWireSphere(p, outerRadius);
if (outerFalloffRadius > outerRadius) {
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(p, outerFalloffRadius);
}
}
内部衰减半径的最小值为零,它设置了内部半径的最小值,而后者又设置了外部半径的最小值。还添加一个内部衰减因子,该函数与其他因子的作用相同,只是半径的顺序相反。
floatinnerFalloffFactor,outerFalloffFactor;
…
void OnValidate () {
innerFalloffRadius = Mathf.Max(innerFalloffRadius, 0f);
innerRadius = Mathf.Max(innerRadius, innerFalloffRadius);
outerRadius = Mathf.Max(outerRadius, innerRadius);
outerFalloffRadius = Mathf.Max(outerFalloffRadius, outerRadius);
innerFalloffFactor = 1f / (innerRadius - innerFalloffRadius);
outerFalloffFactor = 1f / (outerFalloffRadius - outerRadius);
}
现在,GetGravity中
当距离小于内部衰减半径时,我们也可以中止。如果不是,我们还必须检查该距离是否落在内部衰减区域内,如果这样,则可以适当地缩放重力。
public override Vector3 GetGravity (Vector3 position) {
Vector3 vector = transform.position - position;
float distance = vector.magnitude;
if (distance > outerFalloffRadius|| distance < innerFalloffRadius) {
return Vector3.zero;
}
float g = gravity / distance;
if (distance > outerRadius) {
g *= 1f - (distance - outerRadius) * outerFalloffFactor;
}
else if (distance < innerRadius) {
g *= 1f - (innerRadius - distance) * innerFalloffFactor;
}
return g * vector;
}
重力箱
通过返回重力框场景结束本教程。我们将使用一个单独的盒形重力源,而不是使用六个平面使它可以在盒子内部行走。
边界距离
GravityBox
为基于框的重力源创建新类型。这个想法类似于球体,但是重力直接向下拉到最近的面,而不是随方向平滑变化。为此,我们需要可配置的重力以及定义盒子形状的方法。为此,我们将使用边界距离矢量,其作用有点像半径,但分别针对所有三个维度。因此,这些是从中心直线到面的距离,这意味着盒子的大小是其边界距离的两倍。最小边界是零向量,我们可以通过该Vector3.Max
方法强制执行。我们将通过Gizmos.DrawWireCube使用红色线框立方体可视化此边界。
using UnityEngine;
public class GravityBox : GravitySource {
[SerializeField]
float gravity = 9.81f;
[SerializeField]
Vector3 boundaryDistance = Vector3.one;
void Awake () {
OnValidate();
}
void OnValidate () {
boundaryDistance = Vector3.Max(boundaryDistance, Vector3.zero);
}
void OnDrawGizmos () {
Gizmos.matrix =
Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one);
Gizmos.color = Color.red;
Gizmos.DrawWireCube(Vector3.zero, 2f * boundaryDistance);
}
}
内部距离
我们将再次定义一个内部区域,其中重力处于最大强度,外加一个内部区域,该区域将重力降至零,而内部则是一个没有重力的区域。这次的半径没有意义,因此我们将它们定义为相对于边界向内的内部距离。这三个维度的距离都相同,因此我们可以使用两个可配置的值。
[SerializeField, Min(0f)]
float innerDistance = 0f, innerFalloffDistance = 0f;
两个内部距离的最大值等于最小边界距离。除此之外,内部衰减距离必须至少与内部距离一样大。
void OnValidate () {
boundaryDistance = Vector3.Max(boundaryDistance, Vector3.zero);
float maxInner = Mathf.Min€(
Mathf.Min€(boundaryDistance.x, boundaryDistance.y), boundaryDistance.z
);
innerDistance = Mathf.Min€(innerDistance, maxInner);
innerFalloffDistance =
Mathf.Max(Mathf.Min€(innerFalloffDistance, maxInner), innerDistance);
}
两种距离都可以通过线框立方体可视化,其大小等于边界距离减去相关距离的两倍。
void OnDrawGizmos () {
Gizmos.matrix =
Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one);
Vector3 size;
if (innerFalloffDistance > innerDistance) {
Gizmos.color = Color.cyan;
size.x = 2f * (boundaryDistance.x - innerFalloffDistance);
size.y = 2f * (boundaryDistance.y - innerFalloffDistance);
size.z = 2f * (boundaryDistance.z - innerFalloffDistance);
Gizmos.DrawWireCube(Vector3.zero, size);
}
if (innerDistance > 0f) {
Gizmos.color = Color.yellow;
size.x = 2f * (boundaryDistance.x - innerDistance);
size.y = 2f * (boundaryDistance.y - innerDistance);
size.z = 2f * (boundaryDistance.z - innerDistance);
Gizmos.DrawWireCube(Vector3.zero, size);
}
Gizmos.color = Color.red;
Gizmos.DrawWireCube(Vector3.zero, 2f * boundaryDistance);
}
衰减
计算盒子的重力衰减比球体要复杂一些,但是我们可以再次使用一个内部衰减系数。
float innerFalloffFactor;
…
void OnValidate () {
…
innerFalloffFactor = 1f / (innerFalloffDistance - innerDistance);
}
首先考虑与平面相似的单个重力分量是最容易的。为此创建一个带有两个参数的方法GetGravityComponent。第一个是相对于盒子中心的相关位置坐标。第二个是沿相关轴到最近的脸的距离。它沿同一轴返回重力分量。
float GetGravityComponent (float coordinate, float distance) {
float g = gravity;
return g;
}
如果该距离大于内部衰减距离,则我们位于零重力区域,因此结果为零。否则,我们必须检查是否必须像对待球体一样减小重力。唯一的额外步骤是,如果坐标小于零,则必须翻转重力,因为这意味着我们位于中心的另一侧。
float GetGravityComponent (float coordinate, float distance) {
if (distance > innerFalloffDistance) {
return 0f;
}
float g = gravity;
if (distance > innerDistance) {
g *= 1f - (distance - innerDistance) * innerFalloffFactor;
}
returncoordinate > 0f ? -g :g;
}
然后,该GetGravity方法必须确定相对于盒子位置的位置,并从零向量开始。然后计算到中心的绝对距离,调用GetGravityComponent
最小距离,并将结果分配给向量的适当分量。结果是重力相对于最近的面直线向下拉,或者在负重力的情况下朝其推。
public override Vector3 GetGravity (Vector3 position) {
position -= transform.position;
Vector3 vector = Vector3.zero;
Vector3 distances;
distances.x = boundaryDistance.x - Mathf.Abs(position.x);
distances.y = boundaryDistance.y - Mathf.Abs(position.y);
distances.z = boundaryDistance.z - Mathf.Abs(position.z);
if (distances.x < distances.y) {
if (distances.x < distances.z) {
vector.x = GetGravityComponent(position.x, distances.x);
}
else {
vector.z = GetGravityComponent(position.z, distances.z);
}
}
else if (distances.y < distances.z) {
vector.y = GetGravityComponent(position.y, distances.y);
}
else {
vector.z = GetGravityComponent(position.z, distances.z);
}
return vector;
}
最后,为了支持任意旋转的立方体,我们必须旋转相对位置以与立方体对齐。我们通过InverseTransformDirection
对其进行转换而忽略了它的规模来做到这一点。最终重力矢量必须通过TransformDirection反向旋转。
//position -= transform.position;
position =
transform.InverseTransformDirection(position - transform.position);
…
returntransform.TransformDirection(vector);
这种方法的结果是,当最近的面孔发生变化时,重力突然改变了方向。当漂浮在盒子内部时,这很好用,但是使盒子内部的面之间旅行变得困难。我们很快会对此进行改进。
在箱子外面
像球体一样,我们还将支持外部距离和外部衰减距离,以及外部衰减因子。
[SerializeField, Min(0f)]
float outerDistance = 0f, outerFalloffDistance = 0f;
float innerFalloffFactor, outerFalloffFactor;
…
void OnValidate () {
…
outerFalloffDistance = Mathf.Max(outerFalloffDistance, outerDistance);
innerFalloffFactor = 1f / (innerFalloffDistance - innerDistance);
outerFalloffFactor = 1f / (outerFalloffDistance - outerDistance);
}
盒子内部的重力必须突然改变,但是在盒子外面,它会变得更加模糊。假设我们正在盒子的外面移动,被拉向盒子。如果我们经过一个面的边缘,重力仍然会拉低我们,但是如果我们继续将它与最近的边缘对齐,那么我们将跌过盒子而不是朝着盒子。这表明如果我们不直接位于脸部上方,则应该朝最近的边缘或角落掉落。而且,应该确定相对于该边缘或面的距离,因此我们最终得到的是圆角立方体形状的区域。
没有使用可视化圆形立方体的便捷方法,因此让我们创建一个简单的近似值。首先添加一个Gizmos方法,该方法通过四个点绘制闭环,我们将使用该方法绘制矩形。
void DrawGizmosRect (Vector3 a, Vector3 b, Vector3 c, Vector3 d) {
Gizmos.DrawLine(a, b);
Gizmos.DrawLine(b, c);
Gizmos.DrawLine(c, d);
Gizmos.DrawLine(d, a);
}
接下来,创建一种绘制给定距离的外部立方体的方法。给它四个向量变量并进行设置,以便我们在适当的距离为右脸绘制一个正方形。
void DrawGizmosOuterCube (float distance) {
Vector3 a, b, c, d;
a.y = b.y = boundaryDistance.y;
d.y = c.y = -boundaryDistance.y;
b.z = c.z = boundaryDistance.z;
d.z = a.z = -boundaryDistance.z;
a.x = b.x = c.x = d.x = boundaryDistance.x + distance;
DrawGizmosRect(a, b, c, d);
}
然后取反这些点的X坐标,以便绘制左面。
DrawGizmosRect(a, b, c, d);
a.x = b.x = c.x = d.x = -a.x;
DrawGizmosRect(a, b, c, d);
对其他四个脸重复此过程。
DrawGizmosRect(a, b, c, d);
a.x = d.x = boundaryDistance.x;
b.x = c.x = -boundaryDistance.x;
a.z = b.z = boundaryDistance.z;
c.z = d.z = -boundaryDistance.z;
a.y = b.y = c.y = d.y = boundaryDistance.y + distance;
DrawGizmosRect(a, b, c, d);
a.y = b.y = c.y = d.y = -a.y;
DrawGizmosRect(a, b, c, d);
a.x = d.x = boundaryDistance.x;
b.x = c.x = -boundaryDistance.x;
a.y = b.y = boundaryDistance.y;
c.y = d.y = -boundaryDistance.y;
a.z = b.z = c.z = d.z = boundaryDistance.z + distance;
DrawGizmosRect(a, b, c, d);
a.z = b.z = c.z = d.z = -a.z;
DrawGizmosRect(a, b, c, d);
如果需要,在OnDrawGizmos中绘制两个外部立方体。
void OnDrawGizmos () {
…
if (outerDistance > 0f) {
Gizmos.color = Color.yellow;
DrawGizmosOuterCube(outerDistance);
}
if (outerFalloffDistance > outerDistance) {
Gizmos.color = Color.cyan;
DrawGizmosOuterCube(outerFalloffDistance);
}
}
这向我们显示了圆形外部立方体的平坦区域,但未显示圆形边界。我们可以创建一个非常详细的可视化效果来显示它们,但这将需要大量代码。取而代之的是,在立方体的圆角点之间添加一个单独的线框立方体就足够了。这些点与边界立方体之间的距离一定,在所有三个维度上均等地偏移。因此,我们需要的距离等于边界立方体的距离加上以√(1/3)缩放的相关距离。
void DrawGizmosOuterCube (float distance) {
…
distance *= 0.5773502692f;
Vector3 size = boundaryDistance;
size.x = 2f * (size.x + distance);
size.y = 2f * (size.y + distance);
size.z = 2f * (size.z + distance);
Gizmos.DrawWireCube(Vector3.zero, size);
}
您也可以沿圆角的边缘绘制直线,使用在两个维度上由√(1/2)缩放的距离,但是我们当前的方法就足够了。
检测侧面
现在我们必须确定给定位置是位于GetGravity中的框内还是框外。我们根据每个维度确定此数字,并计算出最终有多少外部。首先,检查位置是否超出右脸。如果是这样,请将向量的X分量设置为X边界减去X位置。调整向量使其直接指向脸部而不是中心。另外,增加外部计数。
public override Vector3 GetGravity (Vector3 position) {
position =
transform.InverseTransformDirection(position - transform.position);
Vector3 vector = Vector3.zero;
int outside = 0;
if (position.x > boundaryDistance.x) {
vector.x = boundaryDistance.x - position.x;
outside = 1;
}
…
}
如果我们不在右边,请检查我们是否在左边。如果是这样,请相应地调整矢量,这次是从负边界距离减去位置。
if (position.x > boundaryDistance.x) {
vector.x = boundaryDistance.x - position.x;
outside = 1;
}
else if (position.x < -boundaryDistance.x) {
vector.x = -boundaryDistance.x - position.x;
outside = 1;
}
分别对Y和Z面执行相同的操作。
else if (position.x < -boundaryDistance.x) {
vector.x = -boundaryDistance.x - position.x;
outside = 1;
}
if (position.y > boundaryDistance.y) {
vector.y = boundaryDistance.y - position.y;
outside += 1;
}
else if (position.y < -boundaryDistance.y) {
vector.y = -boundaryDistance.y - position.y;
outside += 1;
}
if (position.z > boundaryDistance.z) {
vector.z = boundaryDistance.z - position.z;
outside += 1;
}
else if (position.z < -boundaryDistance.z) {
vector.z = -boundaryDistance.z - position.z;
outside += 1;
}
完成之后,检查我们是否在至少一张脸之外。如果是这样,则到边界的距离等于调整后的矢量的长度。然后,我们可以使用与球体外相同的方法。否则,我们必须确定内部的重力。
else if (position.z < -boundaryDistance.z) {
vector.z = -boundaryDistance.z - position.z;
outside += 1;
}
if (outside > 0) {
float distance = vector.magnitude;
if (distance > outerFalloffDistance) {
return Vector3.zero;
}
float g = gravity / distance;
if (distance > outerDistance) {
g *= 1f - (distance - outerDistance) * outerFalloffFactor;
}
return transform.TransformDirection(g * vector);
}
Vector3 distances;
请注意,如果我们恰好在一个面的外面,那么我们就在该面的正上方,这意味着矢量的分量中只有一个非零。如果盒子很大,那就很常见。我们在这里取向量分量的绝对和就足够了,这比计算任意向量的长度要快。
float distance =outside == 1 ?
Mathf.Abs(vector.x + vector.y + vector.z) :vector.magnitude;
如果使边界框小于表面框,则采用这种方法时,重力会再次沿边缘和角平滑变化。在这些地区,重力也不再变得更强。您必须确保外部距离足以到达各个角落。
现在也有可能用自己的怪异重力创造出类似盒子的行星。请注意,除非您缓慢移动,否则跑出面部会使您掉到盒子外面一小会儿。可以通过使重力框的边界小于表面边界来缓解这种情况,当您接近边缘或拐角时,这会更早地开始弯曲重力。即使这样,当快速移动时,您可能也想增加盒子的重力以贴近表面。
下一个教程是“ 移动地面”。
资源库(Repository)
https://bitbucket.org/catlikecodingunitytutorials/movement-06-complex-gravity/
往期精选
Unity3D游戏开发中100+效果的实现和源码大全 - 收藏起来肯定用得着
声明:发布此文是出于传递更多知识以供交流学习之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与我们联系,我们将及时更正、删除,谢谢。
原作者:Jasper Flick
原文:
https://catlikecoding.com/unity/tutorials/movement/complex-gravity/
翻译、编辑、整理:MarsZhou
More:【微信公众号】 u3dnotes