移動系統更新為假物理後, 衍生出很多違反物理的Bug, 例如
“可以無視牆壁直接跑進牆”.
而且 Unity3D 的 Raycast, 如果在原始點的時候就已經和物件重疊, 該次的 Raycast result 中將會被忽略
(已確認是官方的設計,並不會改)
所以如何解決掉在物件前面嘗試進行移動的路線修正方式, 就成了最迫切的課題.
為了解決這問題, 我的方案是減小 capsule 的 radius ,預留一個 padding 值, 然後在產生 collision 結果後重新加回去.
避免在下一次物理更新時發生上面 overlap 的問題.
更新進行上限的 2 次 Raycast 的組合
- 前方向檢查
- 前方不通行, 以牆身normal為準
取樣牆面的左/右, 並進行碰撞檢查
- 前方不通行, 以牆身normal為準
- 如以上的修改沒有進行, 檢查該區域有否重疊
黃線為前方障礙(大概只能在第一次接觸時看到)
紅線是避障AI的規避路線.
implement the feature for my fake physic reaction dealing with obstacle ahead. – use raycast + overlap – since raycast cannot detect the collision object at very begin sometime the avatar will not locate the wall in front of it. solution :
process 2 times raycast checking in fixed update.
- check obstacle based on movement ahead
- if blocking, try to project the current direction on wall, and try to override the movement is it’s possible.
- if ALL checking above aren’t doing anything, check if we WILL overlap any obstacle on target position, when that happen override position.
Debug vision :
the yellow line is the origin direction input by player.(only saw on first hit)
the red line is the slip away path then choose by the avoid obstacle logic.d
/// <summary>Vector3 = position, w = weight of this position.</summary>
private Vector3 AvoidObstacleOnPath(Vector3 referencePos)
{
Vector3 currentPos = m_Avatar.self.transform.position;
if (referencePos == currentPos)
return currentPos;
Debug.Assert(mode == eMode.Master, "Mode - " + mode.ToString("F") + "AvoidObstacleOnPath() should only called in " + eMode.Master.ToString("F") + " mode.");
Vector3 targetPos = referencePos;
float delta = Time.fixedDeltaTime;
Vector3 direction = referencePos - currentPos;
float distance = Mathf.Max(float.Epsilon, direction.magnitude);
direction.Normalize();
#if RADIUS_PADDING
const float s_RadiusPadding = 0.01f;
float radius = m_Avatar.self.capsule.radius - s_RadiusPadding;
#else
float radius = m_Avatar.self.capsule.radius;
#endif
bool posOverrideFlag = false;
// Physics 1
// Block detection along the path.
Vector3[] p = CapsuleCastData.GetCapsuleColliderPoints(m_Avatar.self.capsule, currentPos);
if (m_Raycast.CapsuleCast(p[0], p[1], radius, direction, distance, m_AvoidCollision, m_QueryTriggerInteraction))
{
posOverrideFlag = true;
// by default, we use that point to replace the current result.
#if RADIUS_PADDING
// Hotfix : always have padding for next raycast,
// Bug : Raycast overlap at begin will be ignore, result in NO BLOCKING!
targetPos = currentPos + direction * (m_Raycast.hitResult.distance - s_RadiusPadding);
#else
fixedPos = currentPos + direction * m_Raycast.hitResult.distance;
#endif
if (m_Debug)
Debug.DrawLine(currentPos, targetPos, Color.yellow, 3f);
// Physics 2
// Blocking detected, see if we can slip away based on current direction.
Vector3 hitNormal = m_Raycast.hitResult.normal.normalized;
float dot = Vector3.Dot(direction, -hitNormal);
if (dot >= 0f)
{
// try slip away, instead of stuck there.
Vector3 projectNormal = Vector3.ProjectOnPlane(direction, hitNormal).normalized;
float halfDistance = Mathf.Max(s_MinSlipDistance, distance * 0.5f);
/// detect obstacle, based on hit result <see cref="targetPos"/> is point stick on obstacle.
p = CapsuleCastData.GetCapsuleColliderPoints(m_Avatar.self.capsule, targetPos);
if (!m_Raycast.CapsuleCast(p[0], p[1], radius, projectNormal, halfDistance))
{
// path clear.
targetPos = targetPos + projectNormal * halfDistance;
if (m_Debug)
Debug.DrawLine(currentPos, targetPos, Color.red, 3f);
}
}
// else stuck in P1 result.
}
// Physic 3
// final fail safe, if above checking not run at all (Raycast + overlap at begin)
if (!posOverrideFlag)
{
Collider obj = IsOverlap(targetPos);
if (obj != null)
{
if (m_Debug)
Debug.LogWarning("AvoidObstacleOnPath fail, we overlap \'" + obj.name + "\' ahead, clamp mover position.", this);
return Vector3.Lerp(m_AvatarBody.position, m_Rigidbody.position, 0.5f);
}
}
return targetPos;
}
private Collider IsOverlap(Vector3 targetPos)
{
Collider[] objs = new Collider[5];
Vector3[] tp = CapsuleCastData.GetCapsuleColliderPoints(m_Avatar.self.capsule, targetPos);
float smallerRadius = Mathf.Max(float.Epsilon, m_Avatar.self.capsule.radius - 0.001f);
int overlapCnt = m_Capsule.OverlapNonAlloc(tp[0], tp[1], smallerRadius, ref objs, m_AvoidCollision, m_QueryTriggerInteraction);
// skip all avatar's collider. but report any others.
for (int i = 0; i < overlapCnt; i++)
{
if (!objs[i].transform.IsChildOf(m_AvatarBody.transform))
return objs[i];
}
return null;
}