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

Unity中C#静态ActionManager类异常行为求助

Unity ActionManager 协程动作偶发分支日志双打印问题排查

问题描述

在Unity项目中实现了静态类ActionManager,用于按顺序执行PlayableAction数组中的动作:部分动作需等待完成后再执行下一个,部分无需等待直接执行下一个。但出现偶发异常:当waitForCompletion为true的动作执行时,if和else分支的日志均会打印,Dequeue仅调用一次,且该问题仅在动作的Act()方法包含协程时出现。

相关代码片段

if (actionQueue.Count > 0)
{
    isActionPlaying = true;
    Debug.Log("Dequeue");
    _currentAction = actionQueue.Dequeue();
    if (_currentAction.waitForCompletion)
    {
        _currentAction.onActionCompleteEvent.AddListener(HandleActionCompleted);
        Logs.Print("Playing Action " + _currentAction.Name + " waiting for completion!. For the Query: " + _currentAction.Query);
        _currentAction.Act();
        Logs.Print("Act called on current action " + _currentAction.Name);
    }
    else
    {
        Logs.Print("Playing Action " + _currentAction.Name + " NOT waiting for completion!");
        _currentAction.Act();
        HandleActionCompleted();
        Logs.Print("Act called on current action " + _currentAction.Name + " NOT waiting for completion!. For the Query: " + _currentAction.Query);
    }
}

完整类代码

using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.Events;
using VRseBuilder.Core.Utility;

namespace VRseBuilder.Core.NoCode.Utility
{
    public static class ActionManager
    {
        private static Queue<PlayableAction> actionQueue = new Queue<PlayableAction>();
        private static bool isActionPlaying = false;

        private static PlayableAction _currentAction = null;
        public static UnityEvent onPlayableActionSequenceCompleted = new UnityEvent();


        // Method to add actions to the queue and start the sequence if not already started
        public static void PlayActionsInSequence(PlayableAction[] actions)
        {
            foreach (var action in actions)
            {
                actionQueue.Enqueue(action);
                Logs.Print("Adding to the current queue is:" + action.Name);
            }
            Logs.Print("The current length of action queue after clearing is :" + actionQueue.Count);
            if (!isActionPlaying)
            {
                PlayNextAction();
            }
        }

        // Plays the next action in the queue
        private static void PlayNextAction()
        {
            if (actionQueue.Count > 0)
            {
                isActionPlaying = true;
                _currentAction = actionQueue.Dequeue();
                if (_currentAction.waitForCompletion)
                {
                    _currentAction.onActionCompleteEvent.AddListener(HandleActionCompleted);
                    Logs.Print("Playing Action " + _currentAction.Name + " waiting for completion!. For the Query: " + _currentAction.Query);
                    _currentAction.Act();
                    Logs.Print("Act called on current action " + _currentAction.Name);
                }
                else
                {
                    Logs.Print("Playing Action " + _currentAction.Name + " NOT waiting for completion!");
                    _currentAction.Act();
                    HandleActionCompleted();
                    Logs.Print("Act called on current action " + _currentAction.Name + " NOT waiting for completion!. For the Query: " + _currentAction.Query);
                }
            }
            else
            {
                isActionPlaying = false;
            }
        }

        // Handles the completion of an action
        private static void HandleActionCompleted()
        {
            Logs.Print("Entered Handle Action Completed the count for actionQueue is" + actionQueue.Count);
            _currentAction.onActionCompleteEvent.RemoveListener(HandleActionCompleted);
            isActionPlaying = false;
            if (actionQueue.Count <= 0)
            {
                Logs.Print("All the actions in the queue played!");
                onPlayableActionSequenceCompleted?.Invoke();
                return;
            }
            if (actionQueue.Peek() != null)
            {
                PlayNextAction();// Unsubscribe to prevent memory leaks
            } // Play the next action in the sequence
        }
        // Adds an array of PlayableActions to the queue and starts the sequence if not already playing
        public static void AddActionsAndPlay(PlayableAction[] actions)
        {
            foreach (var action in actions)
            {
                actionQueue.Enqueue(action);
            }

            if (!isActionPlaying)
            {
                PlayNextAction();
            }
        }
        // CLears and then adds an array of PlayableActions to the queue and starts the sequence if not already playing
        public static void ClearPreviousAndPlayActionSet(PlayableAction[] actions)
        {
            if (_currentAction != null && isActionPlaying)
            {
                _currentAction.SkipAction();
                _currentAction.onActionCompleteEvent.RemoveListener(HandleActionCompleted);
            }
            //_currentAction.SkipAction();
            actionQueue.Clear();
            isActionPlaying = false;
            PlayActionsInSequence(actions);
        }


