Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of WaitUntil.CoroutineEnds #821

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions Source/Core/Duality/Utility/Coroutines/Coroutine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,8 @@ internal void Update()

try
{
this.currentCondition.Update();
if (this.currentCondition.IsComplete)
{
if (this.enumerator.MoveNext())
this.currentCondition = this.enumerator.Current;
else
this.Status = CoroutineStatus.Complete;
}
if(!CoroutineHelper.MoveNext(this.enumerator, ref this.currentCondition))
this.Status = CoroutineStatus.Complete;
}
catch (Exception e)
{
Expand Down Expand Up @@ -89,4 +83,30 @@ public void Cancel()
this.Status = CoroutineStatus.Cancelled;
}
}

/// <summary>
/// Helper class for advancing a coroutine's enumerator
/// </summary>
internal static class CoroutineHelper
{
/// <summary>
/// Evaluates the current condition, and moves to the next one, if it exists
/// </summary>
/// <param name="enumerator">The Coroutine's enumerator</param>
/// <param name="currentCondition">The Coroutine's current wait condition</param>
/// <returns>True if it found another WaitUntil item, false otherwise</returns>
public static bool MoveNext(IEnumerator<WaitUntil> enumerator, ref WaitUntil currentCondition)
{
currentCondition.Update();

if (currentCondition.IsComplete)
{
if (enumerator.MoveNext())
currentCondition = enumerator.Current;
else
return false;
}
return true;
}
}
}
2 changes: 1 addition & 1 deletion Source/Core/Duality/Utility/Coroutines/CoroutineManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public Coroutine StartNew(IEnumerable<WaitUntil> enumerator)
coroutine = new Coroutine();

coroutine.Setup(enumerator);
coroutine.Update(); // run once as initialization phase, to get past the first Invalid (not yet set) Wait condition
coroutine.Update(); // run once as initialization phase, to position the enumerator on the first WaitUntil condiiton

this.nextCycle.Enqueue(coroutine);
return coroutine;
Expand Down
32 changes: 30 additions & 2 deletions Source/Core/Duality/Utility/Coroutines/WaitUntil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@ private enum WaitType
{
Frames,
GameTime,
RealTime
RealTime,
Function
}

/// <summary>
/// Waits until the next frame
/// </summary>
public static readonly WaitUntil NextFrame = new WaitUntil(1, WaitType.Frames);

private readonly WaitType type;
private float internalValue;
private readonly Func<float> updaterFunction;
private readonly WaitType type;

public bool IsComplete
{
Expand All @@ -32,9 +34,17 @@ public bool IsComplete
private WaitUntil(float startingValue, WaitType type)
{
this.internalValue = startingValue;
this.updaterFunction = null;
this.type = type;
}

private WaitUntil(Func<float> coroutineUpdate)
{
this.internalValue = float.PositiveInfinity;
this.updaterFunction = coroutineUpdate;
this.type = WaitType.Function;
}

internal void Update()
{
switch (this.type)
Expand All @@ -50,6 +60,10 @@ internal void Update()
case WaitType.RealTime:
this.internalValue -= Time.UnscaledDeltaTime;
break;

case WaitType.Function:
this.internalValue = this.updaterFunction();
break;
}
}

Expand Down Expand Up @@ -84,5 +98,19 @@ public static WaitUntil TimeSpan(TimeSpan timeSpan, bool realTime = false)
{
return WaitUntil.Seconds((float)timeSpan.TotalSeconds, realTime);
}

