Reputation: 11
I’m writing a turn-based game, and I have been using async methods to properly control consecutive actions. Whenever the player is prompted to choose objects to interact with, the game manager calls the below asynchronous method for that player (in the script attached to the player object):
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;
}
To allow the player to click on objects that they want to interact with, I have written Update()
in the same script as below as well:
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
GameObject DO = DetectValObj();
if(DO) selection.TrySetResult(DO); //TCS is Set here...
}
}
As you can see, the asynchronous SubmitObjs()
is supposed to be awaiting the TrySetResult()
in Update
so that when the player clicks on the object, it resumes the process to submit the object the player decided.
However, what really happens is that clicking on the objects and running selection.TrySetResult()
doesn’t resume the processing in SubmitObjs()
. I tested DetectValObjs()
and it is transferring DO
into TrySetResult()
properly, and what’s more frustrating is that in one in ten tries, SubmitObjs()
would resume and process with the passed object.
Honestly, I don’t think I understand TaskCompletionSource
and SetResult()
properly. I looked up many references and examples, but I can’t figure out why exactly does TrySetResult()
resumes awaited task only some of the time.
I tried ditching TCS entirely as well, having it replaced with a simple private GameObject sel
, with Task.Yield()
if sel
isn't assigned in Update()
:
while (!sel)
await Task.Yield();
obj = sel;
This version turned out to be worse, as I found out that Update()
wasn't getting called at all this time.
What exactly is going on here, and how can I fix this to make them work as intended?
+Edit(2024-01-07): I checked if the SynchronizationContext
was different in Update()
and SubmitObjs()
, and they were both under UnityEngine.UnitySynchronizationContext
. Making Update()
asynchronous with await Task.Yield()
has proven to be ineffective as well.
+Edit(2024-01-09): As per Anton Tykhyy's suggestion, I have tried to replace the use of TaskCompletionSource into System.Threading.Channels. Checking if SubmitObjs()
and Update()
were working with the same Channel<GameObject>
instance has shown that they were in fact different, even though there is but a singular channel reference in the scope of the script. There are no initializations of the Channel other than the declaration in the script (it is declared to be readonly
).
Upvotes: 1
Views: 375
Reputation: 11
A Partial Answer, but it still answers at least half of it:
I have since broke away from trying to synchronize Update()
and SubmitObjs()
. I tried to find a way to get Input for each frame inside SubmitObjs()
without the need of Update()
, and I have found a solution of WaitUntil(() => Input.GetMouseButtonDown(0))
to detect Mouse click for each frame. Here is the Updated SubmitObjs()
, using UniTask
as a replacement:
public async UniTask<List<GameObject>> SubmitObjs(List<GameObject> options, int selN)
{
List<GameObject> result = new List<GameObject>();
centralTime.ToggleOutlines(options, 1, true);
while (result.Count < selN)
{
await UniTask.WaitUntil(() => Input.GetMouseButtonDown(0));
GameObject obj = DetectValObj();
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;
}
There is still issues with Physics.Raycast()
not detecting Objects properly, but at least the input is being received each frame properly.
Upvotes: 0
Reputation: 20086
The problem is not with TaskCompletionSource
itself but with the shared variable selection
which you are reading and writing from multiple threads without any synchronization. Nothing guarantees that Update()
calls TrySetResult()
on the same TaskCompletionSource
object that you are assigning in SubmitObjs()
. Also, multiple updates could happen while SumbitObjs()
is processing the one it has read, causing updates to be lost. Your idea of how the code ought to work seems to fit the producer/consumer pattern, so the simplest solution for you is to use System.Threading.Channels
instead of a bare TaskCompletionSource
. Channels handle all synchronization details for you and give you an easy to understand abstraction to work with.
// at top level
private readonly Channel<GameObject> selections = Channel.CreateUnbounded<GameObject>() ;
// Update()
if(DO) selections.Writer.TryWrite(DO);
// SubmitObjs()
while (result.Count < selN)
{
GameObject obj = await selections.Reader.ReadAsync();
Upvotes: -1