您好,登录后才能下订单哦!
这篇“Unity怎么实现表面接触保持联系”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Unity怎么实现表面接触保持联系”文章吧。
本教程使用Unity 2019.2.14f1创建。它还使用ProBuilder软件包。
效果之一
跑酷的球。
贴在地面
当我们的球体到达坡道的顶部时,由于其向上的动量,它开始飞行。这是现实的,但可能并不理想。
球体在坡道顶部飞行。
当球体突然突然出现小的高差时,也会发生类似的情况。我制作了一个测试场景,以0.1增量的步长演示了这一步。
步骤测试场景。
如果步距不太高,则以足够的速度接近时,球体会反弹。在测试场景中,这对于平坦车道甚至很少发生,因为我是通过将步高降低到零而不合并顶点来实现的。这会产生所谓的幻影碰撞。应该设计场景几何图形以避免这种情况,但我坚持指出来。
弹起台阶。
在现实生活中,有多种技术可以将某些东西固定在地面上。例如,一级方程式赛车旨在将气流转换为下压力。因此,为我们的领域做类似的事情有现实的基础。
碰撞时间
让我们考虑一下球体将从坡道发射的瞬间。为了使它粘在表面上,我们必须对其速度进行调整,使其与表面重新对齐。让我们检查一下何时可以收到所需信息。通过在基于 OnGround
的 Update
中调整其颜色,将球体不在地面上时将其变为白色,这与上一教程末尾展示的颜色类似
void Update () { … GetComponent<Renderer>().material.SetColor( "_Color", OnGround ? Color.black : Color.white ); }
要观察准确的时间,请暂时减少物理时间步长和时间范围。
三个物理步骤;时间步长0.2;时标0.5。
球体发射的物理步骤仍然存在碰撞。我们会在下一步中根据这些数据采取行动,因此我们认为我们已经扎根,而不再需要。这是我们不再获取碰撞数据之后的步骤。因此,我们总是会为时已晚,但是只要我们意识到这一点就不会有问题。
自上次接地以来的步骤
让我们跟踪一下自从我们接地以来已经采取了多少物理步骤。为其添加一个整数字段,并在 UpdateState
的开头将其递增。然后,如果事实证明我们在地面上,则将其设置回零。我们将使用它来确定何时应该抓紧地面。它对于调试也很有用。
int stepsSinceLastGrounded; … void UpdateState () { stepsSinceLastGrounded += 1; velocity = body.velocity; if (OnGround) { stepsSinceLastGrounded = 0; jumpPhase = 0; if (groundContactCount > 1) { contactNormal.Normalize(); } } else { contactNormal = Vector3.up; } }
我们不必防范整数溢出吗?
我们不必为此担心。整数不溢出将需要花费几个月的实时时间。
抓拍
添加 SnapToGround
方法,该方法可在需要时将我们固定在地面上。如果成功,那么我们将被扎根。通过返回一个布尔值(最初只是返回 false
)来指示是否发生了这种情况。
bool SnapToGround () { return false; }
这样,我们可以方便地将其与使用布尔OR的 UpdateState
中的 OnGround
结合起来。之所以可行,是因为只有在 OnGround
为 false
时,才会调用 SnapToGround
。
void UpdateState () { stepsSinceLastGrounded += 1; velocity = body.velocity; if (OnGround|| SnapToGround()) { … } … }
SnapToGround
仅在我们不接地时被调用,因此自上次接地以来的步骤数大于零。但是,我们仅应在失去联系后立即尝试捕捉一次。因此,当步数大于1时,我们应该中止。
bool SnapToGround () { if (stepsSinceLastGrounded > 1) { return false; } return false; }
射线检测
我们只想在球体下面要坚持的地面时捕捉。我们可以通过以 body.position
和向下向量作为参数调用 Physics.Raycast
从球体的位置垂直向下投射射线来进行检查。物理引擎将执行此射线广播,并返回是否击中某些东西。如果没有,那就没有根据,我们就会中止。
if (stepsSinceLastGrounded > 1) { return false; } if (!Physics.Raycast(body.position, Vector3.down)) { return false; } return false;
如果射线确实击中了某物,那么我们必须检查它是否算作地面。可以通过第三个 RaycastHit
结构输出参数来检索有关命中信息的信息。
if (!Physics.Raycast(body.position, Vector3.down,out RaycastHit hit)) { return false; }
该代码如何工作?
RaycastHit 是一个结构,因此是一个值类型。我们可以通过 RaycastHit hit 定义一个变量,然后将其作为第三个参数传递给 Physics.Raycast 。但这是一个输出参数,这意味着它像对象引用一样通过引用传递。必须通过向其中添加 out 修饰符来明确指出这一点。该方法负责为其分配值。
除此之外,还可以在参数列表内声明用于输出参数的变量,而不必在单独的行上声明。那就是我们在这里所做的。
命中数据包括法线向量,我们可以使用该向量来检查我们命中的表面是否算作地面。如果没有,请中止。请注意,在这种情况下,我们处理的是真实的表面法线,而不是碰撞法线。
if (!Physics.Raycast(body.position, Vector3.down, out RaycastHit hit)) { return false; } if (hit.normal.y < minGroundDotProduct) { return false; }
重新与地面对齐
如果我们此时还没有中止,那么我们只是失去了与地面的接触,但仍然在地面之上,因此我们要抓住它。将地面接触计数设置为1,使用找到的法线作为接触法线,然后返回 true
。
if (hit.normal.y < minGroundDotProduct) { return false; } groundContactCount = 1; contactNormal = hit.normal; return true;
现在我们认为自己已经扎根,尽管我们仍处于空中。下一步是调整速度,使其与地面对齐。就像对齐所需速度一样,它的工作原理是必须保持当前速度,并且我们将显式计算该速度,而不是依靠 ProjectOnContactPlane
。
groundContactCount = 1; contactNormal = hit.normal; float speed = velocity.magnitude; float dot = Vector3.Dot(velocity, hit.normal); velocity = (velocity - hit.normal * dot).normalized * speed; return true;
在这一点上,我们仍然漂浮在地面上,但是重力将有助于将我们拉到地面。实际上,速度可能已经有些下降,在这种情况下,重新调整速度会减慢向地面的收敛速度。因此,仅当其点乘积与表面法线为正时,才应调整速度。
if (dot > 0f) { velocity = (velocity - hit.normal * dot).normalized * speed; }
这足以使我们的球体在越过顶部时保持在坡道上。它们会漂浮一点,但是在实践中几乎看不到。即使球体会在一帧中变成白色,但在 FixedUpdate
中,我们将球体始终视为接地。只是在我们处于中间状态时调用 Update
。
坚持倾斜。
它还可以防止球在跳下台阶时启动球体。
坚持一步。
请注意,我们仅考虑位于我们下方的单个点来确定我们是否位于地面之上。只要关卡的几何图形不太嘈杂或不太详细,此方法就可以正常工作。例如,如果射线正好投射到其中,那么微小的深裂纹可能会导致破裂失败。
最大捕捉速度
无论如何,高速都是我们的球体被发射的原因,所以让我们添加一个可配置的最大捕捉速度。默认情况下,将其设置为最大速度,以便在可能的情况下始终进行捕捉。
[SerializeField, Range(0f, 100f)] float maxSnapSpeed = 100f;
最大捕捉速度。
然后,当当前速度超过最大捕捉速度时,也终止 SnapToGround
。我们可以在射线检测之前通过更早地计算速度来做到这一点。
bool SnapToGround () { if (stepsSinceLastGrounded > 1) { return false; } float speed = velocity.magnitude; if (speed > maxSnapSpeed) { return false; } if (!Physics.Raycast(body.position, Vector3.down, out RaycastHit hit)) { return false; } if (hit.normal.y < minGroundDotProduct) { return false; } groundContactCount = 1; contactNormal = hit.normal; //float speed = velocity.magnitude; float dot = Vector3.Dot(velocity, hit.normal); if (dot > 0f) { velocity = (velocity - hit.normal * dot).normalized * speed; } return true; }
请注意,由于精度限制,将两个最大速度设置为相同的值可能会产生不一致的结果。最好使最大捕捉速度高于或低于最大速度。
相同的最大速度会产生不一致的结果。
探头距离
当球体下方有地面时,无论距离多远,我们都在捕捉。最好只检查附近的地面。我们通过限制探头的范围来做到这一点。没有最佳的最大距离,但是如果过低的捕捉可能会在陡峭的角度或较高的速度下失败,而过高的捕捉则会导致不合理的捕捉到远低于地面的捕捉。使其可配置,最小值为零,默认值为1。由于我们的球体的半径为0.5,这意味着我们要在球体底部以下最多检查半个单位。
[SerializeField, Min(0f)] float probeDistance = 1f;
探头距离。
将距离作为第四个参数添加到 Physics.Raycast
。
if (!Physics.Raycast( body.position, Vector3.down, out RaycastHit hit, probeDistance )) { return false; }
忽略代理(Agents)
在检查地面是否贴合时,我们仅考虑可以表示地面的几何图形是有意义的。默认情况下,raycast会检查除放置在忽略Raycast 层上的对象外的所有内容。不应数的内容可以变化,但我们正在移动的领域很可能不会。我们不会偶然碰到要投射的球体,因为我们是从其位置向外投射,但是我们可能会碰到另一个移动的球体。为了避免这种情况,我们可以将其 Layer 设置为忽略Raycast ,但让我们为所有活动的,应忽略的内容创建一个新层为此目的。
通过游戏对象的 Layer 下拉菜单中的添加图层... 选项进入图层设置。项目设置的标签和图层(Tags and Layers)部分。然后定义一个新的自定义用户层。对于不属于关卡几何体的通用活动实体,我们将其命名为 Agent 。
标签和图层,以及自定义 代理商图层8。
将所有球体移动到该层。更改预制层即可。
图层设置为 代理。
接下来,向 MovingSphere
添加一个可配置的 LayerMask
探针掩码,该掩码最初设置为-1,与所有图层匹配。
[SerializeField] LayerMask probeMask = -1;
然后,我们可以配置球体,以便探测除忽略Raycast 和 Agent 之外的所有层。
探头罩。
要应用遮罩,请将其作为第五个参数添加到 Physics.Raycast
。
if (!Physics.Raycast( body.position, Vector3.down, out RaycastHit hit, probeDistance, probeMask )) { return false; }
跳跃和抓拍
抓拍现在可以工作并且可以配置,但是当我们跳跃时它也会激活,从而抵消了向上的动力。为了使跳跃再次起作用,我们必须避免在跳跃后立即弹跳。我们可以通过计算自上次跳跃以来的物理步数来跟踪此情况,就像我们计算自上次停飞以来的步数一样。在 UpdateState
的开头将其递增,并在激活跳转时将其设置回零。
int stepsSinceLastGrounded, stepsSinceLastJump; … void UpdateState () { stepsSinceLastGrounded += 1; stepsSinceLastJump += 1; … } … void Jump () { if (OnGround || jumpPhase < maxAirJumps) { stepsSinceLastJump = 0; jumpPhase += 1; … } }
现在,即使跳转后过早,我们也可以中止 SnapToGround
。由于碰撞数据的延迟,我们仍然认为启动跳跃后的步骤已接地。因此,如果我们在跳跃后走了两个或更少的步骤,就必须中止。
if (stepsSinceLastGrounded > 1|| stepsSinceLastJump <= 2) { return false; }
楼梯
接下来让我们考虑一种更困难的表面:楼梯。实际上,球体根本无法很好地爬上楼梯,但是无论如何,我们可能希望它们这样做,也许是因为它们代表了应该能够在楼梯上导航的东西。我制作了一个测试场景,其中包含五个45°阶梯,步长分别为0.1、0.2、0.3、0.4和0.5。
楼梯 test scene.
使用默认设置时,球体根本无法处理楼梯。在最大加速度下,大多数设法上升,但是结果不可靠且有弹性,根本没有平稳的运动。试图以一定角度移动而不是直上楼梯几乎是不可能的。
跳上楼梯;加速度100。
简化碰撞体(Collider)
较大的楼梯台阶使移动无法进行。而且,虽然可以以较小的台阶弹起楼梯,但是碰撞变得任意,运动变得不稳定而不是平稳。
与其尝试与物理引擎战斗,不如我们更加务实并使它的工作更轻松。我们要在楼梯上进行平稳,一致,可控制的运动。当我们使用平坦的斜坡代替时,我们可以得到。因此,让我们用坡道代替楼梯的碰撞体。
坡道是楼梯的近似值。最好的折衷方案是设计碰撞体坡道,使其切穿台阶中间。然后,碰撞将发生在可见几何体的上方和下方。
楼梯简化.
我创建了这样的形状以匹配五个阶梯,首先是常规的 ProBuilder 对象。然后,通过 ProBuilder 窗口中的设置碰撞体选项将它们转换为碰撞体。
简化的楼梯,作为普通物体和 碰撞体。
禁用楼梯的网格碰撞体组件,但此时不要删除它们。然后暂时将最大地面角度增加到46°,以使球体可以向上移动45°楼梯。
将楼梯视为坡道,最大地面角度为46°。
尽管需要一些额外的关卡设计工作,但使用简化的碰撞体上楼梯是使它们在物理上可导航的最佳方法。通常,使碰撞形状尽可能简单是一个好主意,出于性能和运动稳定性的原因,避免不必要的细节。因此,我们将坚持使用这种方法。但这是一个近似值,因此在仔细检查时,您会发现球体切穿并悬停在楼梯台阶上方。但是,从远处和运动中通常并不太明显。
近似。
我们可以避免缓慢下楼梯吗?
将来,我们将在关注重力的情况下进行处理。
Detailed 和 Stairs 层(Layer)
我们使用简化的碰撞体进行球体与楼梯之间的交互并不意味着我们不能将原始的楼梯碰撞体用于其他碰撞。例如,我们可能希望小碎片正确地降落在各个楼梯台阶上,而不是从坡道上滑下来。让我们通过增加两层来实现这一点:一层用于详细信息,一层用于楼梯对象。
详细和楼梯对象的图层。
探针遮罩应包括楼梯层,但不包括 Detailed 层。
调整好探头罩。
接下来,转到项目设置的物理部分,然后调整图层碰撞矩阵。楼梯仅应与代理商交互,而代理不应与 Detailed 交互。
层碰撞矩阵。
现在,再次启用楼梯网格碰撞器组件。然后添加一些小的刚体对象,使其落在它们之上,以同时查看两种相互作用。如果您给这些物体提供足够低的质量(例如0.05),那么球体将能够将它们推到一边。
与楼梯相撞的两种方式。
楼梯的最大角度
如果我们能够爬楼梯,那么我们使用的楼梯最大角度与正常地面的最大角度是有道理的。因此,为它们添加一个单独的最大角度,默认情况下设置为50°。
[SerializeField, Range(0, 90)] float maxGroundAngle = 25f, maxStairsAngle = 50f; … float minGroundDotProduct, minStairsDotProduct; … void OnValidate () { minGroundDotProduct = Mathf.Cos(maxGroundAngle * Mathf.Deg2Rad); minStairsDotProduct = Mathf.Cos(maxStairsAngle * Mathf.Deg2Rad); }
最大地面和楼梯角度。
我们现在必须与哪种最小点产品进行比较取决于我们所使用的表面类型。我们将为此添加一个可配置的楼梯遮罩选项,类似于探针遮罩。
[SerializeField] LayerMask probeMask = -1, stairsMask = -1;
楼梯 mask.
为什么不使用 LayerMask.NameToLayer("Stairs")?
这是可能的,但是通过使用遮罩,我们不再依赖于硬编码的图层名称,并且更加灵活,这也使实验更加容易。
创建一个新的 GetMinDot
方法,该方法返回给定图层的适当最小值,该整数是整数。假设我们可以直接比较楼梯的蒙版和图层,如果它们不相等,则返回最小地面点积,否则返回最小楼梯点积。
float GetMinDot (int layer) { return stairsMask != layer ? minGroundDotProduct : minStairsDotProduct; }
但是,该掩码是位掩码,每层只有一位。具体来说,如果楼梯是第11层,则它与第11位匹配。我们可以通过使用 1 << layer
来设置单个位的值,该值将左移运算符应用于数字1的次数等于层索引(十)。结果将是二进制数10000000000。
return stairsMask !=(1 << layer)?
如果蒙版仅选择了一个图层,这将起作用,但是让我们为任何图层组合支持一个蒙版。我们通过采用掩码和图层位的布尔AND来实现。如果结果为零,则该层不属于蒙版。
return(stairsMask & (1 << layer)) == 0?
在 EvaluateCollision
的开头检索正确的最小点值,并使用它来检查接触是否算作地面。
void EvaluateCollision (Collision collision) { float minDot = GetMinDot(collision.gameObject.layer); for (int i = 0; i < collision.contactCount; i++) { Vector3 normal = collision.GetContact(i).normal; if (normal.y >=minDot) { groundContactCount += 1; contactNormal += normal; } } }
检查我们是否在地面上时,还可以在 SnapToGround
中使用 GetMinDot
。
if (hit.normal.y <GetMinDot(hit.collider.gameObject.layer)) { return false; }
陡峭的接触
除了接地触点,还有其他触点。移动需要接地,但有时我们仅与墙壁接触。否则我们可能陷入困境。如果我们有空气加速功能,在这种情况下我们仍然可以控制,但是通过一些额外的工作,我们可以做更多的事情。
检测陡峭的接触
陡峭的触点太陡而不能算作地面,但不是天花板或悬垂。因此,一切都达到了完美的垂直墙。就像在常规地面触点上一样,让我们在字段和属性中跟踪此类触点的正常和计数。
Vector3 contactNormal, steepNormal; int groundContactCount, steepContactCount; bool OnGround => groundContactCount > 0; bool OnSteep => steepContactCount > 0;
还要在 ClearState
中重置新数据。
void ClearState () { groundContactCount =steepContactCount =0; contactNormal =steepNormal =Vector3.zero; }
在 EvaluateCollision
中,如果我们没有地面接触,请检查它是否为陡峭接触。完美垂直的墙的点积应为零,但让我们稍微宽一点些,接受高于-0.01的所有值。
if (normal.y >= minDot) { groundContactCount += 1; contactNormal += normal; } else if (normal.y > -0.01f) { steepContactCount += 1; steepNormal += normal; }
裂缝
缝隙是有问题的,因为一旦卡在缝隙中而没有跳气,除非空气加速度很大,否则就不可能逃脱。我创建了一个带有小裂缝的测试场景来演示这一点。
逃脱裂缝。
跳墙
让我们还回顾一下跳墙。之前,我们仅将跳跃限制为仅在地面或空中跳跃时进行。但是,如果我们将跳跃方向设为陡峭法线而不是接触法线,那么我们也可以支持从墙跳。
开始制作跳转方向变量,并删除 Jump
中的当前有效性检查。
void Jump () { Vector3 jumpDirection; //if (OnGround || jumpPhase < maxAirJumps) { stepsSinceLastJump = 0; jumpPhase += 1; float jumpSpeed = Mathf.Sqrt(-2f * Physics.gravity.y * jumpHeight); float alignedSpeed = Vector3.Dot(velocity,jumpDirection); if (alignedSpeed > 0f) { jumpSpeed = Mathf.Max(jumpSpeed - alignedSpeed, 0f); } velocity +=jumpDirection* jumpSpeed; //} }
相反,请检查我们是否在地面上。如果是这样,请使用接触法线作为跳转方向。如果不是,那么下一个检查是我们是否处在陡峭的状态。如果是这样,请改用陡峭的法线。之后,检查空气跳动,为此我们再次使用接触法线,该法线已设置为向上向量。如果所有这些都不适用,则不可能进行跳转,并且我们中止。
Vector3 jumpDirection; if (OnGround) { jumpDirection = contactNormal; } else if (OnSteep) { jumpDirection = steepNormal; } else if (jumpPhase < maxAirJumps) { jumpDirection = contactNormal; } else { return; }
墙跳。
空中跳跃
此时,我们应该重新考虑空中跳跃。检查跳跃阶段是否小于最大空中跳跃的唯一作用是,在跳跃之后,该阶段会立即重置为零,因为在下一步中,我们仍将其视为接地。因此,我们仅应在启动跳转后多一步才能在 UpdateState
中重置跳转阶段,以免错误着陆。
stepsSinceLastGrounded = 0; if (stepsSinceLastJump > 1) { jumpPhase = 0; }
为了保持空中跳跃的正常进行,我们现在必须检查跳跃阶段是否小于或等于 Jump
中的最大值。
else if (jumpPhase<=maxAirJumps) { jumpDirection = contactNormal; }
但是,这样可以使空气从表面掉下来后再跳一次而不跳动。为了防止这种情况,我们在跳气时会跳过第一跳阶段。
else if (jumpPhase <= maxAirJumps) { if (jumpPhase == 0) { jumpPhase = 1; } jumpDirection = contactNormal; }
但这仅在完全允许空气跳跃的情况下才有效,因此首先检查一下。
else if (maxAirJumps > 0 &&jumpPhase <= maxAirJumps) { if (jumpPhase == 0) { jumpPhase = 1; } jumpDirection = contactNormal; }
最后,让壁跳重设跳跃阶段,以便有可能将壁跳变成新的空中跳跃序列。
else if (OnSteep) { jumpDirection = steepNormal; jumpPhase = 0; }
空气壁空气跳跃。
向上跳跃偏见
从垂直墙上跳下来不会增加垂直速度。因此,尽管有可能在附近的相对壁之间反弹,但重力始终会将球体拉下。我用两个方块制作了一个测试场景来演示这一点。
卡在底部;跳高3。
但是,有些游戏将跳墙作为达到极高的一种手段。我们可以通过在跳转方向上增加一个向上的偏差来支持这一点。最简单的方法是将上矢量添加到跳转方向并将结果标准化。最终方向是两个方向的平均值,因此从平坦地面的跳跃不会受到影响,而从完全垂直的墙壁上跳跃的影响最大,成为45°跳跃。
jumpDirection = (jumpDirection + Vector3.up).normalized; float alignedSpeed = Vector3.Dot(velocity, jumpDirection);
墙跳了起来;跳高3。
这会影响所有不在完美平坦的地面或空中的跳跃轨迹,这在上坡时跳跃时最明显。
跳跃时没有偏见。
最后,从 Update
删除调试球颜色。
//GetComponent<Renderer>().material.SetColor( // "_Color", OnGround ? Color.black : Color.white //);
以上就是关于“Unity怎么实现表面接触保持联系”这篇文章的内容,相信大家都有了一定的了解,希望小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注亿速云行业资讯频道。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。