        // Adds a single PlayableAction to the queue
        public static void AddAction(PlayableAction action)
        {
            actionQueue.Enqueue(action);

            if (!isActionPlaying)
            {
                PlayNextAction();
            }
        }
        // Skips the current action without automatically proceeding to the next action in the queue
        public static void SkipCurrentAction(bool proceedToNext = true)
        {
            Debug.Log("Skipping Current Action");
            if (isActionPlaying)
            {
                // Assuming currentAction represents the currently playing action
                _currentAction?.SkipAction();
                _currentAction.onActionCompleteEvent.RemoveListener(HandleActionCompleted); // Unsubscribe

                isActionPlaying = false; // Stop the playback sequence

                if (proceedToNext)
                {
                    PlayNextAction(); // Proceed to next action only if specified
                }
            }
        }
        public static void PlayActionImmediately(PlayableAction action)
        {
            // Clear the queue of any pending actions
            actionQueue.Clear();

            // Unsubscribe from the current action's completion event and stop it if it's playing
            if (isActionPlaying)
            {
                // Assuming there's a way to get the currently playing action
                // This could be tracked by updating a currentAction variable in the PlayNextAction method
                // For simplicity in this example, we'll just skip the current action
                SkipCurrentAction(); // Note: Adjust SkipCurrentAction to not automatically play the next action in the queue
            }

            // Reset the playing flag as we're manually handling the next action
            isActionPlaying = false;

            // Add the new action to the queue and play it
            AddAction(action);
        }
    }

}

问题原因分析

  1. 静态类竞态条件:ActionManager是静态类,isActionPlaying、_currentAction等字段无线程同步保护。当协程的完成回调触发时,可能和主线程的PlayNextAction逻辑冲突:主线程刚进入waitForCompletion分支,回调就将isActionPlaying设为false,此时若有其他操作触发PlayNextAction,会导致同一_currentAction被重复判断,双分支日志打印。

  2. 协程回调时序问题:若Act()中的协程在同一帧内就触发onActionCompleteEvent,会导致HandleActionCompleted在PlayNextAction的waitForCompletion分支代码执行完前被调用,进而再次触发PlayNextAction,但队列中该动作已被Dequeue取出,所以只打印一次Dequeue日志。

  3. 事件订阅时序混乱:竞态下可能出现事件重复订阅或订阅未完成就取消的情况,导致HandleActionCompleted被多次调用,进一步加剧逻辑混乱。

修复方案

  • 添加线程同步锁:对静态字段的访问和修改加锁,确保同一时间只有一个线程执行关键逻辑:

    private static readonly object _lockObj = new object();
    
    // 在PlayNextAction、HandleActionCompleted等方法的关键代码块外层加锁
    lock(_lockObj)
    {
        // 原逻辑代码,如判断isActionPlaying、操作队列、修改_currentAction等
    }
    
  • 强制主线程执行事件回调:Unity的UnityEvent默认在主线程触发,但如果协程回调来自其他线程,需通过主线程调度器(如自行实现的UnityMainThreadDispatcher)触发onActionCompleteEvent,避免跨线程操作导致的状态异常。

  • 优化状态校验逻辑:在HandleActionCompleted中先判断_currentAction是否为空再取消订阅,避免空引用异常;在PlayNextAction中获取动作后再次确认waitForCompletion状态,防止竞态下状态被篡改。

  • 标记动作执行状态:给PlayableAction添加IsPlaying字段,在PlayNextAction执行前校验,避免同一动作被重复处理。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 03:50:09