TimerKit is a versatile, easy-to-use timer component designed for Unity
Whether you're building a countdown for a game level, managing cooldowns, or triggering events at specific intervals, TimerKit provides a robust solution. It combines basic timing functionality with advanced features, all wrapped in an extensible and Unity-friendly design.
- Zero GC Allocations: Allocation-free per-frame updates for smooth performance.
- Basic Operations: Start, stop, reset, and query the timer's state.
- Pause & Resume: Pause the timer and pick up where you left off.
- Fast Forward & Rewind: Skip ahead or backtrack through time.
- Milestones: Trigger custom actions at specific time or progress points.
- Range Milestones: Trigger events at regular intervals within a time range.
- Serialization: Save and load timer states for persistent gameplay.
- Unity Integration: Works seamlessly as a MonoBehaviour or standalone class.
- Extensible Architecture: Multiple timer classes for different complexity needs.
- Service Locator Support: Optional integration with dependency injection patterns.
- Fixed: package now compiles in projects without ServiceKit installed
- Fixed: recurring milestones re-trigger when a timer is restarted with
StartTimer()alone - Fixed: milestones sharing a trigger value across different time types now trigger independently
- Changed: range milestone intervals must be greater than zero (the constructor now throws)
See CHANGELOG.md for complete version history.
TimerKit is designed for zero per-frame allocations in production scenarios:
| Operation | Allocations |
|---|---|
| BasicTimer.Update() | 0 bytes |
| StandardTimer.Update() (no milestones) | 0 bytes |
| StandardTimer.Update() (with milestones) | 0 bytes |
| Milestone triggering | 0 bytes |
| Range milestone processing | 0 bytes |
Optimizations include:
- Index-based iteration instead of
foreachto avoid enumerator allocations - Pooled collections reused across frames
- Early exit when no milestones are registered
- No LINQ or temporary object creation in hot paths
This makes TimerKit suitable for performance-critical applications where GC pressure must be minimized.
Add TimerKit to your Unity project via Package Manager:
- Open Window > Package Manager
- Click + > Add package from git URL
- Enter:
https://www.pkglnk.dev/timerkit.git
If you like my work then please consider showing your support by buying me a brew
The package provides a flexible hierarchy of timer classes to suit different needs:
BasicTimer: Pure timer functionality without milestone support (~150 lines)MilestoneTimer: Extends BasicTimer with milestone supportStandardTimer: Full-featured timer with all capabilities (recommended for new projects)Timer: Unity MonoBehaviour wrapper for Unity integrationSimpleTimer: [DEPRECATED] Alias for StandardTimer (maintained for backward compatibility)
IReadOnlyTimer: Read-only timer properties (TimeRemaining, TimeElapsed, Progress, etc.)IBasicTimer: Basic timer operations extending IReadOnlyTimerITimer: Full timer functionality with milestone management
For Unity integration with Inspector support:
usingNonatomic.TimerKit;usingUnityEngine;publicclassCountdownExample:MonoBehaviour{privateTimer_timer;voidStart(){_timer=gameObject.AddComponent<Timer>();_timer.Duration=30f;// 30 seconds_timer.OnComplete+=()=>Debug.Log("Countdown finished!");_timer.StartTimer();}}For pure C# usage without Unity dependencies:
usingNonatomic.TimerKit;publicclassStandaloneExample{privateStandardTimer_timer;publicvoidStartCountdown(){_timer=newStandardTimer(30f);// 30 seconds_timer.OnComplete+=()=>Console.WriteLine("Countdown finished!");_timer.StartTimer();// In your update loop:// _timer.Update(deltaTime);}}// Create a timervartimer=newStandardTimer(10f);// 10 second duration// Control the timertimer.StartTimer();// Start from full durationtimer.ResumeTimer();// Resume from current positiontimer.StopTimer();// Pause the timertimer.ResetTimer();// Reset to full duration// Time manipulationtimer.FastForward(2f);// Skip ahead 2 secondstimer.Rewind(1f);// Go back 1 second// Query timer stateboolisRunning=timer.IsRunning;floattimeLeft=timer.TimeRemaining;floatelapsed=timer.TimeElapsed;floatprogress=timer.ProgressElapsed;// 0.0 to 1.0timer.OnStart+=()=>Debug.Log("Timer started");timer.OnResume+=()=>Debug.Log("Timer resumed");timer.OnStop+=()=>Debug.Log("Timer stopped");timer.OnComplete+=()=>Debug.Log("Timer completed");timer.OnTick+=(IReadOnlyTimert)=>Debug.Log($"Time: {t.TimeRemaining}");timer.OnDurationChanged+=(floatnewDuration)=>Debug.Log($"Duration changed to: {newDuration}");Milestones trigger callbacks when the timer reaches specific points. You can create them using either the convenience API (passing components) or by creating milestone objects manually:
// Convenience API - pass components directly (recommended)timer.AddMilestone(TimeType.TimeRemaining,5f,()=>{Debug.Log("5 seconds left!");});// Progress-based milestonetimer.AddMilestone(TimeType.ProgressElapsed,0.75f,()=>{Debug.Log("75% complete!");});// Recurring milestone - triggers every timer roundtimer.AddMilestone(TimeType.TimeRemaining,5f,()=>{Debug.Log("5 seconds warning!");},isRecurring:true);// Manual creation (if you need to store the reference)varmilestone=newTimerMilestone(TimeType.TimeRemaining,5f,()=>{Debug.Log("5 seconds left!");});timer.AddMilestone(milestone);// Remove milestonestimer.RemoveMilestone(milestone);timer.RemoveAllMilestones();timer.RemoveMilestonesByCondition(m =>m.TriggerValue<3f);Range milestones trigger at regular intervals within a specified range. Like regular milestones, you can create them using either the convenience API or by creating instances manually:
// Convenience API - pass components directly (recommended)timer.AddRangeMilestone(TimeType.TimeRemaining,// Type of time to track10f,// Range start (10 seconds remaining)0f,// Range end (0 seconds remaining)1f,// Interval (every 1 second)()=>Debug.Log("Countdown warning!")// Callback);// Trigger every 0.5 seconds from 2-5 seconds elapsedtimer.AddRangeMilestone(TimeType.TimeElapsed,2f,// Start at 2 seconds elapsed5f,// End at 5 seconds elapsed0.5f,// Every 0.5 seconds()=>PlayTickSound()// Callback);// Recurring range milestone - triggers every timer roundtimer.AddRangeMilestone(TimeType.TimeRemaining,10f,0f,2f,()=>Debug.Log("Every 2 seconds!"),isRecurring:true);// Manual creation (if you need to store the reference)varrangeMilestone=newTimerRangeMilestone(TimeType.TimeRemaining,10f,0f,1f,()=>Debug.Log("Countdown warning!"));timer.AddRangeMilestone(rangeMilestone);TimeRemaining: Time left on the timer (countdown)TimeElapsed: Time passed since timer startedProgressElapsed: Completion progress (0.0 to 1.0)ProgressRemaining: Remaining progress (1.0 to 0.0)
publicclassLevelTimer:MonoBehaviour{privateTimer_timer;voidStart(){_timer=gameObject.AddComponent<Timer>();_timer.Duration=300f;// 5 minutes// Add warning milestones using convenience API_timer.AddMilestone(TimeType.TimeRemaining,60f,()=>ShowWarning("1 minute remaining!"));_timer.AddMilestone(TimeType.TimeRemaining,30f,()=>ShowWarning("30 seconds remaining!"));// Add countdown for last 10 seconds_timer.AddRangeMilestone(TimeType.TimeRemaining,10f,0f,1f,()=>PlayCountdownBeep());_timer.OnComplete+=()=>EndLevel();_timer.StartTimer();}}publicclassAbilityCooldown:MonoBehaviour{[SerializeField]privatefloat_cooldownDuration=5f;privateTimer_cooldownTimer;voidStart(){_cooldownTimer=gameObject.AddComponent<Timer>();_cooldownTimer.Duration=_cooldownDuration;_cooldownTimer.OnComplete+=()=>OnCooldownComplete();}publicvoidUseAbility(){if(_cooldownTimer.IsRunning)return;// Still on cooldown// Execute ability logic hereDebug.Log("Ability used!");// Start cooldown_cooldownTimer.StartTimer();}privatevoidOnCooldownComplete(){Debug.Log("Ability ready!");}publicfloatGetCooldownProgress()=>_cooldownTimer.ProgressElapsed;}TimerKit supports external time synchronization through the ITimeSource interface. This allows you to sync timers with external systems like network time, game sessions, or custom time managers:
// Create a custom time sourcepublicclassGameSessionTimeSource:MonoBehaviour,ITimeSource{privatefloat_sessionTimeRemaining=300f;// 5 minutespublicfloatGetTimeRemaining()=>_sessionTimeRemaining;publicvoidSetTimeRemaining(floattimeRemaining)=>_sessionTimeRemaining=timeRemaining;publicboolCanSetTime=>true;// Allow timer to modify timevoidUpdate(){// Update session time from your game logic_sessionTimeRemaining-=Time.deltaTime;}}// Use custom time source with a timer// (MonoBehaviour time sources must be added as components, not constructed with new)varsessionTimeSource=gameObject.AddComponent<GameSessionTimeSource>();vartimer=newStandardTimer(300f,sessionTimeSource);timer.StartTimer();// Timer now syncs with sessionTimeSource instead of tracking its own timeFor automatic Unity component integration, extend TimeSourceProvider:
publicclassNetworkTimeProvider:TimeSourceProvider{privatefloat_networkTime;publicoverridefloatGetTimeRemaining()=>_networkTime;publicoverridevoidSetTimeRemaining(floattimeRemaining)=>_networkTime=timeRemaining;publicoverrideboolCanSetTime=>false;// Read-only from networkvoidStart(){// Fetch network timeStartCoroutine(SyncWithServer());}IEnumeratorSyncWithServer(){// Your network sync logic hereyieldreturnnull;}}Attach the NetworkTimeProvider component to the same GameObject as a Timer component and they will connect automatically. Note that the automatic connection requires the concrete Timer MonoBehaviour; other ITimer implementations will log a warning and remain unconnected.
TimerKit ships with optional integrations that activate automatically when the matching package is installed (via assembly definition version defines):
- Installing ServiceKit 2.0.0+ defines
TIMERKIT_SERVICEKIT_SUPPORT - Installing ServiceLocator 0.5.0+ defines
TIMERKIT_SERVICE_LOCATOR_SUPPORT
Use the built-in TimerService (registered as ITimerService), or derive your own:
usingNonatomic.ServiceKit;usingNonatomic.TimerKit.Extensions.ServiceKit;publicinterfaceIGameTimerService:IBaseTimerService{}[Service(typeof(IGameTimerService))]publicclassGameTimerService:BaseTimerService,IGameTimerService{// Your custom timer logic here}usingNonatomic.TimerKit.Extensions.ServiceLocator;publicinterfaceIGameTimerService:IBaseTimerService{}publicclassGameTimerService:BaseTimerService<IGameTimerService>,IGameTimerService{// Your custom timer logic here}If you're using the deprecated SimpleTimer class:
Before:
vartimer=newSimpleTimer(10f);After:
vartimer=newStandardTimer(10f);The API is identical, but StandardTimer provides better clarity about the class's capabilities.
- Use
BasicTimerwhen you only need start/stop/reset functionality - Use
MilestoneTimerwhen you need milestone support but want a lighter class - Use
StandardTimerfor full functionality (recommended for most use cases) - Use
Timer(MonoBehaviour) for Unity Inspector integration
SimpleTimer has been deprecated in favor of StandardTimer. This is not a breaking change - all existing code continues to work unchanged, but you'll see compiler warnings.
SimpleTimerclass - UseStandardTimerinstead
- All existing APIs remain identical - no code changes required
- Full backward compatibility maintained
- All existing functionality preserved
// Old (still works, but shows deprecation warning)vartimer=newSimpleTimer(10f);// New (recommended for new code)vartimer=newStandardTimer(10f);The new class hierarchy provides better separation of concerns:
- Smaller classes for specific needs (BasicTimer for simple cases)
- Clearer intent with descriptive names (StandardTimer vs SimpleTimer)
- Better extensibility with proper inheritance chain
Existing projects can continue using SimpleTimer without any changes. The deprecation warning can be suppressed if needed:
#pragma warning disable CS0618// Type or member is obsoletevartimer=newSimpleTimer(10f);
#pragma warning restore CS0618
