U3D动作游戏开发读书笔记--3.1 物理系统详解(上)

第三章 物理系统详解

3.1 物理系统的基本梳理

3.1.1 系统参数设置

了解物理配置:

打开Project Settings设置

  • Gravity:重力,常用范围是60~80
  • Queries Hit Backfaces :进行背面查询,如果需要查询MeshCollider背面的情况,可开启
  • Layer Collision Matrix :物理相交矩阵,确定多个Layer层级之间的相交关系,不勾选则表示二者Layer层的物体不产生碰撞关系。

3.1.2 Fixed Update 更新频率

与Update每帧执行一次的轮询周期函数不同,Fixed Update函数的更新频率是固定的,按照设置好的时间间隔来执行。

3.1.3 Rigidbody 参数简介

  • Mass :刚体的质量,作用类似真实物理世界中的质量
  • Drag:阻尼 (不建议设置为0)
  • Angular Drag:角阻尼 旋转类型的阻尼
  • Use Gravity:是否使用重力
  • Is Kinematic :(是否为运动的物体)开启后物体将不受到物理特性的影响
  • Interpolate:插值方式
    • Interpolate内插值会落后后边一些,但比外插值平滑。
    • Extrapolate外插值会基于速度预测刚体位置,但可能会导致某一帧出现错误预测。
    • 对于需要物理表现的物体,建议选择内插值。
  • Collision Detection:碰撞检测方式,
    • Discrete 关闭连续碰撞检测
    • Continues 连续的碰撞检测,对于游戏中快速移动的物体,设置后可以防止穿墙
    • 对于次重要的物体,比如一些特效生成物,建议设置为ContinuousDynamic或者Continuous Speculative,以提升性能。
    • Constraints:刚体约束,勾选后会冻结某个轴上的移动或者旋转;

3.1.4 物理材质:

新建一个材质,一般只需要配置两种物理材质最大摩擦力和最小摩擦力类型即可

3.2 常见的问题

书中介绍了几个常见的问题,这里我们一起来跟随作者介绍来了解下。

3.2.1 物理步的理解

Unity3D中的物理更新时序是按照时间来进行的,每一个物理更新称之为物理步,依赖此的触发事件有OnTrigger、OnCollision系列和FixedUpdate等

主要理解物理刷新是固定的步进时序,和每帧更新调用的Update函数不同。

因此,若将输入监测逻辑或者需要每帧监测的逻辑放入物理步进相关函数中判断则会出错。

3.2.2 重叠与挤出问题

来看一个挤出问题:

当一个刚体对象A在另一个碰撞器B中时候,会发生挤出现象。

如果B对象也有刚体组件并且质量与A相当,那么会有相互的斥力;

如果B对象没有刚体组件或者刚体组件质量比A对象大很多,那么A对象便会被挤出。

如果被挤出的对象A在弹出的过程中遇到了其他非刚体碰撞体或者质量较大的刚体碰撞体,会骤停,卡在原地。

问题分析解决思路:

重叠造成的挤出位移不是一帧内就执行完成的,而是分多步完成,首先挤出持续发生,直到完全不发生重叠为止。由于挤出的方向并不能由用户自定义,所以课程产生朝外挤出的情况,也就是游戏中的穿墙问题。

穿墙问题一般都是由于一些特殊脚本控制瞬移操作造成的,所以要首先保证角色的碰撞检测为连续的,这样可以让刚体驱动的物体位移在高速移动下不会产生穿墙现象。

其次我们可以将一个比较大的场景碰撞拆分成多份,并将一些MeshCollider碰撞勾选Convex转换为凸包,以保证碰撞检测的结果正确性。

3.2.3 地面检测优化

地面检测(Ground Detection)是 Unity 游戏开发中一项非常基础且关键的技术,尤其在动作、平台跳跃或角色控制类游戏中。它主要用于判断游戏对象(尤其是角色)是否与地面接触,以及获取接触面的相关信息。

U3D自带的角色控制器(CharacterController)组件可以通过isGrounded字段来进行判断,但不够灵活。

