Unity IK追踪环绕玩家目标时侧后方单侧看向失效问题解决
Unity IK自动看向目标功能的环绕追踪异常修复
异常表现
- 测试立方体围绕玩家做圆周运动时,从玩家左侧移动至后方的路径上,玩家仅能短暂看向处于后方的立方体就快速转回头部
- 立方体从玩家右后方移动回前方的路径上,追踪逻辑完全失效,直到立方体几乎移动到玩家正前方位置才会触发看向逻辑,右后方返回段全程无追踪响应
- 尝试修改点积计算逻辑、将点积计算中
transform.forward的x、z分量取反后,右后方返回路径的追踪恢复正常,但左侧移动到后方的追踪又出现失效,仅能支持单侧路径的追踪,无法实现前后左右各方向环绕目标的均等追踪效果
问题复现代码
using UnityEngine; using System; using System.Collections; using System.Collections.Generic; [RequireComponent(typeof(Animator))] public class TestingIK : MonoBehaviour { public List<Transform> lookObjects = new List<Transform>(); public float weightDamping = 1.5f; private Animator animator; private Transform lastPrimaryTarget; private float lerpEndDistance = 0.1f; private float finalLookWeight = 0; private bool transitionToNextTarget = false; void Start() { animator = GetComponent<Animator>(); } // Callback for calculating IK void OnAnimatorIK() { if (lookObjects != null) { Transform primaryTarget = null; float closestLookWeight = 0; // Here we find the target which is closest (by angle) to the players view line foreach (Transform target in lookObjects) { Vector3 lookAt = target.position - transform.position; lookAt.y = 0f; float dotProduct = Vector3.Dot(new Vector3(transform.forward.x, 0f, transform.forward.z).normalized, lookAt.normalized); float lookWeight = Mathf.Clamp(dotProduct, 0f, 1f); if (lookWeight > closestLookWeight) { closestLookWeight = lookWeight; primaryTarget = target; } } if (primaryTarget != null) { if ((lastPrimaryTarget != null) && (lastPrimaryTarget != primaryTarget) && (finalLookWeight > 0f)) { // Here we start a new transition because the player looks already to a target but // we have found another target the player should look at transitionToNextTarget = true; } } // The player is in a neutral look position but has found a new target if ((primaryTarget != null) && !transitionToNextTarget) { lastPrimaryTarget = primaryTarget; finalLookWeight = Mathf.Lerp(finalLookWeight, closestLookWeight, Time.deltaTime * weightDamping); float bodyWeight = finalLookWeight * .75f; animator.SetLookAtWeight(finalLookWeight, bodyWeight, 1f); animator.SetLookAtPosition(primaryTarget.position); } // Let the player smoothly look away from the last target to the neutral look position if ((primaryTarget == null && lastPrimaryTarget != null) || transitionToNextTarget) { finalLookWeight = Mathf.Lerp(finalLookWeight, 0f, Time.deltaTime * weightDamping); float bodyWeight = finalLookWeight * .75f; animator.SetLookAtWeight(finalLookWeight, bodyWeight, 1f); animator.SetLookAtPosition(lastPrimaryTarget.position); if (finalLookWeight < lerpEndDistance) { transitionToNextTarget = false; finalLookWeight = 0f; lastPrimaryTarget = null; } } } } }
问题根因
- 点积权重计算逻辑错误:原代码直接对玩家正前方向量与目标方向向量的点积做0~1截断,意味着所有处于玩家身后(点积为负)的目标权重都会被直接判定为0,完全不会进入追踪逻辑,这是后方目标追踪失效的核心原因。取反forward分量本质是把判定方向反过来,只能修复另一侧的追踪,无法解决全方向判定问题。
- 目标切换逻辑不符合单目标环绕场景:原代码的
transitionToNextTarget逻辑是为多目标切换设计的,只要判定当前追踪目标和最优目标不一致,就会强制先把头部权重降到0(转回正视前方)再重新看向新目标。但单目标环绕场景下目标是连续移动的,不存在“切换到另一个目标”的情况,频繁触发重置逻辑就会出现“短暂看一眼就转头”“半程不追踪”的异常。 - 权重计算的角度阈值不符合人体结构:正常人转头水平方向可覆盖左右各80~90度范围,原逻辑只认正前方180度半球内的目标,完全没考虑颈部转动的实际角度范围,也没有做角度到权重的平滑过渡。
修复方案
- 调整角度权重计算逻辑:将水平方向最大可追踪角度设为左右各85度,根据目标和玩家正前方的夹角计算0~1的平滑权重,不再直接把负点积的目标权重置0,只要在可转动角度范围内的目标都能被正常追踪。
- 移除单目标场景下无效的切换重置逻辑:环绕测试场景只有一个追踪目标,不需要触发“先转回头再看新目标”的过渡,删除
transitionToNextTarget相关的强制降权逻辑,持续平滑更新目标位置和追踪权重即可。 - 优化权重平滑逻辑:保留阻尼渐变效果,保证追踪过程头部转动自然,不会出现突兀的跳变。
修复后核心代码片段
[RequireComponent(typeof(Animator))] public class TestingIK : MonoBehaviour { public List<Transform> lookObjects = new List<Transform>(); public float weightDamping = 5f; // 水平方向最大转头角度,左右各85度 public float maxHorizontalLookAngle = 85f; private Animator animator; private float currentLookWeight = 0; void Start() { animator = GetComponent<Animator>(); } void OnAnimatorIK() { if (animator == null || lookObjects == null || lookObjects.Count == 0) { // 没有目标时平滑转回头 currentLookWeight = Mathf.Lerp(currentLookWeight, 0f, Time.deltaTime * weightDamping); animator.SetLookAtWeight(currentLookWeight, currentLookWeight * 0.75f, 1f); return; } Transform primaryTarget = null; float highestWeight = 0f; foreach (Transform target in lookObjects) { if (target == null) continue; Vector3 targetDir = target.position - transform.position; targetDir.y = 0f; targetDir.Normalize(); Vector3 forwardDir = new Vector3(transform.forward.x, 0f, transform.forward.z).normalized(); // 计算目标和正前方的夹角 float angleToTarget = Vector3.Angle(forwardDir, targetDir); // 根据夹角计算权重:角度为0时权重1,超过最大角度时权重0,中间平滑过渡 float targetWeight = Mathf.Clamp01(1f - angleToTarget / maxHorizontalLookAngle); if (targetWeight > highestWeight) { highestWeight = targetWeight; primaryTarget = target; } } if (primaryTarget != null) { // 平滑更新权重 currentLookWeight = Mathf.Lerp(currentLookWeight, highestWeight, Time.deltaTime * weightDamping); animator.SetLookAtWeight(currentLookWeight, currentLookWeight * 0.75f, 1f); animator.SetLookAtPosition(primaryTarget.position); } else { // 无有效目标时平滑回头 currentLookWeight = Mathf.Lerp(currentLookWeight, 0f, Time.deltaTime * weightDamping); animator.SetLookAtWeight(currentLookWeight, currentLookWeight * 0.75f, 1f); } } }
内容的提问来源于stack exchange,提问作者Daniel Lip
相关产品推荐
相关产品推荐

