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

Unity异步交互异常:Update中SetResult/Write无法恢复等待任务

问题描述

我正在开发一款回合制游戏,使用异步方法控制连续操作。当需要玩家选择交互对象时,游戏管理器会调用玩家对象脚本中的以下异步方法:

public async Task<List<GameObject>> SubmitObjs(List<GameObject> options, int selN)
{
    List<GameObject> result = new List<GameObject>();
    centralTime.ToggleOutlines(options, 1, true);
    while (result.Count < selN)
    {
        selection = new TaskCompletionSource<GameObject>(TaskCreationOptions.RunContinuationsAsynchronously);
        GameObject obj = await selection.Task; //Awaited TCS here...

        if (obj && options.Contains(obj))
            if (result.Contains(obj))
            {
                result.Remove(obj);
                centralTime.ToggleOutlines(new List<GameObject>() { obj }, 1, true);
            }
            else
            {
                result.Add(obj);
                centralTime.ToggleOutlines(new List<GameObject>() { obj }, 2, true);
            }
    }
    centralTime.OutlineOff();
    return result;
}

同时,为实现玩家点击选择对象,我在同一脚本中编写了Update方法:

private void Update()
{         
    if (Input.GetMouseButtonDown(0))
    {             
        GameObject DO = DetectValObj();             
        if(DO) selection.TrySetResult(DO); //TCS is Set here...         
     }     
}

预期流程为SubmitObjs方法等待Update中的TrySetResult调用,玩家点击后恢复处理,但实际点击后多数情况无法恢复,仅偶尔生效,且DetectValObj方法能正确传递对象。

我曾尝试用私有GameObject变量配合Task.Yield替代TaskCompletionSource,但此时Update方法完全不执行。

补充信息:

  • 2024-01-07:确认Update和SubmitObjs的SynchronizationContext均为UnityEngine.UnitySynchronizationContext,将Update改为异步也无效。
  • 2024-01-09:尝试替换为System.Threading.Channels,但发现SubmitObjs和Update使用的Channel实例不同(脚本中仅声明一个readonly的Channel)。

请问问题根源是什么,该如何修复?


问题根源与修复方案

问题根源

  1. TaskCompletionSource(TCS)竞态条件:SubmitObjs的while循环中每次迭代都会重新创建selection实例。若玩家在新TCS创建后、await前的极短时间内点击,TrySetResult会作用在旧TCS上,当前迭代的新TCS仍处于等待状态,导致无法触发后续逻辑。
  2. Channel实例不一致:脚本被重复创建(场景多实例、重复加载),导致SubmitObjs和Update引用了不同的Channel对象。

修复方案

方案一:修复TCS竞态问题

通过增加状态判断,确保点击仅作用于当前等待的TCS:

private TaskCompletionSource<GameObject> selection;

public async Task<List<GameObject>> SubmitObjs(List<GameObject> options, int selN)
{
    List<GameObject> result = new List<GameObject>();
    centralTime.ToggleOutlines(options, 1, true);
    
    while (result.Count < selN)
    {
        selection = new TaskCompletionSource<GameObject>(TaskCreationOptions.RunContinuationsAsynchronously);
        GameObject obj = await selection.Task;

        if (obj && options.Contains(obj))
        {
            if (result.Contains(obj))
            {
                result.Remove(obj);
                centralTime.ToggleOutlines(new List<GameObject>() { obj }, 1, true);
            }
            else
            {
                result.Add(obj);
                centralTime.ToggleOutlines(new List<GameObject>() { obj }, 2, true);
            }
        }
        selection = null;
    }
    
    centralTime.OutlineOff();
    return result;
}

private void Update()
{         
    if (Input.GetMouseButtonDown(0) && selection != null)
    {             
        GameObject DO = DetectValObj();             
        if(DO) selection.TrySetResult(DO);         
     }     
}

核心改动:Update中增加selection != null判断,仅当SubmitObjs处于等待状态时处理点击,避免触发失效的TCS。

方案二:改用状态变量+Task.Yield(推荐)

利用Task.Yield让出主线程,结合状态变量控制Update逻辑,避免TCS的竞态问题:

private GameObject pendingSelection;
private bool isWaitingForSelection;

public async Task<List<GameObject>> SubmitObjs(List<GameObject> options, int selN)
{
    List<GameObject> result = new List<GameObject>();
    centralTime.ToggleOutlines(options, 1, true);
    isWaitingForSelection = true;
    
    while (result.Count < selN)
    {
        pendingSelection = null;
        while (pendingSelection == null)
        {
            await Task.Yield(); // 让出主线程,保证Update执行
        }
        
        GameObject obj = pendingSelection;
        if (obj && options.Contains(obj))
        {
            if (result.Contains(obj))
            {
                result.Remove(obj);
                centralTime.ToggleOutlines(new List<GameObject>() { obj }, 1, true);
            }
            else
            {
                result.Add(obj);
                centralTime.ToggleOutlines(new List<GameObject>() { obj }, 2, true);
            }
        }
    }
    
    isWaitingForSelection = false;
    centralTime.OutlineOff();
    return result;
}

private void Update()
{         
    if (!isWaitingForSelection) return;
    
    if (Input.GetMouseButtonDown(0))
    {             
        GameObject DO = DetectValObj();             
        if(DO) pendingSelection = DO;         
     }     
}

此方案通过isWaitingForSelection控制Update是否处理点击,Task.Yield确保异步方法不会阻塞主线程,Update能正常执行。

方案三:修复Channel实例不一致(单例模式)

通过单例模式确保脚本唯一,保证SubmitObjs和Update使用同一个Channel:

public class PlayerSelectionScript : MonoBehaviour
{
    public static PlayerSelectionScript Instance { get; private set; }
    private readonly Channel<GameObject> selectionChannel = Channel.CreateUnbounded<GameObject>();

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }
        Instance = this;
        DontDestroyOnLoad(gameObject);
    }

    public async Task<List<GameObject>> SubmitObjs(List<GameObject> options, int selN)
    {
        List<GameObject> result = new List<GameObject>();
        centralTime.ToggleOutlines(options, 1, true);
        
        while (result.Count < selN)
        {
            GameObject obj = await selectionChannel.Reader.ReadAsync();
            if (obj && options.Contains(obj))
            {
                if (result.Contains(obj))
                {
                    result.Remove(obj);
                    centralTime.ToggleOutlines(new List<GameObject>() { obj }, 1, true);
                }
                else
                {
                    result.Add(obj);
                    centralTime.ToggleOutlines(new List<GameObject>() { obj }, 2, true);
                }
            }
        }
        
        centralTime.OutlineOff();
        return result;
    }

    private void Update()
    {         
        if (Input.GetMouseButtonDown(0))
        {             
            GameObject DO = DetectValObj();             
            if(DO) selectionChannel.Writer.TryWrite(DO);         
         }     
    }
}

单例模式保证游戏中只有一个脚本实例,Channel对象唯一,避免实例不一致问题。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 02:14:55