/// <summary>
/// Waits until the passed coroutine implementation has ended its execution
/// </summary>
/// <param name="coroutine"></param>
/// <returns></returns>
public static WaitUntil CoroutineEnds(IEnumerable<WaitUntil> coroutine)
{
IEnumerator<WaitUntil> enumerator = coroutine.GetEnumerator();
enumerator.MoveNext();

WaitUntil currentCondition = enumerator.Current;
return new WaitUntil(() => CoroutineHelper.MoveNext(enumerator, ref currentCondition) ? float.PositiveInfinity : 0);
}
}
}
98 changes: 68 additions & 30 deletions Test/Core/Utility/CoroutinesTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,39 +20,38 @@ public class CoroutinesTest
{
[Test] public void Basics()
{
Scene scene = new Scene();
scene.Activate();
CoroutineManager cm = new CoroutineManager();

CoroutineObject obj = new CoroutineObject();
Coroutine coroutine = scene.StartCoroutine(this.BasicRoutine(obj));
Coroutine coroutine = cm.StartNew(this.BasicRoutine(obj));

// All code until first yield is already executed
Assert.AreEqual(10, obj.Value);
Assert.True(coroutine.Status == CoroutineStatus.Running);

scene.Update();
cm.Update();

// All code until second yield is executed
Assert.AreEqual(20, obj.Value);
Assert.True(coroutine.Status == CoroutineStatus.Running);

scene.Update();
cm.Update();

// Yelded null, value didn't change, need to wait one more update
Assert.AreEqual(20, obj.Value);
Assert.True(coroutine.Status == CoroutineStatus.Running);

scene.Update();
cm.Update();
// Yielded condition is waiting for two frames now..
Assert.AreEqual(20, obj.Value);
Assert.True(coroutine.Status == CoroutineStatus.Running);

scene.Update();
cm.Update();
// All remaining code has been executed
Assert.AreEqual(30, obj.Value);
Assert.True(coroutine.Status == CoroutineStatus.Complete);

scene.Update();
cm.Update();

// No further changes
Assert.AreEqual(30, obj.Value);
Expand All @@ -61,30 +60,29 @@ [Test] public void Basics()

[Test] public void Cancelling()
{
Scene scene = new Scene();
scene.Activate();
CoroutineManager cm = new CoroutineManager();

CoroutineObject obj = new CoroutineObject();
Coroutine coroutine = scene.StartCoroutine(this.BasicRoutine(obj));
Coroutine coroutine = cm.StartNew(this.BasicRoutine(obj));

// All code until first yield is already executed
Assert.AreEqual(10, obj.Value);
Assert.True(coroutine.Status == CoroutineStatus.Running);

scene.Update();
cm.Update();

// All code until second yield is executed
Assert.AreEqual(20, obj.Value);
Assert.True(coroutine.Status == CoroutineStatus.Running);

coroutine.Cancel();
scene.Update();
cm.Update();

// Coroutine disposed
Assert.AreEqual(20, obj.Value);
Assert.True(coroutine.Status == CoroutineStatus.Cancelled);

scene.Update();
cm.Update();

// No further changes
Assert.AreEqual(20, obj.Value);
Expand All @@ -93,33 +91,32 @@ [Test] public void Cancelling()

[Test] public void Resuming()
{
Scene scene = new Scene();
scene.Activate();
CoroutineManager cm = new CoroutineManager();

CoroutineObject obj = new CoroutineObject();
Coroutine coroutine = scene.StartCoroutine(this.BasicRoutine(obj));
Coroutine coroutine = cm.StartNew(this.BasicRoutine(obj));

// All code until first yield is already executed
Assert.AreEqual(10, obj.Value);
Assert.True(coroutine.Status == CoroutineStatus.Running);

coroutine.Pause();
scene.Update();
cm.Update();

// No changes, since the coroutine is now paused
Assert.AreEqual(10, obj.Value);
Assert.True(coroutine.Status == CoroutineStatus.Paused);

int rnd = MathF.Rnd.Next(ushort.MaxValue);
for(int i = 0; i < rnd; i++)
scene.Update();
cm.Update();

// No matter how many updates
Assert.AreEqual(10, obj.Value);
Assert.True(coroutine.Status == CoroutineStatus.Paused);

coroutine.Resume();
scene.Update();
cm.Update();

// All code until second yield is executed
Assert.AreEqual(20, obj.Value);
Expand All @@ -128,12 +125,11 @@ [Test] public void Resuming()

[Test] public void WaitTwoSeconds()
{
Scene scene = new Scene();
scene.Activate();
CoroutineManager cm = new CoroutineManager();

int secondsToWait = 2;
CoroutineObject obj = new CoroutineObject();
Coroutine coroutine = scene.StartCoroutine(this.WaitSeconds(obj, secondsToWait));
Coroutine coroutine = cm.StartNew(this.WaitSeconds(obj, secondsToWait));

// All code until first yield is already executed
Assert.AreEqual(10, obj.Value);
Expand All @@ -142,7 +138,7 @@ [Test] public void WaitTwoSeconds()
// Waiting...
while (obj.Value == 10)
{
scene.Update();
cm.Update();
Time.FrameTick(true, true);
}

Expand All @@ -157,22 +153,47 @@ [Test] public void WaitTwoSeconds()
Assert.True(coroutine.Status == CoroutineStatus.Complete);
}

[Test] public void Exception()
[Test] public void Subroutines()
{
Scene scene = new Scene();
scene.Activate();
CoroutineManager cm = new CoroutineManager();
CoroutineObject obj = new CoroutineObject();

cm.StartNew(this.CoroutineMaster(obj));

Assert.AreEqual(0, obj.Value);
cm.Update();

// at this point obj.Value should be 1, as it has been updated by the slave coroutine
Assert.AreEqual(1, obj.Value);

for (int i = 2; i <= 100; i++)
{
cm.Update();
Assert.AreEqual(i, obj.Value);
}

// the slave coroutine ended, the master coroutine is stopped at [1], and the object's value should be 100
Assert.AreEqual(100, obj.Value);

// one last update, and it's reset to 0
cm.Update();
Assert.AreEqual(0, obj.Value);
}

Coroutine coroutine = scene.StartCoroutine(this.ExceptionRoutine());
[Test] public void Exception()
{
CoroutineManager cm = new CoroutineManager();
Coroutine coroutine = cm.StartNew(this.ExceptionRoutine());

// All code until first yield is already executed
Assert.True(coroutine.Status == CoroutineStatus.Running);

scene.Update();
cm.Update();
// All code until second yield is executed
Assert.True(coroutine.Status == CoroutineStatus.Running);

// Exception thrown, a log should appear
scene.Update();
cm.Update();
Assert.True(coroutine.Status == CoroutineStatus.Error);
}

Expand All @@ -199,5 +220,22 @@ private IEnumerable<WaitUntil> WaitSeconds(CoroutineObject obj, int seconds)
yield return WaitUntil.Seconds(seconds);
obj.Value = 20;
}

private IEnumerable<WaitUntil> CoroutineMaster(CoroutineObject obj)
{
obj.Value = 0;
yield return WaitUntil.NextFrame;
yield return WaitUntil.CoroutineEnds(this.CoroutineSlave(obj)); // [1]
obj.Value = 0;
}

private IEnumerable<WaitUntil> CoroutineSlave(CoroutineObject obj)
{
while (obj.Value < 100)
{
obj.Value++;
yield return WaitUntil.NextFrame;
}
}
}
}