移動系統更新為假物理後, 衍生出很多違反物理的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; }