Unity3d 人物跳跃后落地悬空问题


问题描述

人物在检测到跳跃事件后,给一个初始速度

v

0

v_0

v0,根据重力加速度

g

g

g和时间差

t

t

t,计算出当前人物的速度为

v

=

v

0

g

t

v= v_0 -g * t

v=v0gt。在当前调用的

t

\triangle t

t时间内,位移为

y

=

v

t

\triangle y= v*\triangle t

y=vt
检测是否落地采用了Physics.CapsuleCast函数,判断人物是否与地面足够近,然后重新设置人物的坐标(根据hit.distance计算出的与地面的距离)。

但是,不知为何人物在落地时,总会高出地面0.25,后续跳跃就总是以此为平面了。

检测碰撞的代码如下:

// 判断是否落地
void GroundCheck()
{
    // Make sure that the ground check distance while already in air is very small, to prevent suddenly snapping to ground
    float groundCheckDistance = -m_JumpSpeed * Time.deltaTime;       
    float slopeLimit = 0.1f;        //  可落地地形角度

    // reset values before the ground check
    Vector3 m_GroundNormal = Vector3.up;
    Debug.Log(check_height);

    if (m_JumpSpeed <= 0)
    {
        // if we're grounded, collect info about the ground normal with a downward capsule cast representing our character capsule
        if (Physics.CapsuleCast(GetCapsuleBottomPoint(), GetCapsuleTopPoint(), m_CharaContr.radius, Vector3.down, out RaycastHit hit, 0.1f))
        {
            // storing the upward direction for the surface found
            m_GroundNormal = hit.normal;

            // Only consider this a valid ground hit if the ground normal goes in the same direction as the character up
            // and if the slope angle is lower than the character controller's limit
            if (Vector3.Dot(hit.normal, transform.up) > 0f &&
                Vector3.Angle(transform.up, m_GroundNormal) <= slopeLimit)
            {
                isJumping = false;
                m_Animator.SetBool("isJumping", isJumping);
               // handle snapping to the ground
                if (hit.distance > m_CharaContr.skinWidth)
                {
                    m_CharaContr.transform.Translate(Vector3.down * hit.distance);
                }
            }
        }
    }
}

Vector3 GetCapsuleBottomPoint()
{
    return m_CharaContr.transform.position + m_CharaContr.center + Vector3.up * -m_CharaContr.height * 0.5F;
}

Vector3 GetCapsuleTopPoint()
{
    return GetCapsuleBottomPoint() + Vector3.up * m_CharaContr.height;
}

通过一番排查,问题主要出在Physics.CapsuleCast部分,它在距离地面0.25f+hit.distance的时候就认为扫描到了碰撞体。此时,transform.position.y总在0.25附近,最终导致悬空。

至于出现这个问题的原因,还尚未清楚。


5/17
距离记录这个问题,已有一段时日,后续也并未找出碰撞检测的位置和坐标不一致的具体原因,最终采用了另一种方法来解决落地问题。
涉及到y轴的变化(跳跃行为,地形变化)时,必然要每帧检测与地面的距离以确保人物贴在地上,在上述诡异的情况下,我采用了CharacterController控件提供的isGrounded成员变量来作为是否落地的直接依据。同时采用CharacterController控件提供的Move函数来控制人物的移动,也能有效保证人物不会1)穿过带碰撞体的物体,2)穿过地面等情况。

因此,最终跳跃的整套解决方法流程如下:

  1. 人物正在正常的移动,检测到玩家按下空格键(跳跃键)。
  2. 人物的动画状态机切换到跳跃动画。人物产生y轴方向上的初始速度

    v

    0

    v_0

    v0
  3. 在每帧中更新人物的高度(也就是

    v

    0

    t

    g

    t

    2

    /

    2

    v_0*t-g*t^2/2

    v0tgt2/2
    ),同时检测是否与地面碰撞。
  4. 若检测到碰撞,则将动画状态机切换为落地动画等。但不改变垂直方向速度,让它继续落地。
  5. 若检测到isGrounded成员变量标志为False,那么增加垂直方向上的速度

    v

    t

    +

    1

    =

    v

    t

    +

    g

    t

    v_{t+1} = v_t + g*\triangle t

    vt+1=vt+gt
    和位移。
  6. 若检测到isGrounded成员变量标志为True,那么将垂直方向的速度设置为0,不再产生垂直方向上的位移。

重复步骤5和6,即使遇到地面凹陷情况,也可以准确的自由落体至掉进底部,效果还算可以。

注意点
在上述的步骤6中,当检测到isGroundedTrue时,由于某些神秘原因,会导致人物稍稍上移(大约在0.00000001左右),下一帧在Update函数中检测isGrounded时又会重新变成False,最终导致该部分反复在往下掉和向上弹的过程中反复波动。
解决这个问题的方式,我采用了判断是否悬空有两个条件:

  1. 就是isGrounded标志为False
  2. 另外,就是用一条射线向下检测,如果检测到hitdistance大于0.01就认为是已经悬空,开始制造垂直方向的加速度。

综合上述,就可以比较完美的实现跳跳乐了,甚至奇形怪状的地形,石头等,也可以安然落在上面。

示意代码如下所示:

void Update()
{
    m_PlayerMovement = m_PlayerMoveStat.GetPlayerMove() * moveSpeed * Time.deltaTime;
    m_PlayerMovement.y = GetVerticalHeight();
    
    m_CharaContr.Move(transform.rotation * m_PlayerMovement);   // 沿着人物的前方行走
}

public void JumpStart()
{
    m_LastJumpedTime = Time.time;
    m_VerticalSpeed = jumpSpeed;
}

float GetVerticalHeight()
{
    if (!isLanded())    // 未着陆状态
    {
        // 下面这个判断是为了处理,当前人物已着路,但离地面距离为0,此时,isGround被认定为false,需要进一步检测与地面距离
        if (Physics.Raycast(GetCapsuleBottomPoint(), Vector3.down, out RaycastHit hit, 777f) && hit.distance > 0.01f) 
        {
            m_VerticalSpeed -= gravityForce * Time.deltaTime;
            m_VerticalSpeed = Mathf.Clamp(m_VerticalSpeed, -2*jumpSpeed, 2*jumpSpeed);
            // h = v0 * t - 1 / 2 * g * t^2
        }
        //Debug.Log(string.Format("unlanded: {0}", transform.position.y));
        return m_VerticalSpeed * Time.deltaTime;
    }
    else 
    {
        //Debug.Log(string.Format("islanded: {0}", transform.position.y));
        m_VerticalSpeed = 0;
        return 0;
    }
}


bool isLanded()
{
    // 判断是否着路
    //Debug.Log(m_CharaContr.isGrounded);
    return m_CharaContr.isGrounded;
}

版权声明:本文为wz2671原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>