也可以通过胶囊体碰撞体和地面之间的碰撞体之间的物理碰撞来检测,但可能受物理引擎更新频率影响,且在复杂逻辑中处理起来可能不如射线检测灵活。

另一种方法是在角色的底部发射一身射线去检测并保证每帧的执行。但是对于较为复杂的地面碰撞,一根摄像并不能很好的完成对地面的检测。可以使用多根射线投射分方式来进行检测。

C# 复制代码
void IsOnGroundUpdate(Transform[] groundPoints,LayerMask laterMask,
            float length,out bool isOnFround,out RaycastHit cacheRaycastHit)
        {
            isOnFround = false;
            cacheRaycastHit = new RaycastHit(); 
            foreach (Transform groundPoint in groundPoints)
            {
                if (Physics.Raycast(groundPoint.position, Vector3.down, out RaycastHit hit, length, laterMask))
                {
                    isOnFround = true;
                    cacheRaycastHit = hit;
                    break;
                }
            }
        }

一般设置使用三个检测点即可。

3.2.4 Dash与瞬移问题的处理

进行瞬移或者重逢类技能时候,需要严谨地考虑会产生的物理问题,所以不能随意地改变坐标来实现需求。

在冲锋或者瞬移之前可以使用SweepTest函数来对瞬移的目标点做测试,提前预判是否可以冲锋:

C# 复制代码
/// <summary>
/// 实现物体的瞬间移动(闪烁)功能,同时避免碰撞穿透
/// </summary>
/// <param name="trans">需要进行瞬移的目标物体的Transform组件</param>
/// <param name="targetPoint">期望瞬移到的目标位置坐标</param>
/// <param name="testTrans">用于碰撞检测计算的参考Transform,通常是自身或相关物体</param>
void BlinkTo(Transform trans, Vector3 targetPoint, Transform testTrans)
{
    // 计算从当前位置到目标位置的向量
    var diff = (targetPoint - transform.position);
    // 计算移动距离
    var length = diff.magnitude;
    // 计算标准化的移动方向
    var dir = diff.normalized;
    // 碰撞信息存储变量
    var hit = default(RaycastHit);
    // 获取需要移动物体的Rigidbody组件
    var selfRigidbody = trans.GetComponent<Rigidbody>();

    // 使用SweepTest模拟物体沿着移动路径移动,检测是否会与其他碰撞体发生碰撞
    if (selfRigidbody.SweepTest(dir, out hit, length))
    {
        // 如果检测到碰撞,计算碰撞体上距离参考点最近的边界点
        var targetClosestPoint = hit.collider.ClosestPointOnBounds(testTrans.position);
        // 计算自身碰撞体上距离目标最近点的最近边界点
        var selfClosestPoint = selfRigidbody.ClosestPointOnBounds(targetClosestPoint);
        // 计算从当前位置到自身最近点的偏移量
        var offset = selfClosestPoint - transform.position;

        // 将物体放置在刚好不发生碰撞穿透的位置
        trans.position = targetClosestPoint - offset;
    }
    else
    {
        // 没有碰到目标点 说明可以瞬移 进行瞬移
        trans.position = targetPoint;
    }
}

对于冲击这种非一次性闪现的多帧操作,需要考虑是否存在空中的因素,这里以空中冲击到Dash为例:

C# 复制代码
IEnumerator DashTo(Transform trans, Vector3 targetPosition)
{
    //准备瞬移的对象
    var startPos = trans.position;

    //此处可以对目标点进行在地面位置的修正
    targetPosition = GetGroundPosition(targetPosition);

    var waitForFixedUpdate = new WaitForFixedUpdate();
    var beginTime = Time.fixedTime;
    //按照物理更新时序 
    for (var duration = 0.15f; Time.fixedTime - beginTime <= duration;)
    {
        var t = (Time.fixedTime - beginTime) / duration;
        t = t * t;
        trans.position = Vector3.Lerp(startPos,targetPosition,t);
        yield return waitForFixedUpdate;
    }
}
        

要点:

【有了AI助手,分析起代码很容易,如果不好好思考和使用,真实说不过去!嘿嘿】