You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 13:48:19