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

Unity AI门未完全关闭:协程未执行完毕致旋转残留

Unity AI门控脚本关闭不全问题修复

问题概述

AI通过门后,门无法完全关闭,Inspector显示残留0.1-1度旋转量。经排查,关闭门的协程未执行完毕,推测是NPC离开时触发StopCoroutine导致流程中断。

原脚本如下:

using System.Collections;
using UnityEngine;
using UnityEngine.AI;
public class DoorScript : MonoBehaviour
{
    private NavMeshObstacle Obstacle;
    public bool IsOpen = false;
    [SerializeField] private float Speed = 2f;
    [SerializeField] private float RotationAmount = 90f;
    [SerializeField] private float FowardDirection = 0f;
    private Vector3 StartRotation;
    private Vector3 Foward;
    private Coroutine AnimationCoroutine;

    private void Awake()
    {
        Obstacle = GetComponent<NavMeshObstacle>();
        Obstacle.carveOnlyStationary = false;
        Obstacle.carving = IsOpen;
        Obstacle.enabled = IsOpen;
        StartRotation = transform.rotation.eulerAngles;
        Foward = transform.right;

    }
    public void Open(Vector3 UserPosition)
    {
        if(!IsOpen)
        {
            if(AnimationCoroutine != null)
            {
                StopCoroutine(AnimationCoroutine);
            }
        }
        float dot = Vector3.Dot(Foward, (UserPosition - transform.position).normalized);
        AnimationCoroutine = StartCoroutine(DoRotationOpen(dot));
    }
    private IEnumerator DoRotationOpen(float ForwardAmount)
    {
        Quaternion startRotation = transform.rotation;
        Quaternion endRotation;
        if(ForwardAmount >= FowardDirection)
        {
            endRotation = Quaternion.Euler(new Vector3(StartRotation.x, startRotation.y - RotationAmount, startRotation.z));
        }
        else
        {
            endRotation = Quaternion.Euler(new Vector3(StartRotation.x, startRotation.y + RotationAmount, startRotation.z));
        }
        IsOpen = true;
        float time = 0;
        while(time < 1)
        {
            transform.rotation = Quaternion.Slerp(startRotation, endRotation, time);
            yield return null;
            time += Time.deltaTime * Speed;
        }
        Obstacle.enabled = true;
        Obstacle.carving = true;
    }
    public void Close()
    {
        if(IsOpen)
        {
            if(AnimationCoroutine != null)
            {
                StopCoroutine(AnimationCoroutine);
            }
            AnimationCoroutine = StartCoroutine(DoRotationClose());
            Obstacle.carving = false;
            Obstacle.enabled = false;

        }
    }
    private IEnumerator DoRotationClose()
    {
        Obstacle.carving = false;
        Obstacle.enabled = false;
        Quaternion startRotation = transform.rotation;
        Quaternion endRotation = Quaternion.Euler(StartRotation);
        IsOpen = false;
        float time = 0;
        while(time < 1)
        {
            transform.rotation = Quaternion.Slerp(startRotation, endRotation, time);
            yield return null;
            time += Time.deltaTime * Speed;
        }
    }
}

问题根源

  1. 协程无差别中断:Open/Close方法中直接停止当前协程,若关闭动画执行到中途被开门操作打断,后续再触发关闭时,起始旋转角度不是完全打开的状态,最终无法回到初始位置。
  2. 插值误差未修正:Quaternion.Slerp依赖time累积值,因Time.deltaTime的波动,循环结束时可能无法精确到达目标旋转,残留微小角度偏差。
  3. 状态更新时机错误:DoRotationClose中过早设置IsOpen = false,若协程被中断,状态标记与门的实际状态不一致,导致后续逻辑混乱。

修复方案

1. 优化协程管理逻辑

仅在当前协程与即将执行的操作冲突时才停止,避免无意义中断;同时在协程结束后清空AnimationCoroutine标记,确保状态准确。

2. 强制修正最终旋转角度

在协程循环结束后,直接将门的旋转设置为目标值,消除插值带来的误差。

3. 调整状态标记时机

将IsOpen的状态更新放在动画完全结束后,确保状态与门的实际状态同步。

修改后的完整脚本

using System.Collections;
using UnityEngine;
using UnityEngine.AI;

