其他分享
首页 > 其他分享> > Unity游戏角色控制器 (二):碰撞效果的实现

Unity游戏角色控制器 (二):碰撞效果的实现

作者:互联网


在开始以前,先确保你的Unity是否已经完成下载安装。这篇文章中所使用的版本是Unity 4.3.4f1。(检查Unity版本的方法是Help->About Unity)打开一个现有的工程或者创建一个新的来开始这篇教程。创建一个新的场景(Scene),然后创建一个立方体(Cube)和一个球体(Sphere)。虽然我们最终会用胶囊体作为我们的控制器形状,但是刚开始还是让事情保持简单一些。将球体命名为Player,立方体命名为Wall。改变墙体每个轴的缩放到6。为了更加形象,我还给Player加了蓝色的材质,给Wall加了绿色的材质。将Player上的Sphere Collider组件移除掉。
 


创建新的C#脚本,然后命名为SuperCharacterController.cs。为了表示我们的角色,拷贝和粘贴一下脚本,然后挂到Player身上:
 

  1. using UnityEngine;
  2. using System;
  3. using System.Collections.Generic;
  4. public class SuperCharacterController : MonoBehaviour {
  5. [SerializeField]
  6.         float radius = 0.5f;
  7.          
  8.         private bool contact;
  9.          
  10.         // Update is called once per frame
  11.         void Update () {
  12.                  
  13.                 contact = false;
  14.                  
  15.                 foreach (Collider col in Physics.OverlapSphere(transform.position, radius))
  16.                 {
  17.                         Vector3 contactPoint = col.ClosestPointOnBounds(transform.position);
  18.                         
  19.                         Vector3 v = transform.position - contactPoint;
  20.                         
  21.                         transform.position += Vector3.ClampMagnitude(v, Mathf.Clamp(radius - v.magnitude, 0, radius));
  22.                         
  23.                         contact = true;
  24.                 }
  25.         }
  26.          
  27.         void OnDrawGizmos()
  28.         {
  29.                 Gizmos.color = contact ? Color.cyan : Color.yellow;
  30.                 Gizmos.DrawWireSphere(transform.position, radius);
  31.         }
  32. }

复制代码


然后就完了。运行项目并且打开场景。将Player往墙体的边缘慢慢拖过去。你可以看到墙体在反推,让Player总是停留在边缘。那么这里做了什么呢?

Physics.OverlapSphere返回了与球体发生碰撞的一组Collider。这是个很好用的函数,参数很简单。只需要传入球心与半径就可以了。

一旦检测到任何碰撞,我们就会开始处理。为了找到box collider上的最近点,我们用了ClosestPointOnBounds函数。紧接着我们就可以通过contactPoint得到我们的位置。contactPoint的长度就是我们所需要推出去的距离。

你可能会注意到,我实现了OnDrawGizmos函数,这样OverlapSphere的碰撞就一清二楚了。
 


两帧演示了碰撞被检测,而后被处理

相当简单。但是我们至今为止的胜利可能只是个开始。创建一个DebugDraw.cs的类,然后添加如下代码。
 

  1. using UnityEngine;
  2. using System.Collections;
  3. public static class DebugDraw {
  4.          
  5.         public static void DrawMarker(Vector3 position, float size, Color color, float duration, bool depthTest = true)
  6.         {
  7.                 Vector3 line1PosA = position + Vector3.up * size * 0.5f
  8.                 Vector3 line1PosB = position - Vector3.up * size * 0.5f;
  9.                  
  10.                 Vector3 line2PosA = position + Vector3.right * size * 0.5f;
  11.                 Vector3 line2PosB = position - Vector3.right * size * 0.5f;
  12.                  
  13.                 Vector3 line3PosA = position + Vector3.forward * size * 0.5f;
  14.                 Vector3 line3PosB = position - Vector3.forward * size * 0.5f;
  15.                  
  16.                 Debug.DrawLine(line1PosA, line1PosB, color, duration, depthTest);
  17.                 Debug.DrawLine(line2PosA, line2PosB, color, duration, depthTest);
  18.                 Debug.DrawLine(line3PosA, line3PosB, color, duration, depthTest);
  19.         }
  20. }

复制代码


