Unity IK实现玩家按最近目标切换注视的问题排查
问题根因
现有代码无法正常切换目标是几个逻辑错误共同导致的:
- 目标筛选逻辑写反:遍历
lookObjects的循环内部,每次迭代都会把closestLookWeight重置为float.MinValue,等于每次对比都清空之前的计算结果,循环结束后永远只会选中列表最后一个元素,这就是玩家始终注视第二个立方体的核心原因。 - 右手IK逻辑位置错误:设置右手IK位置、旋转的代码写在了遍历循环内部,每次遍历都会覆盖上一次的设置结果,最终右手永远指向列表最后一个目标。
- 缺少视野范围判断:当前逻辑没有设置前方视野的点积阈值,哪怕目标在玩家身后(点积为负)也会被判定为可注视目标,自然不会切到前方的对象。
- 注视权重逻辑失效:代码里把
SetLookAtWeight的参数写死为固定值,之前计算的finalLookWeight过渡值完全没有被使用,平滑转头的逻辑根本不生效。
多目标同处视野的选择逻辑
当多个目标同时处于玩家前方视野时,建议按以下优先级规则判定注视目标,避免逻辑异常:
- 基础判定:只选取点积结果>0(即处于玩家正半球视野内)的目标作为候选,身后目标直接排除。
- 第一优先级:候选目标中和玩家正前方视线夹角最小的(即点积值最大的)目标,符合人眼优先关注视线中心物体的习惯。
- 防频繁切换:加入滞回阈值,只有当新候选目标的综合得分比当前正在注视的目标高出固定阈值(比如点积差值大于0.1)时才触发切换,避免两个目标位置接近时视线反复横跳。
- 可选优化:可以给目标加距离权重、自定义优先级参数,比如距离玩家更近的交互物优先级高于远处的装饰物体,剧情触发的NPC优先级高于普通场景对象。
修复后的参考代码
using UnityEngine; using System; using System.Collections; using System.Collections.Generic; [RequireComponent(typeof(Animator))] public class IKTests : MonoBehaviour { public Transform finger; public List<Transform> lookObjects = new List<Transform>(); public float weightDamping = 1.5f; // 切换阈值,避免频繁跳变 public float switchThreshold = 0.1f; // 前方视野最小点积阈值,大于该值才判定为在视野内 public float minVisibleDot = 0.1f; private Animator animator; private Transform lastPrimaryTarget; private float lerpEndDistance = 0.1f; private float finalLookWeight = 0; private bool transitionToNextTarget = false; void Start() { animator = GetComponent<Animator>(); } private void OnDrawGizmos() { if (finger != null && lookObjects != null && lookObjects.Count > 0 && lastPrimaryTarget != null) { Gizmos.color = Color.green; Gizmos.DrawLine(finger.position, lastPrimaryTarget.position); } } void OnAnimatorIK() { if (animator == null || lookObjects == null || lookObjects.Count == 0) return; Transform primaryTarget = null; float closestLookWeight = minVisibleDot; Vector3 playerForward = new Vector3(transform.forward.x, 0f, transform.forward.z).normalized; // 遍历筛选得分最高的目标 foreach (Transform target in lookObjects) { if (target == null) continue; Vector3 lookDir = target.position - transform.position; lookDir.y = 0f; lookDir.Normalize(); float dotProduct = Vector3.Dot(playerForward, lookDir); // 只考虑视野内的目标,且得分高于当前最高值+切换阈值才替换 if (dotProduct > closestLookWeight + (target == lastPrimaryTarget ? 0 : switchThreshold)) { closestLookWeight = dotProduct; primaryTarget = target; } } // 右手IK只绑定最终选中的目标 if (primaryTarget != null) { animator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1); animator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1); animator.SetIKPosition(AvatarIKGoal.RightHand, primaryTarget.position); animator.SetIKRotation(AvatarIKGoal.RightHand, primaryTarget.rotation); } else { animator.SetIKPositionWeight(AvatarIKGoal.RightHand, 0); animator.SetIKRotationWeight(AvatarIKGoal.RightHand, 0); } if (primaryTarget != null) { if ((lastPrimaryTarget != null) && (lastPrimaryTarget != primaryTarget) && (finalLookWeight > 0f)) { transitionToNextTarget = true; } } // 正常注视目标逻辑,使用计算出的权重值 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); } // 过渡转场逻辑 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; } } } }
注:代码中移除了原空Update方法,补全了空值判断,调整了Gizmos绘制逻辑为绘制当前实际注视目标的连线,新增的阈值参数可根据实际手感在Inspector面板调整。
内容的提问来源于stack exchange,提问作者Daniel Lip
相关产品推荐
相关产品推荐

