using UnityEngine; namespace Heroes.Battle { /// /// Forces an to update at a fixed low frame rate, giving /// the discrete, "stop-motion" look of Heroes of Might & Magic III sprites. /// /// Instead of letting Unity interpolate the pose every rendered frame, we disable /// the Animator's automatic update and step it ourselves in fixed chunks of /// 1 / targetFps seconds. Between steps the pose is frozen, so movement /// reads as a sequence of snapping poses rather than smooth motion. /// /// Works on any Animator/clip without editing the animation assets. Parameters /// (SetBool/SetTrigger) are still honoured — they are consumed on the next /// manual call. /// [RequireComponent(typeof(Animator))] public sealed class SteppedAnimator : MonoBehaviour { [Tooltip("How many animation frames per second to sample. Heroes 3 feel: ~8-12. " + "Lower = more choppy/retro, higher = smoother.")] [Range(2f, 30f)] public float targetFps = 10f; [Tooltip("Use unscaled time so the stepping keeps running while the game is paused (Time.timeScale = 0).")] public bool useUnscaledTime = false; private Animator _animator; private float _accumulator; private void Awake() { _animator = GetComponent(); // Stop Unity's per-frame automatic update; we drive it manually in Update(). _animator.enabled = false; } private void OnEnable() { _accumulator = 0f; } private void Update() { if (_animator == null || targetFps <= 0f) return; float step = 1f / targetFps; _accumulator += useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime; // Advance the pose in discrete chunks. If the game hitches and several // steps are due at once we still apply them, so timing stays correct // while the *visible* pose only ever changes on a step boundary. int guard = 0; while (_accumulator >= step && guard++ < 8) { _animator.Update(step); _accumulator -= step; } // Clamp leftover so a long stall doesn't queue a burst of catch-up steps. if (_accumulator > step) _accumulator = step; } /// Change the sampling rate at runtime (e.g. per unit type). public void SetTargetFps(float fps) { targetFps = Mathf.Max(1f, fps); } } }