这是一个我写的挺有用的帮助函数,它可以让我们在任何地方绘制在编辑器中(与此相对的,我们只能在OnDrawGizmos函数中绘制)。修改foreach循环如下。
 

  1. foreach (Collider col in Physics.OverlapSphere(transform.position, radius))
  2. {
  3.         Vector3 contactPoint = col.ClosestPointOnBounds(transform.position);
  4.          
  5.         DebugDraw.DrawMarker(contactPoint, 2.0f, Color.red, 0.0f, false);
  6.          
  7.         Vector3 v = transform.position - contactPoint;
  8.          
  9.         transform.position += Vector3.ClampMagnitude(v, Mathf.Clamp(radius - v.magnitude, 0, radius));
  10.          
  11.         contact = true;
  12. }

复制代码


运行代码,你会注意到当碰撞发生的时候,会在位置上绘制一个红色十字标记。现在,拖动player到墙体内,就能看到标记跟随者player。这对于ClosestPointOnBounds函数来说也不完全是个错误,但是如果要对应上上回提到的退回策略,我们真的希望有一个ClosestPointOnSurfaceOfBoundsOrSomething函数。
 


问题就在于当我们的角色在碰撞体内部的时候,随着返回最近点函数失效,没法正确地处理碰撞。现在,我们就来处理这个问题。

将我们的墙体在y轴上旋转大概20度,然后运行场景。你回发现一切都不正常了。这是因为ClosestPointOnBounds函数返回的最近点实在轴对齐包围盒(AABB)上,而不是朝向包围盒(OBB)上。
 


你可能已经在想如果这个问题扩展一下,不仅仅是盒子会是怎样。由于函数只能返回轴对齐包围盒的最近点,哪怕是其他类型的碰撞,它也是肯定没法得到表面的最近点的。因此这个问题是没有银弹的(也许是我没发现),我们只能每个碰撞类型自己实现。

先让我们从最简单的开始:球体碰撞。手机游戏账号拍卖在场景中创建一个新的球体游戏对象。找到表面上的最近点需要好几步,每一步都比较简单。要知道哪个方向推出玩家,我们计算从我们为之到球体中心的方向。由于球体表面每个点距离球心都一样,我们只要正规化我们的向量,然后乘以半径以及local scale因子即可。

下面是代码实现。你可以看到新的方法中,多加了一个检测当前OverlapSphere的碰撞类型。

  1. using UnityEngine;
  2. using System;
  3. using System.Collections.Generic;
  4. public class SuperCharacterController : MonoBehaviour {
  5. [SerializeField]
  6.         float radius = 0.5f;
  7.          
  8.         private bool contact;
  9.          
  10.         // Update is called once per frame
  11.         void Update () {
  12.                  
  13.                 contact = false;
  14.                  
  15.                 foreach (Collider col in Physics.OverlapSphere(transform.position, radius))
  16.                 {
  17.                         Vector3 contactPoint = Vector3.zero;
  18.                         
  19.                         if (col is BoxCollider)
  20.                         {
  21.                                 contactPoint = col.ClosestPointOnBounds(transform.position);
  22.                         }
  23.                         else if (col is SphereCollider)
  24.                         {
  25.                                 contactPoint = ClosestPointOn((SphereCollider)col, transform.position);
  26.                         }
  27.                         
  28.                         DebugDraw.DrawMarker(contactPoint, 2.0f, Color.red, 0.0f, false);
  29.                         
  30.                         Vector3 v = transform.position - contactPoint;
  31.                         
  32.                         transform.position += Vector3.ClampMagnitude(v, Mathf.Clamp(radius - v.magnitude, 0, radius));
  33.                         
  34.                         contact = true;
  35.                 }
  36.         }
  37.          
  38.         Vector3 ClosestPointOn(SphereCollider collider, Vector3 to)
  39.         {
  40.                 Vector3 p;
  41.                  
  42.                 p = to - collider.transform.position;
  43.                 p.Normalize();
  44.                  
  45.                 p *= collider.radius * collider.transform.localScale.x;
  46.                 p += collider.transform.position;
  47.                  
  48.                 return p;
  49.         }
  50.          
  51.         void OnDrawGizmos()
  52.         {
  53.                 Gizmos.color = contact ? Color.cyan : Color.yellow;
  54.                 Gizmos.DrawWireSphere(transform.position, radius);
  55.         }
  56. }

复制代码


机智的读者可能会发现ClosestPointOn实际上返回的是球体表面的最近点。不像ClosestPointOnBounds返回的是包围盒的最近点。这很简单,但是在使用之前还有一些问题要解决。现在来看看第二种(也是今天的最后一种)碰撞类型的实现:朝向包围盒。
 


图像演示了如何通过球心与控制器位置之间的向量推算出最近点

