Skip to content

Commit 7b45498

Browse files
committed
Monobehaviour-less coroutines. Fixed tests.
1 parent 31655a5 commit 7b45498

File tree

3 files changed

+154
-39
lines changed

3 files changed

+154
-39
lines changed

Runtime/Awaiters.cs

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,33 @@
1-
using System.Collections;
1+
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
34
using System.Runtime.CompilerServices;
45
using System.Threading;
56
using System.Threading.Tasks;
7+
using Gameframe.Async.Coroutines;
68
using UnityEngine;
79

810
namespace Gameframe.Async
911
{
1012
public static class Awaiters
1113
{
1214
private static readonly WaitForBackground _waitForBackground = new WaitForBackground();
13-
private static readonly WaitForUnityThread _waitForUnityThread = new WaitForUnityThread();
14-
15+
private static readonly WaitForUnityThread WaitForUnityUnityThread = new WaitForUnityThread();
16+
17+
/// <summary>
18+
/// Await this property to migrate an async method to a background thread
19+
/// </summary>
1520
public static WaitForBackground BackgroundThread => _waitForBackground;
16-
public static WaitForUnityThread MainThread => _waitForUnityThread;
21+
22+
/// <summary>
23+
/// Await this property to migrate to the Unity main thread.
24+
/// </summary>
25+
public static WaitForUnityThread MainUnityThread => WaitForUnityUnityThread;
26+
27+
/// <summary>
28+
/// Await this property will resume on the same context after the game has advanced a frame
29+
/// </summary>
30+
public static YieldAwaitable NextFrame => Task.Yield();
1731
}
1832

1933
public class WaitForBackground
@@ -24,18 +38,48 @@ public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter()
2438
}
2539
}
2640

27-
public class WaitForUpdate : CustomYieldInstruction
28-
{
29-
public override bool keepWaiting => false;
30-
}
31-
3241
public class WaitForUnityThread
33-
{
34-
public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter()
42+
{
43+
private class MainThreadJoinAwaiter : IAwaitable
3544
{
36-
var task = Task.Factory.StartNew(() => {}, CancellationToken.None, TaskCreationOptions.None, UnityTaskUtil.UnityTaskScheduler);
37-
return task.ConfigureAwait(false).GetAwaiter();
45+
private Action _continuation = null;
46+
private bool isCompleted = false;
47+
public bool IsCompleted => isCompleted;
48+
49+
public void Complete()
50+
{
51+
isCompleted = true;
52+
_continuation?.Invoke();
53+
}
54+
55+
public void GetResult()
56+
{
57+
do
58+
{
59+
} while (!isCompleted);
60+
}
61+
62+
void INotifyCompletion.OnCompleted(Action continuation)
63+
{
64+
_continuation = continuation;
65+
}
3866
}
67+
68+
public IAwaitable GetAwaiter()
69+
{
70+
var awaiter = new MainThreadJoinAwaiter();
71+
UnityTaskUtil.UnitySynchronizationContext.Post(state=>awaiter.Complete(),null);
72+
return awaiter;
73+
}
74+
}
75+
76+
/// <summary>
77+
/// Interface that implements all the properties needed to make an object awaitable
78+
/// </summary>
79+
public interface IAwaitable : INotifyCompletion
80+
{
81+
bool IsCompleted { get; }
82+
void GetResult();
3983
}
4084

4185
}

Runtime/Coroutines/CoroutineRunner.cs

Lines changed: 73 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,92 @@
1-
using System.Collections;
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Threading;
25
using System.Threading.Tasks;
36
using UnityEngine;
47

