// 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; } } } }