Unity3d 人物跳跃后落地悬空问题
问题描述
人物在检测到跳跃事件后,给一个初始速度
v
0
v_0
v0,根据重力加速度
g
g
g和时间差
t
t
t,计算出当前人物的速度为
v
=
v
0
−
g
∗
t
v= v_0 -g * t
v=v0−g∗t。在当前调用的
△
t
\triangle t
△t时间内,位移为
△
y
=
v
∗
△
t
\triangle y= v*\triangle t
△y=v∗△t。
检测是否落地采用了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)穿过地面等情况。
因此,最终跳跃的整套解决方法流程如下:
- 人物正在正常的移动,检测到玩家按下空格键(跳跃键)。
- 人物的动画状态机切换到跳跃动画。人物产生y轴方向上的初始速度
v
0
v_0
- 在每帧中更新人物的高度(也就是
v
0
∗
t
−
g
∗
t
2
/
2
v_0*t-g*t^2/2
- 若检测到碰撞,则将动画状态机切换为落地动画等。但不改变垂直方向速度,让它继续落地。
- 若检测到
isGrounded
成员变量标志为False
,那么增加垂直方向上的速度v
t
+
1
=
v
t
+
g
∗
△
t
v_{t+1} = v_t + g*\triangle t
- 若检测到
isGrounded
成员变量标志为True
,那么将垂直方向的速度设置为0,不再产生垂直方向上的位移。
重复步骤5和6,即使遇到地面凹陷情况,也可以准确的自由落体至掉进底部,效果还算可以。
注意点:
在上述的步骤6中,当检测到isGrounded
为True
时,由于某些神秘原因,会导致人物稍稍上移(大约在0.00000001左右),下一帧在Update
函数中检测isGrounded
时又会重新变成False
,最终导致该部分反复在往下掉和向上弹的过程中反复波动。
解决这个问题的方式,我采用了判断是否悬空有两个条件:
- 就是
isGrounded
标志为False
- 另外,就是用一条射线向下检测,如果检测到
hit
且distance
大于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;
}