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.
- Zero-Allocation Updates: Complete rewrite of milestone processing for allocation-free per-frame updates
- Performance Optimized: All hot-path code now allocates 0 bytes per frame
- Early Exit Optimization: Timer skips milestone processing entirely when no milestones exist
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 the TimerKit package to your Unity project via the Unity Package Manager:
- Open the Package Manager (
Window > Package Manager). - Click the + button and select "Add package from git URL".
- Enter:
https://github.com/PaulNonatomic/TimerKit.git. - Click Add.
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
var sessionTimeSource = new 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 any component implementing ITimer, and they will automatically connect. The TimeSourceProvider works with any ITimer implementation, not just the concrete Timer MonoBehaviour.
When the SERVICE_LOCATOR preprocessor directive is defined, you can use dependency injection:
// Custom timer service
public class GameTimerService : BaseTimerService<IGameTimerService>
{
// Your custom timer logic here
}
// Register and use
ServiceLocator.Register<IGameTimerService>(gameTimerService);
var timerService = ServiceLocator.Get<IGameTimerService>();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