mirror of
https://github.com/Ellpeck/Coroutine.git
synced 2024-12-22 00:59:23 +01:00
increase performance and scalability by storing event coroutines linked to their event
This commit is contained in:
parent
548e07f19f
commit
501d744326
6 changed files with 63 additions and 69 deletions
|
@ -13,6 +13,9 @@ namespace Coroutine {
|
|||
private readonly Stopwatch stopwatch;
|
||||
private Wait current;
|
||||
|
||||
internal Event Event => this.current.Event;
|
||||
internal bool IsWaitingForEvent => this.Event != null;
|
||||
|
||||
/// <summary>
|
||||
/// This property stores whether or not this active coroutine is finished.
|
||||
/// A coroutine is finished if all of its waits have passed, or if it <see cref="WasCanceled"/>.
|
||||
|
@ -79,18 +82,14 @@ namespace Coroutine {
|
|||
}
|
||||
|
||||
internal bool Tick(double deltaSeconds) {
|
||||
if (!this.WasCanceled) {
|
||||
if (this.current.Tick(deltaSeconds))
|
||||
this.MoveNext();
|
||||
}
|
||||
if (!this.WasCanceled && this.current.Tick(deltaSeconds))
|
||||
this.MoveNext();
|
||||
return this.IsFinished;
|
||||
}
|
||||
|
||||
internal bool OnEvent(Event evt) {
|
||||
if (!this.WasCanceled) {
|
||||
if (this.current.OnEvent(evt))
|
||||
this.MoveNext();
|
||||
}
|
||||
if (!this.WasCanceled && Equals(this.current.Event, evt))
|
||||
this.MoveNext();
|
||||
return this.IsFinished;
|
||||
}
|
||||
|
||||
|
@ -112,10 +111,6 @@ namespace Coroutine {
|
|||
return true;
|
||||
}
|
||||
|
||||
internal bool IsWaitingForEvent() {
|
||||
return this.current.IsWaitingForEvent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A delegate method used by <see cref="ActiveCoroutine.OnFinished"/>.
|
||||
/// </summary>
|
||||
|
|
|
@ -11,9 +11,9 @@ namespace Coroutine {
|
|||
public class CoroutineHandlerInstance {
|
||||
|
||||
private readonly List<ActiveCoroutine> tickingCoroutines = new List<ActiveCoroutine>();
|
||||
private readonly List<ActiveCoroutine> eventCoroutines = new List<ActiveCoroutine>();
|
||||
private readonly Dictionary<Event, List<ActiveCoroutine>> eventCoroutines = new Dictionary<Event, List<ActiveCoroutine>>();
|
||||
private readonly Queue<ActiveCoroutine> eventCoroutinesToRemove = new Queue<ActiveCoroutine>();
|
||||
private readonly Queue<ActiveCoroutine> outstandingCoroutines = new Queue<ActiveCoroutine>();
|
||||
private readonly ISet<int> eventCoroutinesToRemove = new HashSet<int>();
|
||||
private readonly Stopwatch stopwatch = new Stopwatch();
|
||||
|
||||
/// <summary>
|
||||
|
@ -23,7 +23,7 @@ namespace Coroutine {
|
|||
/// <summary>
|
||||
/// The amount of <see cref="ActiveCoroutine"/> instances that are currently waiting for an <see cref="Event"/>
|
||||
/// </summary>
|
||||
public int EventCount => this.eventCoroutines.Count;
|
||||
public int EventCount => this.eventCoroutines.Sum(c => c.Value.Count);
|
||||
|
||||
/// <summary>
|
||||
/// Starts the given coroutine, returning a <see cref="ActiveCoroutine"/> object for management.
|
||||
|
@ -66,7 +66,6 @@ namespace Coroutine {
|
|||
|
||||
/// <summary>
|
||||
/// Ticks this coroutine handler, causing all time-based <see cref="Wait"/>s to be ticked.
|
||||
/// Note that this method needs to be called even if only event-based coroutines are used.
|
||||
/// </summary>
|
||||
/// <param name="deltaSeconds">The amount of seconds that have passed since the last time this method was invoked</param>
|
||||
public void Tick(double deltaSeconds) {
|
||||
|
@ -74,7 +73,7 @@ namespace Coroutine {
|
|||
this.tickingCoroutines.RemoveAll(c => {
|
||||
if (c.Tick(deltaSeconds)) {
|
||||
return true;
|
||||
} else if (c.IsWaitingForEvent()) {
|
||||
} else if (c.IsWaitingForEvent) {
|
||||
this.outstandingCoroutines.Enqueue(c);
|
||||
return true;
|
||||
}
|
||||
|
@ -96,15 +95,19 @@ namespace Coroutine {
|
|||
/// </summary>
|
||||
/// <param name="evt">The event to raise</param>
|
||||
public void RaiseEvent(Event evt) {
|
||||
for (var i = 0; i < this.eventCoroutines.Count; i++) {
|
||||
if (this.eventCoroutinesToRemove.Contains(i))
|
||||
continue;
|
||||
var c = this.eventCoroutines[i];
|
||||
if (c.OnEvent(evt)) {
|
||||
this.eventCoroutinesToRemove.Add(i);
|
||||
} else if (!c.IsWaitingForEvent()) {
|
||||
this.outstandingCoroutines.Enqueue(c);
|
||||
this.eventCoroutinesToRemove.Add(i);
|
||||
this.MoveOutstandingCoroutines();
|
||||
var coroutines = this.GetEventCoroutines(evt, 0);
|
||||
if (coroutines != null) {
|
||||
for (var i = 0; i < coroutines.Count; i++) {
|
||||
var c = coroutines[i];
|
||||
if (this.eventCoroutinesToRemove.Contains(c))
|
||||
continue;
|
||||
if (c.OnEvent(evt)) {
|
||||
this.eventCoroutinesToRemove.Enqueue(c);
|
||||
} else if (!c.IsWaitingForEvent) {
|
||||
this.outstandingCoroutines.Enqueue(c);
|
||||
this.eventCoroutinesToRemove.Enqueue(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -114,22 +117,32 @@ namespace Coroutine {
|
|||
/// </summary>
|
||||
/// <returns>All active coroutines</returns>
|
||||
public IEnumerable<ActiveCoroutine> GetActiveCoroutines() {
|
||||
return this.tickingCoroutines.Concat(this.eventCoroutines);
|
||||
return this.tickingCoroutines.Concat(this.eventCoroutines.Values.SelectMany(c => c));
|
||||
}
|
||||
|
||||
private void MoveOutstandingCoroutines() {
|
||||
var i = 0;
|
||||
this.eventCoroutines.RemoveAll(c => this.eventCoroutinesToRemove.Contains(i++));
|
||||
this.eventCoroutinesToRemove.Clear();
|
||||
|
||||
while (this.eventCoroutinesToRemove.Count > 0) {
|
||||
var c = this.eventCoroutinesToRemove.Peek();
|
||||
this.GetEventCoroutines(c.Event, 0).Remove(c);
|
||||
this.eventCoroutinesToRemove.Dequeue();
|
||||
}
|
||||
while (this.outstandingCoroutines.Count > 0) {
|
||||
var coroutine = this.outstandingCoroutines.Dequeue();
|
||||
var list = coroutine.IsWaitingForEvent() ? this.eventCoroutines : this.tickingCoroutines;
|
||||
var coroutine = this.outstandingCoroutines.Peek();
|
||||
var list = coroutine.IsWaitingForEvent ? this.GetEventCoroutines(coroutine.Event, 1) : this.tickingCoroutines;
|
||||
var position = list.BinarySearch(coroutine);
|
||||
list.Insert(position < 0 ? ~position : position, coroutine);
|
||||
this.outstandingCoroutines.Dequeue();
|
||||
}
|
||||
}
|
||||
|
||||
private List<ActiveCoroutine> GetEventCoroutines(Event evt, int capacity) {
|
||||
if (!this.eventCoroutines.TryGetValue(evt, out var ret) && capacity > 0) {
|
||||
ret = new List<ActiveCoroutine>(capacity);
|
||||
this.eventCoroutines.Add(evt, ret);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static IEnumerator<Wait> InvokeLaterImpl(Wait wait, Action action) {
|
||||
yield return wait;
|
||||
action();
|
||||
|
|
|
@ -2,19 +2,19 @@ using System;
|
|||
|
||||
namespace Coroutine {
|
||||
/// <summary>
|
||||
/// Represents either an amount of time, or an <see cref="Event"/> that is being waited for by an <see cref="ActiveCoroutine"/>.
|
||||
/// Represents either an amount of time, or an <see cref="Coroutine.Event"/> that is being waited for by an <see cref="ActiveCoroutine"/>.
|
||||
/// </summary>
|
||||
public struct Wait {
|
||||
|
||||
private readonly Event evt;
|
||||
internal readonly Event Event;
|
||||
private double seconds;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new wait that waits for the given <see cref="Event"/>.
|
||||
/// Creates a new wait that waits for the given <see cref="Coroutine.Event"/>.
|
||||
/// </summary>
|
||||
/// <param name="evt">The event to wait for</param>
|
||||
public Wait(Event evt) {
|
||||
this.evt = evt;
|
||||
this.Event = evt;
|
||||
this.seconds = 0;
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ namespace Coroutine {
|
|||
/// <param name="seconds">The amount of seconds to wait for</param>
|
||||
public Wait(double seconds) {
|
||||
this.seconds = seconds;
|
||||
this.evt = null;
|
||||
this.Event = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -40,13 +40,5 @@ namespace Coroutine {
|
|||
return this.seconds <= 0;
|
||||
}
|
||||
|
||||
internal bool OnEvent(Event evt) {
|
||||
return Equals(evt, this.evt);
|
||||
}
|
||||
|
||||
internal bool IsWaitingForEvent() {
|
||||
return this.evt != null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ namespace Example {
|
|||
|
||||
CoroutineHandler.Start(EmptyCoroutine());
|
||||
|
||||
CoroutineHandler.InvokeLater(new Wait(10), () => {
|
||||
CoroutineHandler.InvokeLater(new Wait(5), () => {
|
||||
Console.WriteLine("Raising test event");
|
||||
CoroutineHandler.RaiseEvent(TestEvent);
|
||||
});
|
||||
|
|
|
@ -66,7 +66,5 @@ To actually cause the event to be raised, causing all currently waiting coroutin
|
|||
CoroutineHandler.RaiseEvent(TestEvent);
|
||||
```
|
||||
|
||||
Note that, since `Tick` is an important lifecycle method, it has to be [called continuously](#Setting-up-the-CoroutineHandler) even if only event-based coroutines are used.
|
||||
|
||||
## Additional Examples
|
||||
For additional examples, take a look at the [Example class](https://github.com/Ellpeck/Coroutine/blob/master/Example/Example.cs).
|
|
@ -21,7 +21,7 @@ namespace Tests {
|
|||
Assert.AreEqual(string.Empty, cr.Name, "Incorrect default name found");
|
||||
Assert.AreEqual(0, cr.Priority, "Default priority is not minimum");
|
||||
for (var i = 0; i < 5; i++)
|
||||
SimulateTime(1);
|
||||
CoroutineHandler.Tick(1);
|
||||
Assert.AreEqual(2, counter, "instruction after yield is not executed.");
|
||||
Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value.");
|
||||
Assert.AreEqual(false, cr.WasCanceled, "Incorrect IsCanceled value.");
|
||||
|
@ -49,7 +49,7 @@ namespace Tests {
|
|||
cr[0] = CoroutineHandler.Start(OnTimeTickNeverReturnYield());
|
||||
cr[1] = CoroutineHandler.Start(OnTimeTickYieldBreak());
|
||||
for (var i = 0; i < 5; i++)
|
||||
SimulateTime(1);
|
||||
CoroutineHandler.Tick(1);
|
||||
|
||||
Assert.AreEqual(3, counter, "Incorrect counter value.");
|
||||
for (var i = 0; i < cr.Length; i++) {
|
||||
|
@ -71,7 +71,7 @@ namespace Tests {
|
|||
|
||||
var cr = CoroutineHandler.Start(OnTimeTickYieldDefault());
|
||||
for (var i = 0; i < 5; i++)
|
||||
SimulateTime(1);
|
||||
CoroutineHandler.Tick(1);
|
||||
|
||||
Assert.AreEqual(2, counter, "Incorrect counter value.");
|
||||
Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value.");
|
||||
|
@ -97,7 +97,7 @@ namespace Tests {
|
|||
var cr = CoroutineHandler.Start(OnTimerTickInfinite());
|
||||
cr.OnFinished += SetCounterToUnreachableValue;
|
||||
for (var i = 0; i < 50; i++)
|
||||
SimulateTime(1);
|
||||
CoroutineHandler.Tick(1);
|
||||
|
||||
Assert.AreEqual(51, counter, "Incorrect counter value.");
|
||||
Assert.AreEqual(false, cr.IsFinished, "Incorrect IsFinished value.");
|
||||
|
@ -126,7 +126,7 @@ namespace Tests {
|
|||
|
||||
var cr = CoroutineHandler.Start(OnTimeTick());
|
||||
cr.OnFinished += SetCounterToUnreachableValue;
|
||||
SimulateTime(50);
|
||||
CoroutineHandler.Tick(50);
|
||||
Assert.AreEqual(-100, counter, "Incorrect counter value.");
|
||||
}
|
||||
|
||||
|
@ -173,22 +173,22 @@ namespace Tests {
|
|||
Assert.AreEqual(0, counterGrandParent, "Grand Parent counter is invalid at time 0.");
|
||||
Assert.AreEqual(0, counterParent, "Parent counter is invalid at time 0.");
|
||||
Assert.AreEqual(0, counterChild, "Child counter is invalid at time 0.");
|
||||
SimulateTime(1);
|
||||
CoroutineHandler.Tick(1);
|
||||
Assert.AreEqual(1, counterAlwaysRunning, "Always running counter is invalid at time 1.");
|
||||
Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 1.");
|
||||
Assert.AreEqual(0, counterParent, "Parent counter is invalid at time 1.");
|
||||
Assert.AreEqual(0, counterChild, "Child counter is invalid at time 1.");
|
||||
SimulateTime(1);
|
||||
CoroutineHandler.Tick(1);
|
||||
Assert.AreEqual(2, counterAlwaysRunning, "Always running counter is invalid at time 2.");
|
||||
Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 2.");
|
||||
Assert.AreEqual(1, counterParent, "Parent counter is invalid at time 2.");
|
||||
Assert.AreEqual(0, counterChild, "Child counter is invalid at time 2.");
|
||||
SimulateTime(1);
|
||||
CoroutineHandler.Tick(1);
|
||||
Assert.AreEqual(3, counterAlwaysRunning, "Always running counter is invalid at time 3.");
|
||||
Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 3.");
|
||||
Assert.AreEqual(1, counterParent, "Parent counter is invalid at time 3.");
|
||||
Assert.AreEqual(1, counterChild, "Child counter is invalid at time 3.");
|
||||
SimulateTime(1);
|
||||
CoroutineHandler.Tick(1);
|
||||
Assert.AreEqual(4, counterAlwaysRunning, "Always running counter is invalid at time 4.");
|
||||
Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 4.");
|
||||
Assert.AreEqual(1, counterParent, "Parent counter is invalid at time 4.");
|
||||
|
@ -243,7 +243,7 @@ namespace Tests {
|
|||
CoroutineHandler.Start(ShouldExecuteAfter());
|
||||
CoroutineHandler.Start(ShouldExecuteBefore0(), priority: highPriority);
|
||||
CoroutineHandler.Start(ShouldExecuteFinally(), priority: -1);
|
||||
SimulateTime(10);
|
||||
CoroutineHandler.Tick(10);
|
||||
Assert.AreEqual(1, counterShouldExecuteAfter, $"ShouldExecuteAfter counter {counterShouldExecuteAfter} is invalid.");
|
||||
Assert.AreEqual(1, counterShouldExecuteFinally, $"ShouldExecuteFinally counter {counterShouldExecuteFinally} is invalid.");
|
||||
}
|
||||
|
@ -270,10 +270,10 @@ namespace Tests {
|
|||
|
||||
CoroutineHandler.Start(IncrementCounter0Ever10Seconds());
|
||||
CoroutineHandler.Start(IncrementCounter1Every5Seconds());
|
||||
SimulateTime(3);
|
||||
CoroutineHandler.Tick(3);
|
||||
Assert.AreEqual(0, counter0, "Incorrect counter0 value after 3 seconds.");
|
||||
Assert.AreEqual(0, counter1, "Incorrect counter1 value after 3 seconds.");
|
||||
SimulateTime(3);
|
||||
CoroutineHandler.Tick(3);
|
||||
Assert.AreEqual(0, counter0, "Incorrect counter0 value after 6 seconds.");
|
||||
Assert.AreEqual(1, counter1, "Incorrect counter1 value after 6 seconds.");
|
||||
|
||||
|
@ -281,7 +281,7 @@ namespace Tests {
|
|||
// increments 5 seconds after last yield. not 5 seconds since start.
|
||||
// So the when we send 3 seconds in the last SimulateTime,
|
||||
// the 3rd second was technically ignored.
|
||||
SimulateTime(5);
|
||||
CoroutineHandler.Tick(5);
|
||||
Assert.AreEqual(1, counter0, "Incorrect counter0 value after 10 seconds.");
|
||||
Assert.AreEqual(2, counter1, "Incorrect counter1 value after next 5 seconds.");
|
||||
}
|
||||
|
@ -293,9 +293,9 @@ namespace Tests {
|
|||
counter++;
|
||||
}, "Bird");
|
||||
|
||||
SimulateTime(5);
|
||||
CoroutineHandler.Tick(5);
|
||||
Assert.AreEqual(0, counter, "Incorrect counter value after 5 seconds.");
|
||||
SimulateTime(5);
|
||||
CoroutineHandler.Tick(5);
|
||||
Assert.AreEqual(1, counter, "Incorrect counter value after 10 seconds.");
|
||||
Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value.");
|
||||
Assert.AreEqual(false, cr.WasCanceled, "Incorrect IsCanceled value.");
|
||||
|
@ -313,7 +313,7 @@ namespace Tests {
|
|||
|
||||
var cr = CoroutineHandler.Start(CoroutineTakesMax500Ms());
|
||||
for (var i = 0; i < 5; i++)
|
||||
SimulateTime(50);
|
||||
CoroutineHandler.Tick(50);
|
||||
|
||||
const int expected1 = 350;
|
||||
const float errorbar1 = 5 / 100f * expected1;
|
||||
|
@ -328,9 +328,5 @@ namespace Tests {
|
|||
Assert.IsTrue(gTc && lTd, $"Maximum Move Next Time {cr.MaxMoveNextTime.Milliseconds} is invalid.");
|
||||
}
|
||||
|
||||
private static void SimulateTime(double totalSeconds) {
|
||||
CoroutineHandler.Tick(totalSeconds);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue