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; } } }
问题根源
- 协程无差别中断:
Open/Close方法中直接停止当前协程,若关闭动画执行到中途被开门操作打断,后续再触发关闭时,起始旋转角度不是完全打开的状态,最终无法回到初始位置。 - 插值误差未修正:
Quaternion.Slerp依赖time累积值,因Time.deltaTime的波动,循环结束时可能无法精确到达目标旋转,残留微小角度偏差。 - 状态更新时机错误:
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
相关产品推荐
相关产品推荐

