前言
很多童鞋没有系统的Unity3D游戏开发基础,也不知道从何开始学。为此我们精选了一套国外优秀的Unity3D游戏开发教程,翻译整理后放送给大家,教您从零开始一步一步掌握Unity3D游戏开发。 本文不是广告,不是推广,是免费的纯干货!本文全名:喵的Unity游戏开发之路 - 移动 - 移动地面 - 搭便车
-
创建动画平台。
-
跟踪连接的身体。
-
尽量保持相对静止。
-
支持轨道连接点。
这是有关控制角色移动的教程系列的第七部分。它处理了在运动中的地形上站立和导航的挑战。
本教程使用Unity 2019.2.21f1制作。它还使用ProBuilder软件包。
效果之一
大部分静止不动时移动。
动画几何
有多种方法可以移动几何。我们可以创建一个脚本来调整对象的转换。我们可以使用Unity的动画系统对其进行动画处理。我们还可以编写自己的可播放图形,并以此方式创建动画。或者我们可以依靠PhysX并让对象响应外力和碰撞而移动。在所有情况下,我们都必须确保运动中的地形和障碍物与PhysX,我们的运动球体和我们的轨道摄像机配合得很好。
动画
在本教程中,我们将使用Unity的动画系统在编辑器中创建简单的动画。我们通过“ 动画”窗口执行此操作,可以通过“ 窗口” /“动画” /“动画”打开该窗口。如果选择的对象还没有Animator
组件,则窗口将显示一个按钮,可让您添加该组件并立即为其创建新的动画。
我制作了一个名为Up Down的简单方形平台对象,然后为其创建了一个新的动画剪辑,名为Up Down Animation。动画是一个新资产,但是按下Create按钮也会创建另一个资产,我将其重命名为Up Down Controller。这是运行动画所需的动画控制器资产。它可以用于创建复杂的混合树和动画状态机,但是如果我们只需要一个动画剪辑,就不必处理它。我把它们都放在一个新的动画文件夹中。
添加到平台对象的组件将自动设置为Animator使用新的控制器资产。我们最初可以将其所有其他配置选项保留为默认值。还要为该对象提供一个启用了Kinematic的Rigidbody组件,因为它是动态PhysX对象。尽管这不是严格必要的,但可以确保所有交互均按预期进行。
要使动画剪辑执行某项操作,您必须在场景中选择相关对象。“ 动画”窗口将在时间轴控制按钮下方的左侧显示我们的动画剪辑。按下录制按钮(红点),然后在右侧的时间线栏中选择所需的时刻。您可以缩放以到达当前不可见的区域。然后,通过其检查器或在场景视图中调整对象的变换。这将创建具有新配置的关键帧。
例如,我将两秒钟的Y位置从0更改为3,并在四秒钟将其重新设置为0。然后我关闭了录音。
此时可以预览动画。进入播放模式后,它也会自动播放并循环播放。
默认情况下,Unity通过缓和过渡来平滑动画。您可以通过“ 动画”窗口底部的切换选项从“ 摄影表 ”切换到“ 曲线”模式来控制确切的行为。
动画曲线;Y坐标为绿色。
为什么不能移动动画对象?
如果对象正在播放更改其位置的动画,则该动画的位置将覆盖该对象的配置位置。您可以通过将动画对象变成另一个对象的子对象,然后将其移动到其他位置。
动画时间
当球体被向上推动并随着平台的垂直运动而下降时,我们的球体已经可以在平台上跳跃并随之移动。但是默认情况下,交互的时间并不正确。当将我们的轨道摄像机的“ 聚焦半径 ” 设置为零,使其随球体刚性移动时,这一点最为明显。
事实证明,向上运动有点抖动,而向下运动则更糟,因为球体反复下降一小段距离,撞击平台,然后再次下降。发生这种情况是因为默认情况下,动画每帧更新一次,因此运动与PhysX不同步。我们可以设置动画更新通过设置每个物理步Update Mode 中的Animator
组件动画物理学。
现在,我们的球体可以在向下移动时粘附在平台上。平台的运动将像其他运动中的物理对象一样抖动,可以通过设置其Rigidbody
插值来解决(如果需要)。
横向运动
解决了垂直运动,但我们还必须支持向其他方向运动的平台。因此,我制作了另一个带有自己的动画剪辑和控制器的平台,该动画剪辑和控制器沿X轴左右移动。
我们的球体可以沿着平台的表面移动,但是当平台站立时,它忽略了平台的水平移动。其他PhysX对象确实会随平台一起拖动,除非它移动得太快,否则它们会左右滑动。但是我们的球体没有任何抓地力,因此不会被拖累。它的阻力系数为零,因为否则会干扰我们的控件。我们必须针对此问题提出解决方案。
连接体
为了能够沿着其站立的表面移动,我们的球体首先需要意识到该表面。通常,这意味着球体可以随时与可能运动的另一个物体连接。第一步是跟踪此主体,我们将其称为“连接主体”。可能同时存在多个这样的主体,但是这种情况很少见,因此我们将自己限制为一个单一的主体。因此,如果球体最终与多个物体接触,我们将使用任意物体,而忽略其他物体。一旦知道了身体,我们就必须检测其运动并将其以某种方式应用于球体。
检测连接
我们不在乎为什么某物在移动,而只是它可能会移动。这个想法是,所有动态对象都具有一个Rigidbody
组件,因此我们将通过向MovingSphere添加字段来跟踪连接的实体。
Rigidbody body, connectedBody;
如果我们在 EvaluateCollision 中检测到地面接触,我们可以简单地将rigidbody
碰撞的属性分配给我们的场。如果另一个对象具有Rigidbody
组件,那么我们现在可以对其进行引用,否则将其设置为null
。请注意,该组件不必直接附加到与之碰撞的对象上。我们可能会与一个复合对象发生冲突,该复合对象的组件在其层次结构中位于较高的位置。
if (upDot >= minDot) {
groundContactCount += 1;
contactNormal += normal;
connectedBody = collision.rigidbody;
}
请注意,仅通过始终分配连接的主体,我们就可以替换之前被视为接地的任何接触,因此最终可以跟踪最后评估的接地体。这很好,因为碰撞顺序是任意的,但在时间上趋于稳定。
但是我们可能最终会走在斜坡上而不是地面上。在这种情况下,我们还应该跟踪身体。但是,我们应该优先选择地面而不是斜坡,因此,如果尚未有地面接触,则仅分配斜坡主体。
else if (upDot > -0.01f) {
steepContactCount += 1;
steepNormal += normal;
if (groundContactCount == 0) {
connectedBody = collision.rigidbody;
}
}
如果我们还没有connectedBody,我们不应该总是使用斜坡吗?
不可以,因为地面可能是静态的,在这种情况下,地面不会有任何Rigidbody组件。在这种情况下,我们将站在不动的地面上,不应受到碰巧碰到的移动斜坡的影响。
如果我们检测到地面,我们也应该在 SnapToGround 中跟踪connectedBody。
bool SnapToGround () {
…
connectedBody = hit.rigidbody;
return true;
}
最后,在 ClearState 中将连接的主体重置为null
。
void ClearState () {
groundContactCount = steepContactCount = 0;
contactNormal = steepNormal = Vector3.zero;
connectedBody = null;
}
连接状态
仅仅在当前的物理步骤中仅知道我们已连接到人体是不够的。我们必须能够弄清楚自上一步以来我们是否仍与同一个身体保持联系,因为这表明我们应该与之保持联系。因此,我们需要另一个字段来存储对先前连接的主体的引用。重置前应将其设置为当前连接的主体。
Rigidbody body, connectedBody, previousConnectedBody;
…
void ClearState () {
groundContactCount = steepContactCount = 0;
contactNormal = steepNormal = Vector3.zero;
previousConnectedBody = connectedBody;
connectedBody = null;
}
让我们还将连接速度存储在一个字段中。虽然这不是绝对必要的,但它很方便。在ClearState中将其设置为零。
Vector3 velocity, desiredVelocity, connectionVelocity;
…
void ClearState () {
groundContactCount = steepContactCount = 0;
contactNormal = steepNormal =connectionVelocity =Vector3.zero;
previousConnectedBody = connectedBody;
connectedBody = null;
}
确定运动
如果连接的物体是自由移动的物理对象,那么它将具有速度,但是在运动动画对象的情况下,其速度将始终为零。因此,我们必须通过跟踪其位置来自己导出连接速度。为此添加一个字段,并将其设置为新方法UpdateConnectionState中连接主体的位置,如果有连接主体,我们将在UpdateState结尾处调用该方法。
-
Vector3 connectionWorldPosition;
…
void UpdateState () {
…
-
if (connectedBody) {
UpdateConnectionState();
}
}
-
void UpdateConnectionState () {
connectionWorldPosition = connectedBody.position;
}
但是,我们应注意不要粘在与我们相撞的轻型物体上,否则当我们将它们推开时,最终可能会随着它们一起自动移动,从而有效地自我发射。如果被连接物体是运动学的,或者至少与球体本身一样大,我们可以通过仅更新连接状态来避免这种情况。
if (connectedBody) {
if (connectedBody.isKinematic || connectedBody.mass >= body.mass) {
UpdateConnectionState();
}
在更新连接之前,可以通过UpdateConnectionState从连接的当前位置减去我们已经拥有的连接位置来找到连接的运动。通过将其运动除以时间增量来找到其速度。
void UpdateConnectionState () {
Vector3 connectionMovement =
connectedBody.position - connectionWorldPosition;
connectionVelocity = connectionMovement / Time.deltaTime;
connectionWorldPosition = connectedBody.position;
}
但是,只有当当前和先前的连接体相同时,该计算才有意义,因此请检查一下。否则,连接速度应保持为零。
if (connectedBody == previousConnectedBody) {
Vector3 connectionMovement =
connectedBody.position - connectionWorldPosition;
connectionVelocity = connectionMovement / Time.deltaTime;
}
connectionWorldPosition = connectedBody.position;
相对于连接的运动
至此,我们知道了我们所站的一切的速度。下一个问题是我们如何将其纳入球体的运动中。实际上,当您从正在移动的物体移到静止的物体(反之亦然)时,您将必须补偿相对运动的突然变化。这很费力,如果变化很大,可能会很困难。如果太大,最终会掉下去。另外,如果您站在可以加速的事物上,则必须做好准备,否则您也会跌倒。最后,应该有可能相对于我们所站立的物体以最大速度移动。请注意,这可能导致世界空间速度超过配置的最大速度,例如在行驶中的火车中行驶时。
最简单的建模方法是使球体加速以匹配其所连接的物体的速度,然后再加速至相对于连接速度的所需速度。我们可以通过AdjustVelocity从球体速度中减去连接速度,然后使用此相对速度来确定当前的X和Z速度来实现。因此,球体的速度调整变得相对于连接速度,而其他所有条件保持不变。
Vector3 relativeVelocity = velocity - connectionVelocity;
float currentX = Vector3.Dot(relativeVelocity, xAxis);
float currentZ = Vector3.Dot(relativeVelocity, zAxis);
现在,我们的球体试图匹配其所站立的物体的速度,但受到其自身加速度的限制。在与平台的运动匹配之前,球体将滑动一点。而且,如果平台快速加速,则如果球面无法跟上,球体可能会滑落。因此,在快速加速的东西上行走可能很尴尬,这与现实相符。这可以通过增加球体的最大加速度来缓解。
回转
尽管我们当前的方法适用于简单的运动,但它尚不支持旋转曲面。为了演示这一点,我用自己的动画创建了另一个平台,这次围绕Y轴旋转360°。通过将动画曲线的切线设置为Linear来使旋转连续。您可以通过其上下文菜单编辑曲线的关键帧控制点来执行此操作。
在旋转连接的情况下,我们无法跟踪其位置,因为它不受旋转的影响。我们还必须跟踪被连接物体的局部空间中的连接位置,因为该点有效地绕过了物体的本地原点。
Vector3 connectionWorldPosition, connectionLocalPosition;
从现在开始,我们将球体的位置用作世界空间中的连接位置,而不是连接本身的位置。这是我们开始跟踪的地方。连接局部位置是同一点,但是在连接体的局部空间中,这是通过调用InverseTransformPoint
连接体的Transform
组件来找到的。我们在UpdateConnectionState结束时执行此操作。
void UpdateConnectionState () {
…
connectionWorldPosition =body.position;
connectionLocalPosition = connectedBody.transform.InverseTransformPoint(
connectionWorldPosition
);
}
现在,通过使用连接物体的当前变换(这次使用TransformPoint
方法)将连接局部位置转换回世界空间,可以找到连接运动。然后从中减去存储的世界位置。如果没有任何旋转,那么结果与以前相同,但是如果有旋转,那么我们现在考虑轨道。
Vector3 connectionMovement =
connectedBody.transform.TransformPoint(connectionLocalPosition)-
connectionWorldPosition;
现在,我们的球体会加速以跟上旋转,但是请注意,它不会调整其方向来匹配。实际上,由于我们的球体永不旋转,它会自动重新定向以保持朝相同的方向看。
另请注意,旋转会导致高速旋转。您离旋转中心越远,轨道速度就越快。如果旋转足够快,您将被甩开,要么迅速从轨道弹出,要么缓慢向外盘旋。
复杂动画
因为我们的方法不在乎表面如何移动,所以我们不仅限于简单的动画。我们支持所有复杂的动画和脚本化运动。我们也支持在不受控制的PhysX对象上运动,尽管这很尴尬,就像在现实生活中在不稳定的地面上行走一样。创建复杂运动的另一种方法是通过构建其中包含多个动画师的对象层次结构。您也可以在层次结构中放置多个物理对象,但是请记住,您不希望带有Rigidbody的任何对象成为另一个此类对象的子对象,因为由于物理干扰,这将产生奇怪的结果。
下一个教程是 攀爬。
资源库(Repository)
https://bitbucket.org/catlikecodingunitytutorials/movement-07-moving-the-ground/
往期精选
Unity3D游戏开发中100+效果的实现和源码大全 - 收藏起来肯定用得着
声明:发布此文是出于传递更多知识以供交流学习之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与我们联系,我们将及时更正、删除,谢谢。
原作者:Jasper Flick
原文:
https://catlikecoding.com/unity/tutorials/movement/moving-the-ground/
翻译、编辑、整理:MarsZhou
More:【微信公众号】 u3dnotes