58
namespace Gameframe.Async.Coroutines
69
{
7-
public class CoroutineRunner : MonoBehaviour
10+
public class CoroutineRunner
811
{
9-
static CoroutineRunner _instance;
12+
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
13+
private static void Install()
14+
{
15+
UnitySynchronizationContext = SynchronizationContext.Current;
16+
Application.quitting += OnApplicationQuitting;
17+
}
18+
19+
private static SynchronizationContext UnitySynchronizationContext { get; set; }
20+
private static CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
1021

1122
public static async Task RunAsync(IEnumerator routine)
1223
{
13-
if (_instance == null)
24+
await UnityTaskUtil.RunOnUnityThreadAsync(() => Run(routine,cancellationTokenSource.Token) );
25+
}
26+
27+
public static void Start(IEnumerator enumerator)
28+
{
29+
UnityTaskUtil.RunOnUnityThread(() => Run(enumerator,cancellationTokenSource.Token));
30+
}
31+
32+
public static void StopAll()
33+
{
34+
cancellationTokenSource.Cancel();
35+
cancellationTokenSource = new CancellationTokenSource();
36+
}
37+
38+
private static void OnApplicationQuitting()
39+
{
40+
StopAll();
41+
}
42+
43+
private static async void Run(IEnumerator routine, CancellationToken token)
44+
{
45+
var coroutine = RunCoroutine(routine);
46+
while (!token.IsCancellationRequested && coroutine.MoveNext())
1447
{
15-
await InstantiateAsync();
48+
//Task.Yield() on the Unity sync context appears to yield for one frame
49+
await Task.Yield();
1650
}
17-
await UnityTaskUtil.StartCoroutineAsync(routine, _instance);
1851
}
19-
20-
private static async Task InstantiateAsync()
52+
53+
private static IEnumerator RunCoroutine(object state)
2154
{
22-
await UnityTaskUtil.RunOnUnityThreadAsync(() =>
55+
var processStack = new Stack<IEnumerator>();
56+
processStack.Push((IEnumerator)state);
57+
58+
while ( processStack.Count > 0)
2359
{
24-
if (_instance != null)
60+
var currentCoroutine = processStack.Peek();
61+
var done = false;
62+
63+
try
2564
{
26-
return;
65+
done = !currentCoroutine.MoveNext();
2766
}
28-
_instance = new GameObject("CoroutineRunner").AddComponent<CoroutineRunner>();
29-
});
30-
}
31-
32-
void Awake()
33-
{
34-
gameObject.hideFlags = HideFlags.HideAndDontSave;
35-
DontDestroyOnLoad(gameObject);
67+
catch (Exception e)
68+
{
69+
Debug.LogException(e);
70+
yield break;
71+
}
72+
73+
if (done)
74+
{
75+
processStack.Pop();
76+
}
77+
else
78+
{
79+
if (currentCoroutine.Current is IEnumerator innerCoroutine)
80+
{
81+
processStack.Push(innerCoroutine);
82+
}
83+
else
84+
{
85+
yield return currentCoroutine.Current;
86+
}
87+
}
88+
89+
}
3690
}
3791

3892
}

Tests/Runtime/AwaitersTests.cs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
using System.Collections;
2-
using System.Collections.Generic;
32
using System.Threading.Tasks;
4-
using Gameframe.Async;
53
using NUnit.Framework;
64
using UnityEngine;
75
using UnityEngine.TestTools;
8-
using UnityEngine.UI;
96

107
namespace Gameframe.Async.Tests
118
{
@@ -17,16 +14,36 @@ public IEnumerator TestBackgroundAndMainThreadAwaiters()
1714
var task = DoTest();
1815
yield return task.AsIEnumerator();
1916
}
20-
17+
2118
private async Task DoTest()
2219
{
2320
//Start on unity thread
2421
Assert.IsTrue(UnityTaskUtil.CurrentThreadIsUnityThread);
25-
await Awaiters.BackgroundThread;
22+
23+
//Migrate to background thread
24+
await Awaiters.BackgroundThread;
2625
Assert.IsFalse(UnityTaskUtil.CurrentThreadIsUnityThread);
27-
await Awaiters.MainThread;
26+
27+
//Migrate back to main thread
28+
await Awaiters.MainUnityThread;
29+
Assert.IsTrue(UnityTaskUtil.CurrentThreadIsUnityThread);
30+
31+
//Await the main thread when already on the main thread should do nothing
32+
await Awaiters.MainUnityThread;
2833
Assert.IsTrue(UnityTaskUtil.CurrentThreadIsUnityThread);
34+
35+
//Await the next frame
36+
int frame = Time.frameCount;
37+
await Task.Yield();
38+
Assert.IsTrue(UnityTaskUtil.CurrentThreadIsUnityThread);
39+
Assert.IsTrue(Time.frameCount == frame+1);
40+
41+
//Test can await next frame from background thread and then resume on background thread
42+
await Awaiters.BackgroundThread;
43+
Assert.IsFalse(UnityTaskUtil.CurrentThreadIsUnityThread);
44+
await Awaiters.NextFrame;
45+
Assert.IsFalse(UnityTaskUtil.CurrentThreadIsUnityThread);
2946
}
3047

3148
}
32-
}
49+
}

0 commit comments

Comments
 (0)