// MIT License - Copyright (c) 2023 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Core.DataStructure
{
using System;
using Random;
using UnityEngine;
///
/// A lightweight time-based cache that recomputes a value after a time-to-live interval expires.
///
///
/// enemyCount = new TimedCache(TimeSpan.FromSeconds(1f), () => FindEnemies().Count);
/// int cachedValue = enemyCount.GetValue(Time.time);
/// ]]>
///
/// Value type produced by the cache factory.
///
/// Use for expensive computations that can be reused for a short period (e.g., path costs, counts, queries).
/// Optionally introduces a one-time jitter to spread refreshes across frames when many caches exist.
///
public sealed class TimedCache
{
///
/// Gets the cached value, recomputing if the TTL (plus optional jitter) has elapsed.
///
public T Value
{
get
{
if (!_lastRead.HasValue)
{
ResetInternal(consumeJitter: false);
}
else
{
float expiration =
_cacheTtl + (_shouldUseJitter && !_usedJitter ? _jitterAmount : 0f);
if (_lastRead.Value + expiration < CurrentTime)
{
if (_shouldUseJitter)
{
_usedJitter = true;
}
ResetInternal(consumeJitter: false);
}
}
return _value;
}
}
private readonly Func _valueProducer;
private readonly float _cacheTtl;
private float? _lastRead;
private T _value;
private bool _usedJitter;
private readonly bool _shouldUseJitter;
private readonly float _jitterAmount;
private readonly Func _timeProvider;
///
/// Creates a time-based cache.
///
/// Factory invoked to recompute the value.
/// Time to live, in seconds.
/// If true, applies a single randomized offset up to to the first refresh.
/// Thrown when is null.
/// Thrown when is negative.
public TimedCache(
Func valueProducer,
float cacheTtl,
bool useJitter = false,
Func timeProvider = null,
float? jitterOverride = null
)
{
_valueProducer =
valueProducer ?? throw new ArgumentNullException(nameof(valueProducer));
if (cacheTtl < 0)
{
throw new ArgumentException(nameof(cacheTtl));
}
_cacheTtl = cacheTtl;
_shouldUseJitter = useJitter;
_jitterAmount = useJitter
? Mathf.Max(0f, jitterOverride ?? PRNG.Instance.NextFloat(0f, cacheTtl))
: 0f;
_timeProvider = timeProvider ?? (() => Time.time);
}
private float CurrentTime => _timeProvider();
///
/// Forces the cache to recompute the value and resets the TTL timer.
///
public void Reset()
{
ResetInternal(consumeJitter: true);
}
private void ResetInternal(bool consumeJitter)
{
_value = _valueProducer();
_lastRead = CurrentTime;
if (consumeJitter && _shouldUseJitter)
{
_usedJitter = true;
}
}
}
}