├── CHANGELOG.md ├── CHANGELOG.md.meta ├── LICENSE.md ├── LICENSE.md.meta ├── README.md ├── README.md.meta ├── Runtime.meta ├── Runtime ├── Extensions.meta ├── Extensions │ ├── ListExtensions.cs │ └── ListExtensions.cs.meta ├── ImprovedTimers.asmdef ├── ImprovedTimers.asmdef.meta ├── PlayerLoopUtils.cs ├── PlayerLoopUtils.cs.meta ├── Timer.cs ├── Timer.cs.meta ├── TimerBootstrapper.cs ├── TimerBootstrapper.cs.meta ├── TimerManager.cs ├── TimerManager.cs.meta ├── Timers.meta └── Timers │ ├── CountdownTimer.cs │ ├── CountdownTimer.cs.meta │ ├── FrequencyTimer.cs │ ├── FrequencyTimer.cs.meta │ ├── StopwatchTimer.cs │ └── StopwatchTimer.cs.meta ├── package.json └── package.json.meta /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.2] - 2024-06-15 2 | ### Disable Logging 3 | - Removes the call to automatically list all Player Loop Systems during bootstrap 4 | 5 | ## [1.0.1] - 2024-06-03 6 | ### Efficiency Improvements 7 | - Improves Timer Manager Efficiency By Reducing Memory Allocation 8 | - Disposes All Timers When Clear Method Is Called 9 | 10 | ## [1.0.0] - 2024-05-26 11 | ### First Release 12 | - Inserts Timer Manager into Player Loop For Self Management 13 | - Includes Countdown, Frequency and Stopwatch Timers 14 | -------------------------------------------------------------------------------- /CHANGELOG.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6ff872c286a053e429aea5546f9b3c27 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | ### Anti-War Crimes Clause 24 | The code provided in this repository may not be used to commit, support, 25 | or facilitate war crimes, crimes against humanity, or any other acts of 26 | violence that violate international humanitarian laws. 27 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c086c04a97388484095585c3092f8543 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Improved Unity Timers 2 | ![PlayerLoop2](https://github.com/adammyhre/Unity-Improved-Timers/assets/38876398/faefe51b-3fef-42ea-8440-1a92cd5c5abc) 3 | 4 | An extensible Timer solution for Unity Game Development. Timers are self managing 5 | by injecting a Timer Manager class into Unity's Update loop. 6 | 7 | Create your own Timers by extending the Timer abstract class! 8 | 9 | ## Example Usage 10 | 11 | ```csharp 12 | CountdownTimer timer = new CountdownTimer(5f); 13 | 14 | void Start() { 15 | timer.OnTimerStart += () => Debug.Log("Timer started"); 16 | timer.OnTimerStop += () => Debug.Log("Timer stopped"); 17 | timer.Start(); 18 | 19 | timer.Pause(); 20 | timer.Resume(); 21 | 22 | timer.Reset(); 23 | timer.Reset(10f); 24 | 25 | Debug.Log(timer.IsRunning ? "Timer is running" : "Timer is not running"); 26 | Debug.Log(timer.IsFinished ? "Timer is finished" : "Timer is not finished"); 27 | 28 | timer.Stop(); 29 | } 30 | 31 | void Update() { 32 | Debug.Log(timer.CurrentTime); 33 | Debug.Log(timer.Progress); 34 | } 35 | 36 | void OnDestroy() { 37 | timer.Dispose(); 38 | } 39 | ``` 40 | 41 | ## Notes 42 | 43 | Several Timers are already included, but you can create any kind of Timer you need that can be 44 | adjusted every frame. The included Timers are: 45 | 46 | - CountdownTimer: Counts down from a specified time to zero. 47 | - FrequencyTimer: Ticks N times per second. 48 | - StopwatchTimer: Counts up from zero to infinity. 49 | 50 | Classes extending the Timer class must implement the `Tick` method to increment or decrement the Timer, 51 | and the `IsFinished` property which is a convenience for consumers. 52 | 53 | Call `Dispose` when you don't need a Timer anymore to ensure proper garbage collection. 54 | 55 | ## How to Install 56 | 57 | Simply download the library into your Unity project and access the utilities across your scripts or import it in Unity with 58 | the Unity Package Manager using this URL: 59 | 60 | `https://github.com/adammyhre/Unity-Improved-Timers.git` 61 | 62 | ### Add to Manifest 63 | 64 | Alternatively, you can add the following line to your project's `manifest.json` file. 65 | 66 | ``` 67 | "com.gitamend.improvedtimers": "https://github.com/adammyhre/Unity-Improved-Timers.git" 68 | ``` 69 | 70 | ## YouTube 71 | 72 | - [Improved Timers in Unity](https://youtu.be/ilvmOQtl57c) 73 | 74 | You can also check out my [YouTube channel](https://www.youtube.com/@git-amend?sub_confirmation=1) for more Unity content. 75 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d142f08123461aa4981cc4204f67140c 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 83ff5ff4d68fb774ba667b8e607588f6 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/Extensions.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bd9e6848496edaa40ba9f14f8a831452 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/Extensions/ListExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ImprovedTimers { 4 | public static class ListExtensions { 5 | public static void RefreshWith(this List list, IEnumerable items) { 6 | list.Clear(); 7 | list.AddRange(items); 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /Runtime/Extensions/ListExtensions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 05801ac37cc24cd5b394f9bc3edd8e6c 3 | timeCreated: 1717462608 -------------------------------------------------------------------------------- /Runtime/ImprovedTimers.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ImprovedTimers", 3 | "rootNamespace": "ImprovedTimers", 4 | "references": [], 5 | "includePlatforms": [], 6 | "excludePlatforms": [], 7 | "allowUnsafeCode": false, 8 | "overrideReferences": false, 9 | "precompiledReferences": [], 10 | "autoReferenced": true, 11 | "defineConstraints": [], 12 | "versionDefines": [], 13 | "noEngineReferences": false 14 | } -------------------------------------------------------------------------------- /Runtime/ImprovedTimers.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 91d419664d8c89c4b8871250445a9bf5 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime/PlayerLoopUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text; 3 | using UnityEngine; 4 | using UnityEngine.LowLevel; 5 | 6 | namespace ImprovedTimers { 7 | public static class PlayerLoopUtils { 8 | // Remove a system from the player loop 9 | public static void RemoveSystem(ref PlayerLoopSystem loop, in PlayerLoopSystem systemToRemove) { 10 | if (loop.subSystemList == null) return; 11 | 12 | var playerLoopSystemList = new List(loop.subSystemList); 13 | for (int i = 0; i < playerLoopSystemList.Count; ++i) { 14 | if (playerLoopSystemList[i].type == systemToRemove.type && playerLoopSystemList[i].updateDelegate == systemToRemove.updateDelegate) { 15 | playerLoopSystemList.RemoveAt(i); 16 | loop.subSystemList = playerLoopSystemList.ToArray(); 17 | return; 18 | } 19 | } 20 | 21 | HandleSubSystemLoopForRemoval(ref loop, systemToRemove); 22 | } 23 | 24 | static void HandleSubSystemLoopForRemoval(ref PlayerLoopSystem loop, PlayerLoopSystem systemToRemove) { 25 | if (loop.subSystemList == null) return; 26 | 27 | for (int i = 0; i < loop.subSystemList.Length; ++i) { 28 | RemoveSystem(ref loop.subSystemList[i], systemToRemove); 29 | } 30 | } 31 | 32 | // Insert a system into the player loop 33 | public static bool InsertSystem(ref PlayerLoopSystem loop, in PlayerLoopSystem systemToInsert, int index) { 34 | if (loop.type != typeof(T)) return HandleSubSystemLoop(ref loop, systemToInsert, index); 35 | 36 | var playerLoopSystemList = new List(); 37 | if (loop.subSystemList != null) playerLoopSystemList.AddRange(loop.subSystemList); 38 | playerLoopSystemList.Insert(index, systemToInsert); 39 | loop.subSystemList = playerLoopSystemList.ToArray(); 40 | return true; 41 | } 42 | 43 | static bool HandleSubSystemLoop(ref PlayerLoopSystem loop, in PlayerLoopSystem systemToInsert, int index) { 44 | if (loop.subSystemList == null) return false; 45 | 46 | for (int i = 0; i < loop.subSystemList.Length; ++i) { 47 | if (!InsertSystem(ref loop.subSystemList[i], in systemToInsert, index)) continue; 48 | return true; 49 | } 50 | 51 | return false; 52 | } 53 | 54 | public static void PrintPlayerLoop(PlayerLoopSystem loop) { 55 | StringBuilder sb = new StringBuilder(); 56 | sb.AppendLine("Unity Player Loop"); 57 | foreach (PlayerLoopSystem subSystem in loop.subSystemList) { 58 | PrintSubsystem(subSystem, sb, 0); 59 | } 60 | Debug.Log(sb.ToString()); 61 | } 62 | 63 | static void PrintSubsystem(PlayerLoopSystem system, StringBuilder sb, int level) { 64 | sb.Append(' ', level * 2).AppendLine(system.type.ToString()); 65 | if (system.subSystemList == null || system.subSystemList.Length == 0) return; 66 | 67 | foreach (PlayerLoopSystem subSystem in system.subSystemList) { 68 | PrintSubsystem(subSystem, sb, level + 1); 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /Runtime/PlayerLoopUtils.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: df76bc37dab34c7db8cb92ccf95a49e9 3 | timeCreated: 1716000103 -------------------------------------------------------------------------------- /Runtime/Timer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace ImprovedTimers { 5 | public abstract class Timer : IDisposable { 6 | public float CurrentTime { get; protected set; } 7 | public bool IsRunning { get; private set; } 8 | 9 | protected float initialTime; 10 | 11 | public float Progress => Mathf.Clamp(CurrentTime / initialTime, 0, 1); 12 | 13 | public Action OnTimerStart = delegate { }; 14 | public Action OnTimerStop = delegate { }; 15 | 16 | protected Timer(float value) { 17 | initialTime = value; 18 | } 19 | 20 | public void Start() { 21 | CurrentTime = initialTime; 22 | if (!IsRunning) { 23 | IsRunning = true; 24 | TimerManager.RegisterTimer(this); 25 | OnTimerStart.Invoke(); 26 | } 27 | } 28 | 29 | public void Stop() { 30 | if (IsRunning) { 31 | IsRunning = false; 32 | TimerManager.DeregisterTimer(this); 33 | OnTimerStop.Invoke(); 34 | } 35 | } 36 | 37 | public abstract void Tick(); 38 | public abstract bool IsFinished { get; } 39 | 40 | public void Resume() => IsRunning = true; 41 | public void Pause() => IsRunning = false; 42 | 43 | public virtual void Reset() => CurrentTime = initialTime; 44 | 45 | public virtual void Reset(float newTime) { 46 | initialTime = newTime; 47 | Reset(); 48 | } 49 | 50 | bool disposed; 51 | 52 | ~Timer() { 53 | Dispose(false); 54 | } 55 | 56 | // Call Dispose to ensure deregistration of the timer from the TimerManager 57 | // when the consumer is done with the timer or being destroyed 58 | public void Dispose() { 59 | Dispose(true); 60 | GC.SuppressFinalize(this); 61 | } 62 | 63 | protected virtual void Dispose(bool disposing) { 64 | if (disposed) return; 65 | 66 | if (disposing) { 67 | TimerManager.DeregisterTimer(this); 68 | } 69 | 70 | disposed = true; 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /Runtime/Timer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bbe44af075bd4e32aeba6b4d43424095 3 | timeCreated: 1716049567 -------------------------------------------------------------------------------- /Runtime/TimerBootstrapper.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | using UnityEngine.LowLevel; 4 | using UnityEngine.PlayerLoop; 5 | 6 | namespace ImprovedTimers { 7 | internal static class TimerBootstrapper { 8 | static PlayerLoopSystem timerSystem; 9 | 10 | [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)] 11 | internal static void Initialize() { 12 | PlayerLoopSystem currentPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); 13 | 14 | if (!InsertTimerManager(ref currentPlayerLoop, 0)) { 15 | Debug.LogWarning("Improved Timers not initialized, unable to register TimerManager into the Update loop."); 16 | return; 17 | } 18 | PlayerLoop.SetPlayerLoop(currentPlayerLoop); 19 | 20 | #if UNITY_EDITOR 21 | EditorApplication.playModeStateChanged -= OnPlayModeState; 22 | EditorApplication.playModeStateChanged += OnPlayModeState; 23 | 24 | static void OnPlayModeState(PlayModeStateChange state) { 25 | if (state == PlayModeStateChange.ExitingPlayMode) { 26 | PlayerLoopSystem currentPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); 27 | RemoveTimerManager(ref currentPlayerLoop); 28 | PlayerLoop.SetPlayerLoop(currentPlayerLoop); 29 | 30 | TimerManager.Clear(); 31 | } 32 | } 33 | #endif 34 | } 35 | 36 | static void RemoveTimerManager(ref PlayerLoopSystem loop) { 37 | PlayerLoopUtils.RemoveSystem(ref loop, in timerSystem); 38 | } 39 | 40 | static bool InsertTimerManager(ref PlayerLoopSystem loop, int index) { 41 | timerSystem = new PlayerLoopSystem() { 42 | type = typeof(TimerManager), 43 | updateDelegate = TimerManager.UpdateTimers, 44 | subSystemList = null 45 | }; 46 | return PlayerLoopUtils.InsertSystem(ref loop, in timerSystem, index); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Runtime/TimerBootstrapper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e7a31777110e463cae492dea5c97d49c 3 | timeCreated: 1715979183 -------------------------------------------------------------------------------- /Runtime/TimerManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ImprovedTimers { 4 | public static class TimerManager { 5 | static readonly List timers = new(); 6 | static readonly List sweep = new(); 7 | 8 | public static void RegisterTimer(Timer timer) => timers.Add(timer); 9 | public static void DeregisterTimer(Timer timer) => timers.Remove(timer); 10 | 11 | public static void UpdateTimers() { 12 | if (timers.Count == 0) return; 13 | 14 | sweep.RefreshWith(timers); 15 | foreach (var timer in sweep) { 16 | timer.Tick(); 17 | } 18 | } 19 | 20 | public static void Clear() { 21 | sweep.RefreshWith(timers); 22 | foreach (var timer in sweep) { 23 | timer.Dispose(); 24 | } 25 | 26 | timers.Clear(); 27 | sweep.Clear(); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Runtime/TimerManager.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2bfa1555011848b9a73c2f770a6ef088 3 | timeCreated: 1716049555 -------------------------------------------------------------------------------- /Runtime/Timers.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 13afeb55753aa224f8c477485174d88a 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/Timers/CountdownTimer.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace ImprovedTimers { 4 | /// 5 | /// Timer that counts down from a specific value to zero. 6 | /// 7 | public class CountdownTimer : Timer { 8 | public CountdownTimer(float value) : base(value) { } 9 | 10 | public override void Tick() { 11 | if (IsRunning && CurrentTime > 0) { 12 | CurrentTime -= Time.deltaTime; 13 | } 14 | 15 | if (IsRunning && CurrentTime <= 0) { 16 | Stop(); 17 | } 18 | } 19 | 20 | public override bool IsFinished => CurrentTime <= 0; 21 | } 22 | } -------------------------------------------------------------------------------- /Runtime/Timers/CountdownTimer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0c325dd659a74e6aa1618f0ba21bb3d0 3 | timeCreated: 1716169238 -------------------------------------------------------------------------------- /Runtime/Timers/FrequencyTimer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace ImprovedTimers { 5 | /// 6 | /// Timer that ticks at a specific frequency. (N times per second) 7 | /// 8 | public class FrequencyTimer : Timer { 9 | public int TicksPerSecond { get; private set; } 10 | 11 | public Action OnTick = delegate { }; 12 | 13 | float timeThreshold; 14 | 15 | public FrequencyTimer(int ticksPerSecond) : base(0) { 16 | CalculateTimeThreshold(ticksPerSecond); 17 | } 18 | 19 | public override void Tick() { 20 | if (IsRunning && CurrentTime >= timeThreshold) { 21 | CurrentTime -= timeThreshold; 22 | OnTick.Invoke(); 23 | } 24 | 25 | if (IsRunning && CurrentTime < timeThreshold) { 26 | CurrentTime += Time.deltaTime; 27 | } 28 | } 29 | 30 | public override bool IsFinished => !IsRunning; 31 | 32 | public override void Reset() { 33 | CurrentTime = 0; 34 | } 35 | 36 | public void Reset(int newTicksPerSecond) { 37 | CalculateTimeThreshold(newTicksPerSecond); 38 | Reset(); 39 | } 40 | 41 | void CalculateTimeThreshold(int ticksPerSecond) { 42 | TicksPerSecond = ticksPerSecond; 43 | timeThreshold = 1f / TicksPerSecond; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /Runtime/Timers/FrequencyTimer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 288dd77b65004f8ea76e417031125cd7 3 | timeCreated: 1716169233 -------------------------------------------------------------------------------- /Runtime/Timers/StopwatchTimer.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace ImprovedTimers { 4 | /// 5 | /// Timer that counts up from zero to infinity. Great for measuring durations. 6 | /// 7 | public class StopwatchTimer : Timer { 8 | public StopwatchTimer() : base(0) { } 9 | 10 | public override void Tick() { 11 | if (IsRunning) { 12 | CurrentTime += Time.deltaTime; 13 | } 14 | } 15 | 16 | public override bool IsFinished => false; 17 | } 18 | } -------------------------------------------------------------------------------- /Runtime/Timers/StopwatchTimer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cb41bb125558422a84a40df1e900aacb 3 | timeCreated: 1716237753 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.gitamend.improvedtimers", 3 | "version": "1.0.2", 4 | "displayName": "Improved Timers", 5 | "description": "Improved Timer System for Unity that hooks into the Unity Player Loop.", 6 | "unity": "2022.1", 7 | "author": { 8 | "name": "Git Amend", 9 | "url": "https://www.youtube.com/@git-amend" 10 | } 11 | } -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 13bf5a4cbc79f9d42913bf11e8eee559 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------