using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Unity.IL2CPP.CompilerServices; using UnityEngine; namespace AlicizaX.Timer.Runtime { public delegate void TimerHandlerNoArgs(); internal delegate void TimerGenericInvoker(object handler, object arg); internal static class TimerGenericInvokerCache where T : class { public static readonly TimerGenericInvoker Invoke = InvokeGeneric; [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void InvokeGeneric(object handler, object arg) { ((Action)handler).Invoke((T)arg); } } [Il2CppSetOption(Option.NullChecks, false)] [Il2CppSetOption(Option.ArrayBoundsChecks, false)] [StructLayout(LayoutKind.Sequential, Pack = 4)] internal struct TimerInfo { public int TimerId; public int Version; public int QueueIndex; public int ActiveIndex; public float TriggerTime; public float Duration; public float RemainingTime; public TimerHandlerNoArgs NoArgsHandler; public TimerGenericInvoker GenericInvoker; public object GenericHandler; public object GenericArg; public bool IsLoop; public bool IsRunning; public bool IsUnscaled; public bool IsActive; public byte HandlerType; #if UNITY_EDITOR public float CreationTime; #endif [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Clear() { TimerId = 0; QueueIndex = -1; ActiveIndex = -1; TriggerTime = 0f; Duration = 0f; RemainingTime = 0f; NoArgsHandler = null; GenericInvoker = null; GenericHandler = null; GenericArg = null; IsLoop = false; IsRunning = false; IsUnscaled = false; IsActive = false; HandlerType = 0; #if UNITY_EDITOR CreationTime = 0f; #endif } } [UnityEngine.Scripting.Preserve] [Il2CppSetOption(Option.NullChecks, false)] [Il2CppSetOption(Option.ArrayBoundsChecks, false)] internal sealed class TimerService : ServiceBase, ITimerService, IServiceTickable, ITimerServiceDebugView { private const int MAX_CAPACITY = 256; private const int HANDLE_INDEX_BITS = 9; private const int HANDLE_INDEX_MASK = MAX_CAPACITY - 1; private const int HANDLE_VERSION_MASK = 0x3FFFFF; private const float MINIMUM_DELAY = 0.0001f; private const byte HANDLER_NO_ARGS = 0; private const byte HANDLER_GENERIC = 1; #if UNITY_EDITOR private const float LEAK_DETECTION_THRESHOLD = 300f; #endif private readonly TimerInfo[] _timerPool; private readonly int[] _freeIndices; private readonly int[] _activeIndices; private readonly TimerQueue _scaledQueue; private readonly TimerQueue _unscaledQueue; private int _freeCount; private int _activeCount; private int _peakActiveCount; private sealed class TimerQueue { private readonly TimerService _owner; private readonly int[] _heap; private int _count; public TimerQueue(TimerService owner) { _owner = owner; _heap = new int[MAX_CAPACITY]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(int poolIndex) { ref TimerInfo timer = ref _owner._timerPool[poolIndex]; int index = _count++; _heap[index] = poolIndex; timer.QueueIndex = index; BubbleUp(index); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Remove(int poolIndex) { ref TimerInfo timer = ref _owner._timerPool[poolIndex]; int index = timer.QueueIndex; if ((uint)index >= (uint)_count) { timer.QueueIndex = -1; return; } int lastIndex = --_count; int lastPoolIndex = _heap[lastIndex]; timer.QueueIndex = -1; if (index == lastIndex) { return; } _heap[index] = lastPoolIndex; _owner._timerPool[lastPoolIndex].QueueIndex = index; if (!BubbleUp(index)) { BubbleDown(index); } } public void Advance(float currentTime) { while (_count > 0) { int poolIndex = _heap[0]; ref TimerInfo timer = ref _owner._timerPool[poolIndex]; if (!timer.IsActive || !timer.IsRunning) { RemoveRoot(); continue; } if (timer.TriggerTime > currentTime) { break; } RemoveRoot(); _owner.ProcessDueTimer(poolIndex, currentTime); } } public void Clear() { while (_count > 0) { int poolIndex = _heap[--_count]; _owner._timerPool[poolIndex].QueueIndex = -1; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RemoveRoot() { int rootPoolIndex = _heap[0]; int lastIndex = --_count; _owner._timerPool[rootPoolIndex].QueueIndex = -1; if (lastIndex <= 0) { return; } int lastPoolIndex = _heap[lastIndex]; _heap[0] = lastPoolIndex; _owner._timerPool[lastPoolIndex].QueueIndex = 0; BubbleDown(0); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool BubbleUp(int index) { bool moved = false; while (index > 0) { int parent = (index - 1) >> 1; if (!Less(_heap[index], _heap[parent])) { break; } Swap(index, parent); index = parent; moved = true; } return moved; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void BubbleDown(int index) { while (true) { int left = (index << 1) + 1; if (left >= _count) { return; } int right = left + 1; int smallest = left; if (right < _count && Less(_heap[right], _heap[left])) { smallest = right; } if (!Less(_heap[smallest], _heap[index])) { return; } Swap(index, smallest); index = smallest; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool Less(int leftPoolIndex, int rightPoolIndex) { ref TimerInfo left = ref _owner._timerPool[leftPoolIndex]; ref TimerInfo right = ref _owner._timerPool[rightPoolIndex]; if (left.TriggerTime < right.TriggerTime) { return true; } if (left.TriggerTime > right.TriggerTime) { return false; } return left.TimerId < right.TimerId; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Swap(int leftIndex, int rightIndex) { int leftPoolIndex = _heap[leftIndex]; int rightPoolIndex = _heap[rightIndex]; _heap[leftIndex] = rightPoolIndex; _heap[rightIndex] = leftPoolIndex; _owner._timerPool[leftPoolIndex].QueueIndex = rightIndex; _owner._timerPool[rightPoolIndex].QueueIndex = leftIndex; } } public TimerService() { _timerPool = new TimerInfo[MAX_CAPACITY]; _freeIndices = new int[MAX_CAPACITY]; _activeIndices = new int[MAX_CAPACITY]; _scaledQueue = new TimerQueue(this); _unscaledQueue = new TimerQueue(this); _freeCount = MAX_CAPACITY; for (int i = 0; i < MAX_CAPACITY; i++) { _freeIndices[i] = i; _timerPool[i].QueueIndex = -1; _timerPool[i].ActiveIndex = -1; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public int AddTimer(TimerHandlerNoArgs callback, float time, bool isLoop = false, bool isUnscaled = false) { if (callback == null) { return 0; } int poolIndex = AcquireTimerIndex(); if (poolIndex < 0) { return 0; } InitializeTimer(poolIndex, time, isLoop, isUnscaled); ref TimerInfo timer = ref _timerPool[poolIndex]; timer.HandlerType = HANDLER_NO_ARGS; timer.NoArgsHandler = callback; AddToActiveSet(poolIndex); ScheduleTimer(poolIndex); return timer.TimerId; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public int AddTimer(Action callback, T arg, float time, bool isLoop = false, bool isUnscaled = false) where T : class { if (callback == null) { return 0; } int poolIndex = AcquireTimerIndex(); if (poolIndex < 0) { return 0; } InitializeTimer(poolIndex, time, isLoop, isUnscaled); ref TimerInfo timer = ref _timerPool[poolIndex]; timer.HandlerType = HANDLER_GENERIC; timer.GenericInvoker = TimerGenericInvokerCache.Invoke; timer.GenericHandler = callback; timer.GenericArg = arg; AddToActiveSet(poolIndex); ScheduleTimer(poolIndex); return timer.TimerId; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Stop(int timerId) { int poolIndex = GetPoolIndex(timerId); if (poolIndex < 0) { return; } ref TimerInfo timer = ref _timerPool[poolIndex]; if (!timer.IsRunning) { return; } if (timer.QueueIndex >= 0) { GetQueue(timer.IsUnscaled).Remove(poolIndex); float currentTime = GetCurrentTime(timer.IsUnscaled); float remainingTime = timer.TriggerTime - currentTime; timer.RemainingTime = remainingTime > MINIMUM_DELAY ? remainingTime : MINIMUM_DELAY; } else { timer.RemainingTime = timer.IsLoop ? timer.Duration : MINIMUM_DELAY; } timer.IsRunning = false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Resume(int timerId) { int poolIndex = GetPoolIndex(timerId); if (poolIndex < 0) { return; } ref TimerInfo timer = ref _timerPool[poolIndex]; if (timer.IsRunning) { return; } float delay = timer.RemainingTime > MINIMUM_DELAY ? timer.RemainingTime : MINIMUM_DELAY; timer.TriggerTime = GetCurrentTime(timer.IsUnscaled) + delay; timer.RemainingTime = 0f; timer.IsRunning = true; ScheduleTimer(poolIndex); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsRunning(int timerId) { int poolIndex = GetPoolIndex(timerId); return poolIndex >= 0 && _timerPool[poolIndex].IsRunning; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetLeftTime(int timerId) { int poolIndex = GetPoolIndex(timerId); if (poolIndex < 0) { return 0f; } ref TimerInfo timer = ref _timerPool[poolIndex]; if (!timer.IsRunning) { return timer.RemainingTime; } float leftTime = timer.TriggerTime - GetCurrentTime(timer.IsUnscaled); return leftTime > 0f ? leftTime : 0f; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Restart(int timerId) { int poolIndex = GetPoolIndex(timerId); if (poolIndex < 0) { return; } ref TimerInfo timer = ref _timerPool[poolIndex]; if (timer.QueueIndex >= 0) { GetQueue(timer.IsUnscaled).Remove(poolIndex); } timer.TriggerTime = GetCurrentTime(timer.IsUnscaled) + timer.Duration; timer.RemainingTime = 0f; timer.IsRunning = true; ScheduleTimer(poolIndex); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void RemoveTimer(int timerId) { int poolIndex = GetPoolIndex(timerId); if (poolIndex >= 0) { ReleaseTimer(poolIndex); } } void IServiceTickable.Tick(float deltaTime) { _scaledQueue.Advance(Time.time); _unscaledQueue.Advance(Time.unscaledTime); } protected override void OnInitialize() { } protected override void OnDestroyService() { RemoveAllTimers(); } public int Order => 0; void ITimerServiceDebugView.GetStatistics(out int activeCount, out int poolCapacity, out int peakActiveCount, out int freeCount) { activeCount = _activeCount; poolCapacity = MAX_CAPACITY; peakActiveCount = _peakActiveCount; freeCount = _freeCount; } int ITimerServiceDebugView.GetAllTimers(TimerDebugInfo[] results) { if (results == null || results.Length == 0) { return 0; } int count = _activeCount < results.Length ? _activeCount : results.Length; float currentTime = Time.time; float currentUnscaledTime = Time.unscaledTime; for (int i = 0; i < count; i++) { FillDebugInfo(_activeIndices[i], ref results[i], currentTime, currentUnscaledTime); } return count; } #if UNITY_EDITOR int ITimerServiceDebugView.GetStaleOneShotTimers(TimerDebugInfo[] results) { if (results == null || results.Length == 0) { return 0; } int count = 0; float realtimeSinceStartup = Time.realtimeSinceStartup; float currentTime = Time.time; float currentUnscaledTime = Time.unscaledTime; for (int i = 0; i < _activeCount && count < results.Length; i++) { int poolIndex = _activeIndices[i]; ref TimerInfo timer = ref _timerPool[poolIndex]; if (timer.IsLoop) { continue; } if (realtimeSinceStartup - timer.CreationTime <= LEAK_DETECTION_THRESHOLD) { continue; } FillDebugInfo(poolIndex, ref results[count], currentTime, currentUnscaledTime); count++; } return count; } #endif [MethodImpl(MethodImplOptions.AggressiveInlining)] private int AcquireTimerIndex() { if (_freeCount <= 0) { return -1; } return _freeIndices[--_freeCount]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void InitializeTimer(int poolIndex, float time, bool isLoop, bool isUnscaled) { ref TimerInfo timer = ref _timerPool[poolIndex]; float duration = NormalizeDelay(time); float currentTime = GetCurrentTime(isUnscaled); int version = NextVersion(timer.Version); timer.Version = version; timer.TimerId = ComposeTimerId(poolIndex, version); timer.TriggerTime = currentTime + duration; timer.Duration = duration; timer.RemainingTime = 0f; timer.NoArgsHandler = null; timer.GenericInvoker = null; timer.GenericHandler = null; timer.GenericArg = null; timer.IsLoop = isLoop; timer.IsRunning = true; timer.IsUnscaled = isUnscaled; timer.IsActive = true; timer.HandlerType = HANDLER_NO_ARGS; #if UNITY_EDITOR timer.CreationTime = Time.realtimeSinceStartup; #endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RemoveAllTimers() { while (_activeCount > 0) { ReleaseTimer(_activeIndices[_activeCount - 1]); } _scaledQueue.Clear(); _unscaledQueue.Clear(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ProcessDueTimer(int poolIndex, float currentTime) { ref TimerInfo timer = ref _timerPool[poolIndex]; if (!timer.IsActive) { return; } bool shouldRemoveAfterCallback = !timer.IsLoop; switch (timer.HandlerType) { case HANDLER_NO_ARGS: timer.NoArgsHandler(); break; case HANDLER_GENERIC: timer.GenericInvoker(timer.GenericHandler, timer.GenericArg); break; } if (!timer.IsActive) { return; } if (timer.QueueIndex >= 0) { return; } if (shouldRemoveAfterCallback) { ReleaseTimer(poolIndex); } else if (timer.IsRunning) { timer.TriggerTime = currentTime + timer.Duration; ScheduleTimer(poolIndex); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ScheduleTimer(int poolIndex) { ref TimerInfo timer = ref _timerPool[poolIndex]; if (!timer.IsActive || !timer.IsRunning) { return; } if (timer.QueueIndex >= 0) { return; } float currentTime = GetCurrentTime(timer.IsUnscaled); if (timer.TriggerTime <= currentTime) { timer.TriggerTime = currentTime + MINIMUM_DELAY; } GetQueue(timer.IsUnscaled).Add(poolIndex); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ReleaseTimer(int poolIndex) { ref TimerInfo timer = ref _timerPool[poolIndex]; if (!timer.IsActive) { return; } if (timer.QueueIndex >= 0) { GetQueue(timer.IsUnscaled).Remove(poolIndex); } RemoveFromActiveSet(poolIndex); timer.Clear(); _freeIndices[_freeCount++] = poolIndex; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddToActiveSet(int poolIndex) { int position = _activeCount; _activeIndices[position] = poolIndex; _timerPool[poolIndex].ActiveIndex = position; _activeCount = position + 1; if (_activeCount > _peakActiveCount) { _peakActiveCount = _activeCount; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RemoveFromActiveSet(int poolIndex) { ref TimerInfo timer = ref _timerPool[poolIndex]; int position = timer.ActiveIndex; if ((uint)position >= (uint)_activeCount) { return; } int lastPosition = --_activeCount; int lastPoolIndex = _activeIndices[lastPosition]; if (position != lastPosition) { _activeIndices[position] = lastPoolIndex; _timerPool[lastPoolIndex].ActiveIndex = position; } timer.ActiveIndex = -1; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private int GetPoolIndex(int timerId) { if (timerId <= 0) { return -1; } int poolIndex = (timerId & HANDLE_INDEX_MASK) - 1; if ((uint)poolIndex >= MAX_CAPACITY) { return -1; } ref TimerInfo timer = ref _timerPool[poolIndex]; return timer.IsActive && timer.TimerId == timerId ? poolIndex : -1; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int ComposeTimerId(int poolIndex, int version) { return (version << HANDLE_INDEX_BITS) | (poolIndex + 1); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int NextVersion(int version) { version++; if (version > HANDLE_VERSION_MASK) { version = 1; } return version; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static float NormalizeDelay(float time) { return time > MINIMUM_DELAY ? time : MINIMUM_DELAY; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static float GetCurrentTime(bool isUnscaled) { return isUnscaled ? Time.unscaledTime : Time.time; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private TimerQueue GetQueue(bool isUnscaled) { return isUnscaled ? _unscaledQueue : _scaledQueue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void FillDebugInfo(int poolIndex, ref TimerDebugInfo info, float currentTime, float currentUnscaledTime) { ref TimerInfo timer = ref _timerPool[poolIndex]; float leftTime; if (timer.IsRunning) { leftTime = timer.TriggerTime - (timer.IsUnscaled ? currentUnscaledTime : currentTime); if (leftTime < 0f) { leftTime = 0f; } } else { leftTime = timer.RemainingTime; } info.TimerId = timer.TimerId; info.LeftTime = leftTime; info.Duration = timer.Duration; info.IsLoop = timer.IsLoop; info.IsRunning = timer.IsRunning; info.IsUnscaled = timer.IsUnscaled; #if UNITY_EDITOR info.CreationTime = timer.CreationTime; #endif } } }