我们的一般做法是获取输入,然后clamp到box内部。这样的效果与内建的ClosestPointOnBounds是一样的,除了即使box带旋转也能处理之外。

Box Collider的扩展定义了局部大小x、y和z。为了将我们的点clamp到Box Collider内部,我们需要将作为从世界坐标系转换到Box Collider的局部坐标系。在完成之后,我们对位置clamp到包围盒内即可。最后,我们再将改点转换回世界坐标系。代码如下。
 

  1. using UnityEngine;
  2. using System;
  3. using System.Collections.Generic;
  4. public class SuperCharacterController : MonoBehaviour {
  5. [SerializeField]
  6.         float radius = 0.5f;
  7.          
  8.         private bool contact;
  9.          
  10.         // Update is called once per frame
  11.         void Update () {
  12.                  
  13.                 contact = false;
  14.                  
  15.                 foreach (Collider col in Physics.OverlapSphere(transform.position, radius))
  16.                 {
  17.                         Vector3 contactPoint = Vector3.zero;
  18.                         
  19.                         if (col is BoxCollider)
  20.                         {
  21.                                 contactPoint = ClosestPointOn((BoxCollider)col, transform.position);
  22.                         }
  23.                         else if (col is SphereCollider)
  24.                         {
  25.                                 contactPoint = ClosestPointOn((SphereCollider)col, transform.position);
  26.                         }
  27.                         
  28.                         DebugDraw.DrawMarker(contactPoint, 2.0f, Color.red, 0.0f, false);
  29.                         
  30.                         Vector3 v = transform.position - contactPoint;
  31.                         
  32.                         transform.position += Vector3.ClampMagnitude(v, Mathf.Clamp(radius - v.magnitude, 0, radius));
  33.                         
  34.                         contact = true;
  35.                 }
  36.         }
  37.          
  38.         Vector3 ClosestPointOn(BoxCollider collider, Vector3 to)
  39.         {
  40.                 if (collider.transform.rotation == Quaternion.identity)
  41.                 {
  42.                         return collider.ClosestPointOnBounds(to);
  43.                 }
  44.                  
  45.                 return closestPointOnOBB(collider, to);
  46.         }
  47.          
  48.         Vector3 ClosestPointOn(SphereCollider collider, Vector3 to)
  49.         {
  50.                 Vector3 p;
  51.                  
  52.                 p = to - collider.transform.position;
  53.                 p.Normalize();
  54.                  
  55.                 p *= collider.radius * collider.transform.localScale.x;
  56.                 p += collider.transform.position;
  57.                  
  58.                 return p;
  59.         }
  60.          
  61.         Vector3 closestPointOnOBB(BoxCollider collider, Vector3 to)
  62.         {
  63.                 // Cache the collider transform
  64.                 var ct = collider.transform;
  65.                  
  66.                 // Firstly, transform the point into the space of the collider
  67.                 var local = ct.InverseTransformPoint(to);
  68.                  
  69.                 // Now, shift it to be in the center of the box
  70.                 local -= collider.center;
  71.                  
  72.                 // Inverse scale it by the colliders scale
  73.                 var localNorm =
  74.                 new Vector3(
  75.                 Mathf.Clamp(local.x, -collider.size.x * 0.5f, collider.size.x * 0.5f),
  76.                 Mathf.Clamp(local.y, -collider.size.y * 0.5f, collider.size.y * 0.5f),
  77.                 Mathf.Clamp(local.z, -collider.size.z * 0.5f, collider.size.z * 0.5f)
  78.                 );
  79.                  
  80.                 // Now we undo our transformations
  81.                 localNorm += collider.center;
  82.                  
  83.                 // Return resulting point
  84.                 return ct.TransformPoint(localNorm);
  85.         }
  86.          
  87.         void OnDrawGizmos()
  88.         {
  89.                 Gizmos.color = contact ? Color.cyan : Color.yellow;
  90.                 Gizmos.DrawWireSphere(transform.position, radius);
  91.         }
  92. }

复制代码


你可能会注意到在主碰撞循环中做了一些修改,使得我们不管是轴对齐还是朝向的用ClosesPointOn就可以了。这里的大部分实现都参考自fholm的RPGController package。
 


到这里我们第一部分的实现就结束了。在后面的文章中,我会讲讲Unity物理API会遇到的一些问题。然后开始为实现理想中的角色控制器开发一些组件。

标签:控制器,collider,Vector3,碰撞,transform,Unity,radius,position,contactPoint
来源: https://blog.csdn.net/wangchewen/article/details/120473410