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

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;
                }
            }
        }
    }
}

问题根因

  1. 点积权重计算逻辑错误:原代码直接对玩家正前方向量与目标方向向量的点积做0~1截断,意味着所有处于玩家身后(点积为负)的目标权重都会被直接判定为0,完全不会进入追踪逻辑,这是后方目标追踪失效的核心原因。取反forward分量本质是把判定方向反过来,只能修复另一侧的追踪,无法解决全方向判定问题。
  2. 目标切换逻辑不符合单目标环绕场景:原代码的transitionToNextTarget逻辑是为多目标切换设计的,只要判定当前追踪目标和最优目标不一致,就会强制先把头部权重降到0(转回正视前方)再重新看向新目标。但单目标环绕场景下目标是连续移动的,不存在“切换到另一个目标”的情况,频繁触发重置逻辑就会出现“短暂看一眼就转头”“半程不追踪”的异常。
  3. 权重计算的角度阈值不符合人体结构:正常人转头水平方向可覆盖左右各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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 10:51:21