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:
using Nonatomic.TimerKit;
using UnityEngine;
public class CountdownExample : MonoBehaviour
{
private Timer _timer;
void Start()
{
_timer = gameObject.AddComponent<Timer>();
_timer.Duration = 30f; // 30 seconds
_timer.OnComplete += () => Debug.Log("Countdown finished!");
_timer.StartTimer();
}
}For pure C# usage without Unity dependencies:
using Nonatomic.TimerKit;
public class StandaloneExample
{
private StandardTimer _timer;
public void StartCountdown()
{
_timer = new StandardTimer(30f); // 30 seconds
_timer.OnComplete += () => Console.WriteLine("Countdown finished!");
_timer.StartTimer();
// In your update loop:
// _timer.Update(deltaTime);
}
}// Create a timer
var timer = new StandardTimer(10f); // 10 second duration
// Control the timer
timer.StartTimer(); // Start from full duration
timer.ResumeTimer(); // Resume from current position
timer.StopTimer(); // Pause the timer
timer.ResetTimer(); // Reset to full duration
// Time manipulation
timer.FastForward(2f); // Skip ahead 2 seconds
timer.Rewind(1f); // Go back 1 second
// Query timer state
bool isRunning = timer.IsRunning;
float timeLeft = timer.TimeRemaining;
float elapsed = timer.TimeElapsed;
float progress = 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 += (IReadOnlyTimer t) => Debug.Log($"Time: {t.TimeRemaining}");
timer.OnDurationChanged += (float newDuration) => 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 milestone
timer.AddMilestone(TimeType.ProgressElapsed, 0.75f, () => {
Debug.Log("75% complete!");
});
// Recurring milestone - triggers every timer round
timer.AddMilestone(TimeType.TimeRemaining, 5f, () => {
Debug.Log("5 seconds warning!");
}, isRecurring: true);
// Manual creation (if you need to store the reference)
var milestone = new TimerMilestone(TimeType.TimeRemaining, 5f, () => {
Debug.Log("5 seconds left!");
});
timer.AddMilestone(milestone);
// Remove milestones
timer.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 track
10f, // 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 elapsed
timer.AddRangeMilestone(
TimeType.TimeElapsed,
2f, // Start at 2 seconds elapsed
5f, // End at 5 seconds elapsed
0.5f, // Every 0.5 seconds
() => PlayTickSound() // Callback
);
// Recurring range milestone - triggers every timer round
timer.AddRangeMilestone(
TimeType.TimeRemaining,
10f,
0f,
2f,
() => Debug.Log("Every 2 seconds!"),
isRecurring: true
);
// Manual creation (if you need to store the reference)
var rangeMilestone = new TimerRangeMilestone(
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)
public class LevelTimer : MonoBehaviour
{
private Timer _timer;
void Start()
{
_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();
}
}public class AbilityCooldown : MonoBehaviour
{
[SerializeField] private float _cooldownDuration = 5f;
private Timer _cooldownTimer;
void Start()
{
_cooldownTimer = gameObject.AddComponent<Timer>();
_cooldownTimer.Duration = _cooldownDuration;
_cooldownTimer.OnComplete += () => OnCooldownComplete();
}
public void UseAbility()
{
if (_cooldownTimer.IsRunning) return; // Still on cooldown
// Execute ability logic here
Debug.Log("Ability used!");
// Start cooldown
_cooldownTimer.StartTimer();
}
private void OnCooldownComplete()
{
Debug.Log("Ability ready!");
}
public float GetCooldownProgress() => _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 source
public class GameSessionTimeSource : MonoBehaviour, ITimeSource
{
private float _sessionTimeRemaining = 300f; // 5 minutes
public float GetTimeRemaining() => _sessionTimeRemaining;
public void SetTimeRemaining(float timeRemaining) => _sessionTimeRemaining = timeRemaining;
public bool CanSetTime => true; // Allow timer to modify time
void Update()
{
// 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)
var sessionTimeSource = gameObject.AddComponent<GameSessionTimeSource>();
var timer = new StandardTimer(300f, sessionTimeSource);
timer.StartTimer();
// Timer now syncs with sessionTimeSource instead of tracking its own timeFor automatic Unity component integration, extend TimeSourceProvider:
public class NetworkTimeProvider : TimeSourceProvider
{
private float _networkTime;
public override float GetTimeRemaining() => _networkTime;
public override void SetTimeRemaining(float timeRemaining) => _networkTime = timeRemaining;
public override bool CanSetTime => false; // Read-only from network
void Start()
{
// Fetch network time
StartCoroutine(SyncWithServer());
}
IEnumerator SyncWithServer()
{
// Your network sync logic here
yield return null;
}
}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:
using Nonatomic.ServiceKit;
using Nonatomic.TimerKit.Extensions.ServiceKit;
public interface IGameTimerService : IBaseTimerService { }
[Service(typeof(IGameTimerService))]
public class GameTimerService : BaseTimerService, IGameTimerService
{
// Your custom timer logic here
}using Nonatomic.TimerKit.Extensions.ServiceLocator;
public interface IGameTimerService : IBaseTimerService { }
public class GameTimerService : BaseTimerService<IGameTimerService>, IGameTimerService
{
// Your custom timer logic here
}If you're using the deprecated SimpleTimer class:
Before:
var timer = new SimpleTimer(10f);After:
var timer = new StandardTimer(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)
var timer = new SimpleTimer(10f);
// New (recommended for new code)
var timer = new StandardTimer(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 obsolete
var timer = new SimpleTimer(10f);
#pragma warning restore CS0618
