【Unity3D Debug】跳跃时的地面检测:“CharacterController.isGrounded的值总是为false”的原因与解决方案

FPS控制器中处理跳跃时需要进行角色与地面的碰撞检测,如果使用Character Controller,会发现有一个isGrounded属性,其描述如下:

isGrounded: Was the CharacterController touching the ground during the last move?

其实际意思是上一次调用CharacterController.Move时有没有触地。

isGrounded如果恒为false:其一,可能没有在判断其值之前调用Move函数;其二, 有可能是因为没有将其判断语句放在正确的位置,比如在Y轴方向的移动操作之前就进行该属性的判断,这时候可能还没有触地,像如下的代码就会导致isGrounded恒为false。

// 根据curInput处理处理角色的真实移动
    public void UpdateMove()
    {
        if (isDead)  // 角色死亡
            return;
        float curSpeed = moveSpeed;
        dealWithMoveSpeed(ref curSpeed);  // 根据持有物确定移动速度
        Vector3 v = transform.TransformDirection(curMoveXZInput);  // 别忘了转为世界坐标系下的向量

        // 处理XZ平面的移动
        cc.Move(v * curSpeed * Time.deltaTime);  // Move方法传入的向量解读为世界坐标系下的向量       

        // 处理重力与起跳
        if (cc.isGrounded) 
        {
            velocity.y = -2f;  // 设为负数,确保在地面时能恒定接触地面,而不会误判为false
        }
        
        if(Input.GetKeyDown(KeyCode.Space) && cc.isGrounded)
        {
            //    m*g*h = 0.5*m*v_y0^2 - 0 -> v_y0 = sqrt(2*g*h)
            velocity.y = Mathf.Sqrt(-2f * GameManager.gravity * jumpHeight);
        }
        velocity.y += GameManager.gravity * Time.deltaTime;  // dv = g*t
        cc.Move(velocity * Time.deltaTime);  // 处理y轴方向的移动       
    }

正确的做法是,把含有isGrounded的判断语句放在施加重力以及Y轴方向的Move之后,如下代码所示(可以和上面的代码对比一下)。

// 根据curInput处理处理角色的真实移动
    public void UpdateMove()
    {
        if (isDead)  // 角色死亡
            return;
        float curSpeed = moveSpeed;
        dealWithMoveSpeed(ref curSpeed);  // 根据持有物确定移动速度
        Vector3 v = transform.TransformDirection(curMoveXZInput);  // 别忘了转为世界坐标系下的向量

        // 处理XZ平面的移动
        cc.Move(v * curSpeed * Time.deltaTime);  // Move方法传入的向量解读为世界坐标系下的向量       
        // 处理重力与起跳
        velocity.y += GameManager.gravity * Time.deltaTime;  // dv = g*t
        cc.Move(velocity * Time.deltaTime);  // 处理y轴方向的移动       
        if (cc.isGrounded)  // Code1
        {
            velocity.y = -2f;  // 设为负数,确保在地面时能恒定接触地面,而不会误判为false
        }
        if(Input.GetKeyDown(KeyCode.Space) && cc.isGrounded)
        {
            //    m*g*h = 0.5*m*v_y0^2 - 0 -> v_y0 = sqrt(2*g*h)
            velocity.y = Mathf.Sqrt(-2f * GameManager.gravity * jumpHeight);
        }
    }

另外,观察上面的代码,可以发现:为了确保触地,当isGrounded为true时,将Y轴方向的速度设为了一个较小的负值,这是为了确保在地面时能稳定触碰地面,避免isGrounded误判为false。

最后,如果只需要角色与部分层进行碰撞检测,可以通过Edit → ightarrow → Project Settings → ightarrow → Physics → ightarrow → Layer Collision Matrix,即修改层的碰撞矩阵,确认角色层所在的行和列,勾选与之交互的层,并取消其他层即可。举个例子,对于FirstPerson层,我只要确保它与Ground层、UI层进行碰撞,则影响范围和相关勾选状态如下图红线所示。

unity AddressablesMgr 使用 unity isgrounded_ci

笔者没有研究CharacterController.Move和.isGrounded的具体实现(Unity没有公开其源码),以上结论仅来自于笔者的个人实践,可能存在局限性。若存有谬误,欢迎大佬们批评指正!