public class DoorScript : MonoBehaviour
{
    private NavMeshObstacle Obstacle;
    public bool IsOpen = false;
    [SerializeField] private float Speed = 2f;
    [SerializeField] private float RotationAmount = 90f;
    [SerializeField] private float ForwardDirection = 0f;
    private Vector3 StartRotation;
    private Vector3 Forward;
    private Coroutine AnimationCoroutine;
    // 标记当前协程类型,避免重复启动或无意义中断
    private enum CoroutineType { None, Opening, Closing }
    private CoroutineType CurrentCoroutineType = CoroutineType.None;

    private void Awake()
    {
        Obstacle = GetComponent<NavMeshObstacle>();
        Obstacle.carveOnlyStationary = false;
        Obstacle.carving = IsOpen;
        Obstacle.enabled = IsOpen;
        StartRotation = transform.rotation.eulerAngles;
        Forward = transform.right;
    }

    public void Open(Vector3 UserPosition)
    {
        // 若门已打开,或正在执行开门协程,直接返回
        if (IsOpen && CurrentCoroutineType == CoroutineType.Opening) return;
        
        // 若正在执行关闭协程,先停止
        if (AnimationCoroutine != null && CurrentCoroutineType == CoroutineType.Closing)
        {
            StopCoroutine(AnimationCoroutine);
            AnimationCoroutine = null;
        }
        
        float dot = Vector3.Dot(Forward, (UserPosition - transform.position).normalized);
        AnimationCoroutine = StartCoroutine(DoRotationOpen(dot));
    }

    private IEnumerator DoRotationOpen(float ForwardAmount)
    {
        CurrentCoroutineType = CoroutineType.Opening;
        Quaternion startRotation = transform.rotation;
        Quaternion endRotation;

        if (ForwardAmount >= ForwardDirection)
        {
            endRotation = Quaternion.Euler(new Vector3(StartRotation.x, startRotation.y - RotationAmount, startRotation.z));
        }
        else
        {
            endRotation = Quaternion.Euler(new Vector3(StartRotation.x, startRotation.y + RotationAmount, startRotation.z));
        }

        float time = 0;
        while (time < 1)
        {
            transform.rotation = Quaternion.Slerp(startRotation, endRotation, time);
            yield return null;
            time += Time.deltaTime * Speed;
        }
        // 强制设置最终旋转角度,消除插值误差
        transform.rotation = endRotation;
        
        IsOpen = true;
        Obstacle.enabled = true;
        Obstacle.carving = true;
        
        CurrentCoroutineType = CoroutineType.None;
        AnimationCoroutine = null;
    }

    public void Close()
    {
        // 若门已关闭,或正在执行关闭协程,直接返回
        if (!IsOpen && CurrentCoroutineType == CoroutineType.Closing) return;
        
        // 若正在执行开门协程,先停止
        if (AnimationCoroutine != null && CurrentCoroutineType == CoroutineType.Opening)
        {
            StopCoroutine(AnimationCoroutine);
            AnimationCoroutine = null;
        }
        
        AnimationCoroutine = StartCoroutine(DoRotationClose());
    }

    private IEnumerator DoRotationClose()
    {
        CurrentCoroutineType = CoroutineType.Closing;
        Obstacle.carving = false;
        Obstacle.enabled = false;
        
        Quaternion startRotation = transform.rotation;
        Quaternion endRotation = Quaternion.Euler(StartRotation);

        float time = 0;
        while (time < 1)
        {
            transform.rotation = Quaternion.Slerp(startRotation, endRotation, time);
            yield return null;
            time += Time.deltaTime * Speed;
        }
        // 强制设置最终旋转角度,消除插值误差
        transform.rotation = endRotation;
        
        IsOpen = false;
        
        CurrentCoroutineType = CoroutineType.None;
        AnimationCoroutine = null;
    }
}

额外优化建议

  • 可以添加门旋转的角度检测,当门的当前旋转与目标旋转的差值小于阈值时,直接设置为目标值,避免不必要的协程执行。
  • 针对NPC的触发逻辑,建议增加延迟关闭机制(比如NPC完全离开门区域后延迟0.5秒再触发关闭),避免频繁触发开门/关闭操作导致协程频繁中断。

内容的提问来源于stack exchange,提问作者BRO Animations

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 05:05:27