文/瀚阳
到目前为止,我讲了几个化学API,但是我们还没有提到细节。相信机智的读者看见标题以后都早已猜到了,这就是接下来要讨论的。我们会讨论函数的可用性,分析会碰到的问题,以及相应的解决方案。
物理API
由于好多函数都有很多变种,所以这儿就不花太多时间在数学文档上。我不准备沉闷地把所有API都讲一遍,它们之间的差异无非就是光线是否打中物体就结束之类的。
Raycast: 打出一条指定方向与宽度(也许无限远)的射线。如果有一个对象被打中了,就可以从RaycastHit结构体得到有用的信息:打中那里,打中点表面的法线怎么等等。因为它只能打出无限细小的射线,因此不会用于做碰撞处理。
CapsuleCastAll: 第一眼看上去,这个函数用在角色控制器上是十分理想的(由于它使用的是胶囊体形状)。值得注意的是,这是一个投射,它只能测量法线面上投射的表面,背面是难以测量下来的。也就是说,投射难以测量出包围了胶囊体初始位置的物体,也就是这些与初始位置相交的对象。如果我们想将其用于角色控制器的开发,这是一个必须要攻破的缺陷。
CheckCapsule: 这次我们有了一个解决刚才讲到的问题的候选人。CheckCapsule看上去正是可以解决CapsuleCastAll难以测量到的初始位置的问题。不幸的是,它只能返回布尔值,而不是一组碰撞体,缺少了我们所须要的信息。
CheckSphere: 与前面一样,只是换了球状。
Linecast: 标准的投射函数。只是换了一种定义射线初始位置、方向、长度的方法。
OverlapSphere: 现在我们总算找到了。OverlapSphere的行为就跟名子一样。请注意文档中的这一行:
注意:目前它只能测量出碰撞物体的包围盒,而不是真正的碰撞体。
我真的不知道它为何要如此写。因为我早已测试过Box Collider、Sphere Collider、Mesh Collider,用上去确实是测量出了真正的碰撞体而不是包围盒。这里我所理解的包围盒是轴对齐包围盒。我只能觉得是文档错误。
RaycastAll: 与Raycast一样,除了不会在第一个碰撞的对象就停下来之外。
SphereCastAll: 与CapsuleCastAll一样,有着同样难以测量初始位置相交对象的缺点。SphereCast也不能保证返回正确的碰撞法线。因为这是投射一个球出去,它可以与网格的边相交。当与网格的边碰撞时,hit.normal返回的是边所联接的两个顶点的法线配准。由于CapsuleCast也只是投射一个扫略体,因此与网格的边碰撞时也是同样处理。
除了前面提及的工具以外,Unity还提供了Rigidbody.SweepTestAll技巧。经过测试,它的行为看起来与投射方式差不多。包围了面条的碰撞体难以被测量下来。比起SweepTestAll,我更倾向于使用CapsuleCastAll和SphereCastAll,因为它们有着更多的可选项(比如选择初始位置),但是SweepTest对于圆形的角色很有用,因为没有BoxCast技巧。
网格碰撞体
在我们进一步之前,我们讲讲网格碰撞体(Mesh Collider)。到目前为止,我们只研究了基础形状(Box、Sphere、Capsule等等)。但是在实际的关卡当中,关卡会用到网格碰撞体。
与基础形状不同的是,基础形状都通过预定义的参数来定义其形状(球体的直径、盒子的高度等等),一个网格碰撞体的碰撞数据是通过3D网格来定义的。网格碰撞体主要分两类: 凸多边形与凹五边形。这篇文档挺好地解释了二者的优缺。
凸多边形所有面条必须是封闭的,而且Unity限制了六边形数目上限为255,用它来表示错综复杂的关卡地形是不理想的。凹多边形可以是任意形状,但是缺点是不保证封闭。也就是说比起固体来讲,它们更多的是一块面条。这意味着我们再也不会测量下来某个对象在凸多边形面条内部。这也带来了一个相位问题。相位会在角色联通速率很快的时侯发生。就是两帧之间直接就穿过去了,没有发生任何碰撞。而凸多边形致使问题更容易发生了。
#6:8:9:e:b:1:9:9:8:c:8:5:a:c:c:6:7:1:b:c:b:e:6:4:a:8:c:5:5:6:d:f#
角色控制器一帧的运动,他的速率大的足以让他一帧就直接穿过了外墙
假设我们就贴近着网格碰撞体,而且法线正对着我们的方向,我们就能联通的最大距离是直径的两倍。想象一下我们碰撞处理的做法是将对象贴近着墙壁,因此这些情况很容易发生。如果你的角色大约2米高,半径为0.5米。那么角色的最大联通速率为每帧1米。如果游戏运行在30帧,那么每秒30米,每小时108千米。看上去很快,但是对于索尼克这类游戏来说还不够。
#5:7:a:4:a:e:3:c:b:6:7:4:0:f:f:6:0:3:9:a:1:3:a:e:d:5:6:b:c:7:4:f#
因为角色贴近着表面,不能速度快过直径两倍,否则会发生相位。
一个方式就是让数学引擎每帧运行多次。也可以使用CapsuleCastAll来测量每一帧起点与终点之间的碰撞体。我们会在后续的文章中讲讲这部份的内容。
初次实现
今天我们只关注单个类,这是最简单的。我将会跳过控制器的最基础的结构体,而是进一步会深入更多的细节。
控制器会经历三个阶段: 移动(Movement)、反推(Pushback)、处理(Resolution)。在联通阶段,我们估算所有我们角色的联通逻辑,然后相应地改变他得位置。紧接着会运行PushBack函数,来确保他没有与任何几何体发生交叉。最后,我们会做必要的处理步骤。这里包括了斜坡的处理等等的情况。
#8:d:c:c:c:3:2:d:6:f:1:c:5:e:8:5:b:2:1:a:5:c:d:4:f:9:2:2:5:e:6:9#
图中展示了控制器的联通以及反推
在我们开始之前,要注意一下,这次不像之前的控制器,这次是使用了三个OverlapSpheres,一个个叠上去,对胶囊体进行模拟。这个控制器可以用任意多个圆球–高瘦的角色可能须要不止三个,而矮圆的角色可能多于三个。现在让我们瞧瞧代码。刚开始的时侯我们的控制器代码还相对简单,只有一条代码:
transform.position += debugMove * Time.deltaTime;
复制代码
这促使你可以通过inspector设置角色控制器每帧运动多远。当我们构造真正的角色的时侯,这一行会被实际的联通逻辑取代。现在只是作为调试工具使用。第二阶段就是反推。这里的目标就是确定是否与其他碰撞体发生交叉,如果发生了,就将其推到近来的表面。基本的实现可以看之前的文章。而这一次,算法显得稍稍复杂了一点点。前半段与之前大致相同,我们使用OverlapSphere找到某个发生碰撞的近来表面的点。接着,要瞧瞧OverlapSphere得到的法线在哪一边。我们通过从圆球原点到碰撞体表面打射线来判别。由于光线投射只能测量法线朝着投射方向的表面。因此此次投射会返回true或则false,这样就晓得我们的原点是在内部还是外部。值得注意的是,代码中使用的是用了很小直径的SphereCast来代替光线投射。这样就防止了光线直接打在网格线段上的情况。
#1:4:b:c:0:c:a:a:2:d:8:4:4:f:9:f:7:d:4:9:f:7:6:e:e:3:3:5:9:b:5:c#
角色的”脚”与斜坡发生OverlapSphere,找到了近来点,然后朝近来点打射向(红色箭头)
在应用回退向量将角色推回来之前,我们还是须要做最后检测来确保我们是否与某个对象发生了碰撞。因为OverlapSphere会返回所有碰撞体,然后一个个地反推。这会促使在与多个碰撞体发生碰撞的情况下,上一个碰撞得到的反推会促使与这一个碰撞体不再交叉。我们解决这个问题的方式是检测我们圆球的原点与碰撞体近来点的距离。如果距离小于直径,我们就直接顺着法线往外推,我们就觉得这些情况下,我们上一个反推促使不再与这个碰撞体交叉。
#b:d:3:5:3:4:e:6:a:9:e:1:6:4:4:2:7:4:b:7:c:9:f:3:e:7:8:d:1:7:9:a#
最下方的圆球的OverlapSphere与红色的斜坡以及白色的地面发生交叉,而分别的近来点为靛和蓝。斜坡的反推先起作用,使得圆球不再与地面发生碰撞。
第三阶段就不像前两个阶段那样清晰。可以觉得就是清除或则反应逻辑。这里须要执行两个主要的逻辑是:坡度限制以及掐住地面。每个对Unity内建的Character Controller熟悉的朋友应当都了解斜度限制:如果角色尝试向指定斜度更大的地方走去,他都会被像一堵墙一样封住。钳住地面则是Unity角色控制器所不具备的能力,而且相当重要。当水平走过不平的桥面时,控制器将不会贴近着地面。在真实世界当中,我们通过手臂每次的轻微上出来保持平衡。但是在游戏世界中,我们须要特殊处理。与真实世界不同的是,重力不是总是作用在控制器头上。当我们没有站在平面上时,会添加向上的重力加速度。当我们在平面上时,我们设置垂直速率为0,表示平面的斥力。由于我们站在平面上的垂直速率为0,当我们走出平面时,需要时间来形成向上的速率。对于走出峭壁来说,这么做没问题,但是当我们在斜坡或则不平滑的桥面行走时,会形成不真实的回调疗效。为了防止有视觉问题,在地面与非地面之间的振幅会构成逻辑问题,特别是在地面上与掉落时的差异。
#c:5:1:b:a:d:0:f:e:1:c:9:9:2:9:7:8:d:2:9:c:4:2:3:8:9:8:8:a:f:c:6#
左边的图显示了角色在不平滑的桥面联通时,钳住地面的疗效。而右侧则是没有掐住地面,从而造成角色很跳。每个蓝色的大叉都代表了向上的重力归零。
这个问题在我们的第三阶段得到解决,就是之前谈到的掐住地面,就像名子描述的那样,会通过从头部向上SphereCast来调整角色位置。显然,有很多情况下都不希望角色掐住地面,比如起跳或则在地面比较远的上方都不能算上站在前面。你可能注意到我会经常讨论角色是否站在平面上。同时也容易注意到ProbeGround函数在主循环中调用了多次。知道角色是否在平面上是十分重要的。我不准备在控制器类中提供测量角色是否着地的插口,因为这很大程度取决于你的游戏。但是,我会提供一种查询角色手掌下有哪些(以及愈发丰富的信息)的插口。如何使用则取决于你,但是这个系列的下一章,我会提供一个使用这个控制器以及ProbeGround数据的角色的反例。而SlopeLimit方式应当足够简单去理解,而且我还写了不少注释(我还没即将开始,但是在递交文件之前就计划好了)。提到熟悉的功能,那些熟悉Unity控制器的朋友应当看出我的控制器少了一个功能: StepOffset。我是准备处理这个问题,但是实际上比我想像的要复杂不少,也许是我还没想到一个简约的方案。对于大多数应用来讲,这是一个必要的功能。以上就大约是我的角色控制器的内容。下次,我会用一个用到明天详尽介绍的控制的反例角色,同时也会提供源代码。如果我的任何代码有问题,请与我联系,我会将其更改。
from:游资网