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)。
请问问题根源是什么,该如何修复?
问题根源与修复方案
问题根源
- TaskCompletionSource(TCS)竞态条件:
SubmitObjs的while循环中每次迭代都会重新创建selection实例。若玩家在新TCS创建后、await前的极短时间内点击,TrySetResult会作用在旧TCS上,当前迭代的新TCS仍处于等待状态,导致无法触发后续逻辑。 - 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
相关产品推荐
相关产品推荐

