mirror of https://github.com/Cysharp/UniTask
commit
8a10f2191f
|
@ -43,6 +43,7 @@
|
||||||
..\UniTask\Assets\Plugins\UniTask\Runtime\EnumeratorAsyncExtensions.cs;
|
..\UniTask\Assets\Plugins\UniTask\Runtime\EnumeratorAsyncExtensions.cs;
|
||||||
..\UniTask\Assets\Plugins\UniTask\Runtime\TimeoutController.cs;
|
..\UniTask\Assets\Plugins\UniTask\Runtime\TimeoutController.cs;
|
||||||
..\UniTask\Assets\Plugins\UniTask\Runtime\PlayerLoopHelper.cs;
|
..\UniTask\Assets\Plugins\UniTask\Runtime\PlayerLoopHelper.cs;
|
||||||
|
..\UniTask\Assets\Plugins\UniTask\Runtime\PlayerLoopTimer.cs;
|
||||||
..\UniTask\Assets\Plugins\UniTask\Runtime\UniTask.Delay.cs;
|
..\UniTask\Assets\Plugins\UniTask\Runtime\UniTask.Delay.cs;
|
||||||
..\UniTask\Assets\Plugins\UniTask\Runtime\UniTask.Run.cs;
|
..\UniTask\Assets\Plugins\UniTask\Runtime\UniTask.Run.cs;
|
||||||
..\UniTask\Assets\Plugins\UniTask\Runtime\UniTask.Bridge.cs;
|
..\UniTask\Assets\Plugins\UniTask\Runtime\UniTask.Bridge.cs;
|
||||||
|
|
|
@ -4,32 +4,29 @@ using System.Threading;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Cysharp.Threading.Tasks.Triggers;
|
using Cysharp.Threading.Tasks.Triggers;
|
||||||
using System;
|
using System;
|
||||||
|
using Cysharp.Threading.Tasks.Internal;
|
||||||
|
|
||||||
namespace Cysharp.Threading.Tasks
|
namespace Cysharp.Threading.Tasks
|
||||||
{
|
{
|
||||||
|
|
||||||
public static class CancellationTokenSourceExtensions
|
public static partial class CancellationTokenSourceExtensions
|
||||||
{
|
{
|
||||||
public static void CancelAfterSlim(this CancellationTokenSource cts, int millisecondsDelay, DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update)
|
readonly static Action<object> CancelCancellationTokenSourceStateDelegate = new Action<object>(CancelCancellationTokenSourceState);
|
||||||
|
|
||||||
|
static void CancelCancellationTokenSourceState(object state)
|
||||||
{
|
{
|
||||||
var delay = UniTask.Delay(millisecondsDelay, delayType, delayTiming, cts.Token);
|
var cts = (CancellationTokenSource)state;
|
||||||
CancelAfterCore(cts, delay).Forget();
|
cts.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void CancelAfterSlim(this CancellationTokenSource cts, TimeSpan delayTimeSpan, DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update)
|
public static IDisposable CancelAfterSlim(this CancellationTokenSource cts, int millisecondsDelay, DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update)
|
||||||
{
|
{
|
||||||
var delay = UniTask.Delay(delayTimeSpan, delayType, delayTiming, cts.Token);
|
return CancelAfterSlim(cts, TimeSpan.FromMilliseconds(millisecondsDelay), delayType, delayTiming);
|
||||||
CancelAfterCore(cts, delay).Forget();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async UniTaskVoid CancelAfterCore(CancellationTokenSource cts, UniTask delayTask)
|
public static IDisposable CancelAfterSlim(this CancellationTokenSource cts, TimeSpan delayTimeSpan, DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update)
|
||||||
{
|
{
|
||||||
var alreadyCanceled = await delayTask.SuppressCancellationThrow();
|
return PlayerLoopTimer.StartNew(delayTimeSpan, false, delayType, delayTiming, cts.Token, CancelCancellationTokenSourceStateDelegate, cts);
|
||||||
if (!alreadyCanceled)
|
|
||||||
{
|
|
||||||
cts.Cancel();
|
|
||||||
cts.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RegisterRaiseCancelOnDestroy(this CancellationTokenSource cts, Component component)
|
public static void RegisterRaiseCancelOnDestroy(this CancellationTokenSource cts, Component component)
|
||||||
|
@ -40,11 +37,7 @@ namespace Cysharp.Threading.Tasks
|
||||||
public static void RegisterRaiseCancelOnDestroy(this CancellationTokenSource cts, GameObject gameObject)
|
public static void RegisterRaiseCancelOnDestroy(this CancellationTokenSource cts, GameObject gameObject)
|
||||||
{
|
{
|
||||||
var trigger = gameObject.GetAsyncDestroyTrigger();
|
var trigger = gameObject.GetAsyncDestroyTrigger();
|
||||||
trigger.CancellationToken.RegisterWithoutCaptureExecutionContext(state =>
|
trigger.CancellationToken.RegisterWithoutCaptureExecutionContext(CancelCancellationTokenSourceStateDelegate, cts);
|
||||||
{
|
|
||||||
var cts2 = (CancellationTokenSource)state;
|
|
||||||
cts2.Cancel();
|
|
||||||
}, cts);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,262 @@
|
||||||
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
|
||||||
|
using System.Threading;
|
||||||
|
using System;
|
||||||
|
using Cysharp.Threading.Tasks.Internal;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Cysharp.Threading.Tasks
|
||||||
|
{
|
||||||
|
public abstract class PlayerLoopTimer : IDisposable, IPlayerLoopItem
|
||||||
|
{
|
||||||
|
readonly CancellationToken cancellationToken;
|
||||||
|
readonly Action<object> timerCallback;
|
||||||
|
readonly object state;
|
||||||
|
readonly PlayerLoopTiming playerLoopTiming;
|
||||||
|
readonly bool periodic;
|
||||||
|
|
||||||
|
bool isRunning;
|
||||||
|
bool tryStop;
|
||||||
|
bool isDisposed;
|
||||||
|
|
||||||
|
protected PlayerLoopTimer(bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
|
||||||
|
{
|
||||||
|
this.periodic = periodic;
|
||||||
|
this.playerLoopTiming = playerLoopTiming;
|
||||||
|
this.cancellationToken = cancellationToken;
|
||||||
|
this.timerCallback = timerCallback;
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlayerLoopTimer Create(TimeSpan interval, bool periodic, DelayType delayType, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
|
||||||
|
{
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
// force use Realtime.
|
||||||
|
if (PlayerLoopHelper.IsMainThread && !UnityEditor.EditorApplication.isPlaying)
|
||||||
|
{
|
||||||
|
delayType = DelayType.Realtime;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
switch (delayType)
|
||||||
|
{
|
||||||
|
case DelayType.UnscaledDeltaTime:
|
||||||
|
return new IgnoreTimeScalePlayerLoopTimer(interval, periodic, playerLoopTiming, cancellationToken, timerCallback, state);
|
||||||
|
case DelayType.Realtime:
|
||||||
|
return new RealtimePlayerLoopTimer(interval, periodic, playerLoopTiming, cancellationToken, timerCallback, state);
|
||||||
|
case DelayType.DeltaTime:
|
||||||
|
default:
|
||||||
|
return new DeltaTimePlayerLoopTimer(interval, periodic, playerLoopTiming, cancellationToken, timerCallback, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlayerLoopTimer StartNew(TimeSpan interval, bool periodic, DelayType delayType, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
|
||||||
|
{
|
||||||
|
var timer = Create(interval, periodic, delayType, playerLoopTiming, cancellationToken, timerCallback, state);
|
||||||
|
timer.Restart();
|
||||||
|
return timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restart(Reset and Start) timer.
|
||||||
|
/// </summary>
|
||||||
|
public void Restart()
|
||||||
|
{
|
||||||
|
if (isDisposed) throw new ObjectDisposedException(null);
|
||||||
|
|
||||||
|
ResetCore(null); // init state
|
||||||
|
if (!isRunning)
|
||||||
|
{
|
||||||
|
isRunning = true;
|
||||||
|
PlayerLoopHelper.AddAction(playerLoopTiming, this);
|
||||||
|
}
|
||||||
|
tryStop = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restart(Reset and Start) and change interval.
|
||||||
|
/// </summary>
|
||||||
|
public void Restart(TimeSpan interval)
|
||||||
|
{
|
||||||
|
if (isDisposed) throw new ObjectDisposedException(null);
|
||||||
|
|
||||||
|
ResetCore(interval); // init state
|
||||||
|
if (!isRunning)
|
||||||
|
{
|
||||||
|
isRunning = true;
|
||||||
|
PlayerLoopHelper.AddAction(playerLoopTiming, this);
|
||||||
|
}
|
||||||
|
tryStop = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop timer.
|
||||||
|
/// </summary>
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
tryStop = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void ResetCore(TimeSpan? newInterval);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
isDisposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IPlayerLoopItem.MoveNext()
|
||||||
|
{
|
||||||
|
if (isDisposed)
|
||||||
|
{
|
||||||
|
isRunning = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (tryStop)
|
||||||
|
{
|
||||||
|
isRunning = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
isRunning = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MoveNextCore())
|
||||||
|
{
|
||||||
|
timerCallback(state);
|
||||||
|
|
||||||
|
if (periodic)
|
||||||
|
{
|
||||||
|
ResetCore(null);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
isRunning = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract bool MoveNextCore();
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class DeltaTimePlayerLoopTimer : PlayerLoopTimer
|
||||||
|
{
|
||||||
|
int initialFrame;
|
||||||
|
float elapsed;
|
||||||
|
float interval;
|
||||||
|
|
||||||
|
public DeltaTimePlayerLoopTimer(TimeSpan interval, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
|
||||||
|
: base(periodic, playerLoopTiming, cancellationToken, timerCallback, state)
|
||||||
|
{
|
||||||
|
ResetCore(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool MoveNextCore()
|
||||||
|
{
|
||||||
|
if (elapsed == 0.0f)
|
||||||
|
{
|
||||||
|
if (initialFrame == Time.frameCount)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed += Time.deltaTime;
|
||||||
|
if (elapsed >= interval)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ResetCore(TimeSpan? interval)
|
||||||
|
{
|
||||||
|
this.elapsed = 0.0f;
|
||||||
|
this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1;
|
||||||
|
if (interval != null)
|
||||||
|
{
|
||||||
|
this.interval = (float)interval.Value.TotalSeconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class IgnoreTimeScalePlayerLoopTimer : PlayerLoopTimer
|
||||||
|
{
|
||||||
|
int initialFrame;
|
||||||
|
float elapsed;
|
||||||
|
float interval;
|
||||||
|
|
||||||
|
public IgnoreTimeScalePlayerLoopTimer(TimeSpan interval, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
|
||||||
|
: base(periodic, playerLoopTiming, cancellationToken, timerCallback, state)
|
||||||
|
{
|
||||||
|
ResetCore(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool MoveNextCore()
|
||||||
|
{
|
||||||
|
if (elapsed == 0.0f)
|
||||||
|
{
|
||||||
|
if (initialFrame == Time.frameCount)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed += Time.unscaledDeltaTime;
|
||||||
|
if (elapsed >= interval)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ResetCore(TimeSpan? interval)
|
||||||
|
{
|
||||||
|
this.elapsed = 0.0f;
|
||||||
|
this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1;
|
||||||
|
if (interval != null)
|
||||||
|
{
|
||||||
|
this.interval = (float)interval.Value.TotalSeconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class RealtimePlayerLoopTimer : PlayerLoopTimer
|
||||||
|
{
|
||||||
|
ValueStopwatch stopwatch;
|
||||||
|
long intervalTicks;
|
||||||
|
|
||||||
|
public RealtimePlayerLoopTimer(TimeSpan interval, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
|
||||||
|
: base(periodic, playerLoopTiming, cancellationToken, timerCallback, state)
|
||||||
|
{
|
||||||
|
ResetCore(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool MoveNextCore()
|
||||||
|
{
|
||||||
|
if (stopwatch.ElapsedTicks >= intervalTicks)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ResetCore(TimeSpan? interval)
|
||||||
|
{
|
||||||
|
this.stopwatch = ValueStopwatch.StartNew();
|
||||||
|
if (interval != null)
|
||||||
|
{
|
||||||
|
this.intervalTicks = interval.Value.Ticks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 57095a17fdca7ee4380450910afc7f26
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
|
@ -1,8 +1,7 @@
|
||||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
|
||||||
using System.Threading;
|
|
||||||
using System;
|
using System;
|
||||||
using Cysharp.Threading.Tasks.Internal;
|
using System.Threading;
|
||||||
|
|
||||||
namespace Cysharp.Threading.Tasks
|
namespace Cysharp.Threading.Tasks
|
||||||
{
|
{
|
||||||
|
@ -14,26 +13,44 @@ namespace Cysharp.Threading.Tasks
|
||||||
|
|
||||||
public sealed class TimeoutController : IDisposable
|
public sealed class TimeoutController : IDisposable
|
||||||
{
|
{
|
||||||
|
readonly static Action<object> CancelCancellationTokenSourceStateDelegate = new Action<object>(CancelCancellationTokenSourceState);
|
||||||
|
|
||||||
|
static void CancelCancellationTokenSourceState(object state)
|
||||||
|
{
|
||||||
|
var cts = (CancellationTokenSource)state;
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
CancellationTokenSource timeoutSource;
|
CancellationTokenSource timeoutSource;
|
||||||
CancellationTokenSource linkedSource;
|
CancellationTokenSource linkedSource;
|
||||||
StoppableDelayRealtimePromise timeoutDelay;
|
PlayerLoopTimer timer;
|
||||||
|
bool isDisposed;
|
||||||
|
|
||||||
|
readonly DelayType delayType;
|
||||||
|
readonly PlayerLoopTiming delayTiming;
|
||||||
readonly CancellationTokenSource originalLinkCancellationTokenSource;
|
readonly CancellationTokenSource originalLinkCancellationTokenSource;
|
||||||
|
|
||||||
public TimeoutController()
|
public TimeoutController(DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update)
|
||||||
{
|
{
|
||||||
this.timeoutSource = new CancellationTokenSource();
|
this.timeoutSource = new CancellationTokenSource();
|
||||||
this.originalLinkCancellationTokenSource = null;
|
this.originalLinkCancellationTokenSource = null;
|
||||||
this.linkedSource = null;
|
this.linkedSource = null;
|
||||||
this.timeoutDelay = null;
|
this.delayType = delayType;
|
||||||
|
this.delayTiming = delayTiming;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TimeoutController(CancellationTokenSource linkCancellationTokenSource)
|
public TimeoutController(CancellationTokenSource linkCancellationTokenSource, DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update)
|
||||||
{
|
{
|
||||||
this.timeoutSource = new CancellationTokenSource();
|
this.timeoutSource = new CancellationTokenSource();
|
||||||
this.originalLinkCancellationTokenSource = linkCancellationTokenSource;
|
this.originalLinkCancellationTokenSource = linkCancellationTokenSource;
|
||||||
this.linkedSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, linkCancellationTokenSource.Token);
|
this.linkedSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, linkCancellationTokenSource.Token);
|
||||||
this.timeoutDelay = null;
|
this.delayType = delayType;
|
||||||
|
this.delayTiming = delayTiming;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CancellationToken Timeout(int millisecondsTimeout)
|
||||||
|
{
|
||||||
|
return Timeout(TimeSpan.FromMilliseconds(millisecondsTimeout));
|
||||||
}
|
}
|
||||||
|
|
||||||
public CancellationToken Timeout(TimeSpan timeout)
|
public CancellationToken Timeout(TimeSpan timeout)
|
||||||
|
@ -43,6 +60,7 @@ namespace Cysharp.Threading.Tasks
|
||||||
return originalLinkCancellationTokenSource.Token;
|
return originalLinkCancellationTokenSource.Token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Timeouted, create new source and timer.
|
||||||
if (timeoutSource.IsCancellationRequested)
|
if (timeoutSource.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
timeoutSource.Dispose();
|
timeoutSource.Dispose();
|
||||||
|
@ -53,18 +71,25 @@ namespace Cysharp.Threading.Tasks
|
||||||
this.linkedSource.Dispose();
|
this.linkedSource.Dispose();
|
||||||
this.linkedSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, originalLinkCancellationTokenSource.Token);
|
this.linkedSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, originalLinkCancellationTokenSource.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timer?.Dispose();
|
||||||
|
timer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeoutDelay == null)
|
var useSource = (linkedSource != null) ? linkedSource : timeoutSource;
|
||||||
|
var token = useSource.Token;
|
||||||
|
if (timer == null)
|
||||||
{
|
{
|
||||||
RunDelayAsync(timeout).Forget(); // timeoutDelay = ... in RunDelayAsync(immediately, before await)
|
// Timer complete => timeoutSource.Cancel() -> linkedSource will be canceled.
|
||||||
|
// (linked)token is canceled => stop timer
|
||||||
|
timer = PlayerLoopTimer.StartNew(timeout, false, delayType, delayTiming, token, CancelCancellationTokenSourceStateDelegate, timeoutSource);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
timeoutDelay.RestartStopwatch(); // already running RunDelayAsync
|
timer.Restart(timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (linkedSource != null) ? linkedSource.Token : timeoutSource.Token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsTimeout()
|
public bool IsTimeout()
|
||||||
|
@ -74,184 +99,30 @@ namespace Cysharp.Threading.Tasks
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
if (timeoutDelay != null)
|
timer.Stop();
|
||||||
{
|
|
||||||
timeoutDelay.Stop(); // stop delay, will finish RunDelayAsync
|
|
||||||
timeoutDelay = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async UniTaskVoid RunDelayAsync(TimeSpan timeout)
|
|
||||||
{
|
|
||||||
timeoutDelay = StoppableDelayRealtimePromise.Create(timeout, PlayerLoopTiming.Update, (linkedSource == null) ? CancellationToken.None : linkedSource.Token, out var version);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var reason = await new UniTask<DelayResult>(timeoutDelay, version);
|
|
||||||
if (reason == DelayResult.DelayCompleted)
|
|
||||||
{
|
|
||||||
// UnityEngine.Debug.Log("DEBUG:Timeout Complete, try to call timeoutSource.Cancel");
|
|
||||||
timeoutSource.Cancel();
|
|
||||||
}
|
|
||||||
else if (reason == DelayResult.LinkedTokenCanceled)
|
|
||||||
{
|
|
||||||
// UnityEngine.Debug.Log("DEBUG:LinkedSource IsCancellationRequested");
|
|
||||||
}
|
|
||||||
else if (reason == DelayResult.ExternalStopped)
|
|
||||||
{
|
|
||||||
// Reset(Promise.Stop) called, do nothing.
|
|
||||||
// UnityEngine.Debug.Log("DEBUG:Reset called");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
timeoutDelay = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (timeoutDelay != null)
|
if (isDisposed) return;
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
timeoutDelay.Stop();
|
// stop timer.
|
||||||
}
|
timer.Dispose();
|
||||||
timeoutSource.Dispose();
|
|
||||||
if (linkedSource != null)
|
|
||||||
{
|
|
||||||
linkedSource.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DelayResult
|
// cancel and dispose.
|
||||||
{
|
timeoutSource.Cancel();
|
||||||
LinkedTokenCanceled,
|
timeoutSource.Dispose();
|
||||||
ExternalStopped,
|
if (linkedSource != null)
|
||||||
DelayCompleted, // as Timeout.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop + SuppressCancellationThrow.
|
|
||||||
sealed class StoppableDelayRealtimePromise : IUniTaskSource<DelayResult>, IPlayerLoopItem, ITaskPoolNode<StoppableDelayRealtimePromise>
|
|
||||||
{
|
|
||||||
static OperationCanceledException ExterenalStopException = new OperationCanceledException();
|
|
||||||
|
|
||||||
static TaskPool<StoppableDelayRealtimePromise> pool;
|
|
||||||
StoppableDelayRealtimePromise nextNode;
|
|
||||||
public ref StoppableDelayRealtimePromise NextNode => ref nextNode;
|
|
||||||
|
|
||||||
static StoppableDelayRealtimePromise()
|
|
||||||
{
|
|
||||||
TaskPool.RegisterSizeGetter(typeof(StoppableDelayRealtimePromise), () => pool.Size);
|
|
||||||
}
|
|
||||||
|
|
||||||
long delayTimeSpanTicks;
|
|
||||||
ValueStopwatch stopwatch;
|
|
||||||
CancellationToken cancellationToken;
|
|
||||||
bool externalStop;
|
|
||||||
|
|
||||||
UniTaskCompletionSourceCore<DelayResult> core;
|
|
||||||
|
|
||||||
StoppableDelayRealtimePromise()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public static StoppableDelayRealtimePromise Create(TimeSpan delayTimeSpan, PlayerLoopTiming timing, CancellationToken cancellationToken, out short token)
|
|
||||||
{
|
|
||||||
if (!pool.TryPop(out var result))
|
|
||||||
{
|
{
|
||||||
result = new StoppableDelayRealtimePromise();
|
linkedSource.Cancel();
|
||||||
}
|
linkedSource.Dispose();
|
||||||
|
|
||||||
result.stopwatch = ValueStopwatch.StartNew();
|
|
||||||
result.delayTimeSpanTicks = delayTimeSpan.Ticks;
|
|
||||||
result.cancellationToken = cancellationToken;
|
|
||||||
result.externalStop = false;
|
|
||||||
|
|
||||||
TaskTracker.TrackActiveTask(result, 3);
|
|
||||||
|
|
||||||
PlayerLoopHelper.AddAction(timing, result);
|
|
||||||
|
|
||||||
token = result.core.Version;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Stop()
|
|
||||||
{
|
|
||||||
externalStop = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RestartStopwatch()
|
|
||||||
{
|
|
||||||
stopwatch = ValueStopwatch.StartNew();
|
|
||||||
}
|
|
||||||
|
|
||||||
public DelayResult GetResult(short token)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return core.GetResult(token);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
TryReturn();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
void IUniTaskSource.GetResult(short token)
|
|
||||||
{
|
{
|
||||||
GetResult(token);
|
isDisposed = true;
|
||||||
}
|
|
||||||
|
|
||||||
public UniTaskStatus GetStatus(short token)
|
|
||||||
{
|
|
||||||
return core.GetStatus(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
public UniTaskStatus UnsafeGetStatus()
|
|
||||||
{
|
|
||||||
return core.UnsafeGetStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnCompleted(Action<object> continuation, object state, short token)
|
|
||||||
{
|
|
||||||
core.OnCompleted(continuation, state, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool MoveNext()
|
|
||||||
{
|
|
||||||
if (cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
core.TrySetResult(DelayResult.LinkedTokenCanceled);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (externalStop)
|
|
||||||
{
|
|
||||||
core.TrySetResult(DelayResult.ExternalStopped);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stopwatch.IsInvalid)
|
|
||||||
{
|
|
||||||
core.TrySetResult(DelayResult.DelayCompleted);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stopwatch.ElapsedTicks >= delayTimeSpanTicks)
|
|
||||||
{
|
|
||||||
core.TrySetResult(DelayResult.DelayCompleted);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TryReturn()
|
|
||||||
{
|
|
||||||
TaskTracker.RemoveTracking(this);
|
|
||||||
core.Reset();
|
|
||||||
stopwatch = default;
|
|
||||||
cancellationToken = default;
|
|
||||||
externalStop = false;
|
|
||||||
return pool.TryPush(this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,179 @@
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using FluentAssertions;
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using UnityEngine.TestTools;
|
||||||
|
|
||||||
|
namespace Cysharp.Threading.TasksTests
|
||||||
|
{
|
||||||
|
public class PlayerLoopTimerTest
|
||||||
|
{
|
||||||
|
void Between(TimeSpan l, TimeSpan data, TimeSpan r)
|
||||||
|
{
|
||||||
|
NUnit.Framework.Assert.AreEqual(l < data, true, "{0} < {1} failed.", l, data);
|
||||||
|
NUnit.Framework.Assert.AreEqual(data < r, true, "{0} < {1} failed.", data, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator StandardTicks() => UniTask.ToCoroutine(async () =>
|
||||||
|
{
|
||||||
|
foreach (var delay in new[] { DelayType.DeltaTime, DelayType.Realtime, DelayType.UnscaledDeltaTime })
|
||||||
|
{
|
||||||
|
var raisedTimeout = new UniTaskCompletionSource();
|
||||||
|
PlayerLoopTimer.StartNew(TimeSpan.FromSeconds(1), false, delay, PlayerLoopTiming.Update, CancellationToken.None, _ =>
|
||||||
|
{
|
||||||
|
raisedTimeout.TrySetResult();
|
||||||
|
}, null);
|
||||||
|
|
||||||
|
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
await raisedTimeout.Task;
|
||||||
|
sw.Stop();
|
||||||
|
|
||||||
|
Between(TimeSpan.FromSeconds(0.9), sw.Elapsed, TimeSpan.FromSeconds(1.1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator Periodic() => UniTask.ToCoroutine(async () =>
|
||||||
|
{
|
||||||
|
var raisedTime = new List<DateTime>();
|
||||||
|
var count = 0;
|
||||||
|
var complete = new UniTaskCompletionSource();
|
||||||
|
|
||||||
|
PlayerLoopTimer timer = null;
|
||||||
|
timer = PlayerLoopTimer.StartNew(TimeSpan.FromSeconds(1), true, DelayType.DeltaTime, PlayerLoopTiming.Update, CancellationToken.None, _ =>
|
||||||
|
{
|
||||||
|
raisedTime.Add(DateTime.UtcNow);
|
||||||
|
count++;
|
||||||
|
if (count == 3)
|
||||||
|
{
|
||||||
|
complete.TrySetResult();
|
||||||
|
timer.Dispose();
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
|
|
||||||
|
var start = DateTime.UtcNow;
|
||||||
|
await complete.Task;
|
||||||
|
|
||||||
|
Between(TimeSpan.FromSeconds(0.9), raisedTime[0] - start, TimeSpan.FromSeconds(1.1));
|
||||||
|
Between(TimeSpan.FromSeconds(1.9), raisedTime[1] - start, TimeSpan.FromSeconds(2.1));
|
||||||
|
Between(TimeSpan.FromSeconds(2.9), raisedTime[2] - start, TimeSpan.FromSeconds(3.1));
|
||||||
|
});
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator CancelAfterSlimTest() => UniTask.ToCoroutine(async () =>
|
||||||
|
{
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
var complete = new UniTaskCompletionSource();
|
||||||
|
cts.Token.RegisterWithoutCaptureExecutionContext(() =>
|
||||||
|
{
|
||||||
|
complete.TrySetResult();
|
||||||
|
});
|
||||||
|
|
||||||
|
cts.CancelAfterSlim(TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
await complete.Task;
|
||||||
|
|
||||||
|
Between(TimeSpan.FromSeconds(0.9), sw.Elapsed, TimeSpan.FromSeconds(1.1));
|
||||||
|
});
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator CancelAfterSlimCancelTest() => UniTask.ToCoroutine(async () =>
|
||||||
|
{
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
var complete = new UniTaskCompletionSource();
|
||||||
|
cts.Token.RegisterWithoutCaptureExecutionContext(() =>
|
||||||
|
{
|
||||||
|
complete.TrySetResult();
|
||||||
|
});
|
||||||
|
|
||||||
|
var d = cts.CancelAfterSlim(TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
await UniTask.Delay(TimeSpan.FromMilliseconds(100));
|
||||||
|
d.Dispose();
|
||||||
|
|
||||||
|
await UniTask.Delay(TimeSpan.FromSeconds(2));
|
||||||
|
|
||||||
|
complete.Task.Status.Should().Be(UniTaskStatus.Pending);
|
||||||
|
});
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator TimeoutController() => UniTask.ToCoroutine(async () =>
|
||||||
|
{
|
||||||
|
var controller = new TimeoutController();
|
||||||
|
|
||||||
|
var token = controller.Timeout(TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
var complete = new UniTaskCompletionSource();
|
||||||
|
token.RegisterWithoutCaptureExecutionContext(() =>
|
||||||
|
{
|
||||||
|
complete.TrySetResult();
|
||||||
|
});
|
||||||
|
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
await complete.Task;
|
||||||
|
Between(TimeSpan.FromSeconds(0.9), sw.Elapsed, TimeSpan.FromSeconds(1.1));
|
||||||
|
|
||||||
|
controller.IsTimeout().Should().BeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator TimeoutReuse() => UniTask.ToCoroutine(async () =>
|
||||||
|
{
|
||||||
|
var controller = new TimeoutController(DelayType.DeltaTime);
|
||||||
|
|
||||||
|
var token = controller.Timeout(TimeSpan.FromSeconds(2));
|
||||||
|
|
||||||
|
var complete = new UniTaskCompletionSource();
|
||||||
|
token.RegisterWithoutCaptureExecutionContext(() =>
|
||||||
|
{
|
||||||
|
complete.TrySetResult(); // reuse, used same token?
|
||||||
|
});
|
||||||
|
|
||||||
|
await UniTask.Delay(TimeSpan.FromMilliseconds(100));
|
||||||
|
controller.Reset();
|
||||||
|
|
||||||
|
controller.IsTimeout().Should().BeFalse();
|
||||||
|
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
controller.Timeout(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
await complete.Task;
|
||||||
|
|
||||||
|
UnityEngine.Debug.Log(UnityEngine.Time.timeScale);
|
||||||
|
Between(TimeSpan.FromSeconds(4.9), sw.Elapsed, TimeSpan.FromSeconds(5.1));
|
||||||
|
|
||||||
|
controller.IsTimeout().Should().BeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator LinkedTokenTest() => UniTask.ToCoroutine(async () =>
|
||||||
|
{
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
var controller = new TimeoutController(cts);
|
||||||
|
var token = controller.Timeout(TimeSpan.FromSeconds(2));
|
||||||
|
|
||||||
|
await UniTask.DelayFrame(3);
|
||||||
|
|
||||||
|
cts.Cancel();
|
||||||
|
|
||||||
|
token.IsCancellationRequested.Should().BeTrue();
|
||||||
|
|
||||||
|
controller.Dispose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c0c49de697f829f44aa8709b4d1eff3e
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
Loading…
Reference in New Issue