Skip to content

Instantly share code, notes, and snippets.

@unitycoder
Created June 19, 2024 05:56
Show Gist options
  • Save unitycoder/63536f6fdf34b33953c6bbe219f1c620 to your computer and use it in GitHub Desktop.
Save unitycoder/63536f6fdf34b33953c6bbe219f1c620 to your computer and use it in GitHub Desktop.
Better FixedUpdate
// https://forum.unity.com/threads/stable-fixedupdate-scheduling-plus-motion-interpolation-global-solution.1547513/
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// This script schedules FixedUpdate() calls globally according to the Hysteresis loop.
///
/// How to use FixedUpdateScheduler:
/// Simply attach this script to a SINGLE object in the scene.
/// The script order is just a matter of preference as it works for any order.
/// But it reacts faster (in the next frame) to a change made to UpdatesPerSecond
/// when executed after that.
/// </summary>
public class FixedUpdateScheduler : MonoBehaviour
{
public static float InterpolationAlpha { get; private set; } = 0.0f;
public float AverageDeltaTime { get; private set; } = 0.0f;
public int NumberOfUpdatesInNextFrame { get; private set; } = 0;
public float UpdatesPerSecond
{
get { return updatesPerSecond_; }
set
{
updatesPerSecond_ = value;
if (updatesPerSecond_ < 0.0f) updatesPerSecond_ = 0.0f;
// When in doubt, increase that number:
if (updatesPerSecond_ > 360.0f) updatesPerSecond_ = 360.0f;
if (updatesPerSecond_ == 0.0f)
updatePeriod_ = float.PositiveInfinity;
else
updatePeriod_ = 1.0f / updatesPerSecond_;
}
}
void Awake()
{
UpdatesPerSecond = updatesPerSecond_;
if (maxUpdatesPerFrame_ < 0) maxUpdatesPerFrame_ = 0;
if (noiseWidth_ < 0.0f) noiseWidth_ = 0.0f;
if (filterLength_ < 1) filterLength_ = 1;
deltaTimeSamples_ = new List<float>(filterLength_);
thresholdOffset_ = 0.5f * noiseWidth_;
}
void Update()
{
InterpolationAlpha = Mathf.Clamp(unprocessedTime_ / updatePeriod_, 0.0f, 1.0f);
if (updatesPerSecond_ == 0.0f) // Gameplay is paused.
{
Time.fixedDeltaTime = float.PositiveInfinity;
return;
}
// Have one additional updatePeriod_ as buffer so that the unprocessedTime_
// is not truncated to zero (potential stutter otherwise) when
// maxUpdatesPerFrame_ are processed.
float maxUnprocessedTime = (maxUpdatesPerFrame_ + 1) * updatePeriod_;
float deltaTime = Time.deltaTime;
if (deltaTime > maxUnprocessedTime) deltaTime = maxUnprocessedTime;
UpdateAverageDeltaTime(deltaTime);
unprocessedTime_ += AverageDeltaTime;
if (unprocessedTime_ > maxUnprocessedTime) unprocessedTime_ = maxUnprocessedTime;
NumberOfUpdatesInNextFrame = 0;
while (true)
{
if (IsValueInUpper(unprocessedTime_))
thresholdOffset_ = -0.5f * noiseWidth_;
else if (IsValueInLower(unprocessedTime_))
thresholdOffset_ = 0.5f * noiseWidth_;
if (unprocessedTime_ < updatePeriod_ + thresholdOffset_ ||
NumberOfUpdatesInNextFrame >= maxUpdatesPerFrame_)
break;
NumberOfUpdatesInNextFrame++;
unprocessedTime_ -= updatePeriod_;
}
UpdateFixedDeltaTime(NumberOfUpdatesInNextFrame);
}
private bool IsValueInUpper(float value)
{
return value >= updatePeriod_ + 0.5f * noiseWidth_ &&
value < updatePeriod_ + 2.5f * noiseWidth_;
}
private bool IsValueInLower(float value)
{
return value > updatePeriod_ - 2.5f * noiseWidth_ &&
value < updatePeriod_ - 0.5f * noiseWidth_;
}
private void UpdateFixedDeltaTime(int numberOfUpdates)
{
float estimatedTimeGapInNextFrame = (Time.time - Time.fixedTime) + AverageDeltaTime;
// Maximize the probability to call FixedUpdate() numberOfUpdates times
// in the next frame.
Time.fixedDeltaTime = estimatedTimeGapInNextFrame / (numberOfUpdates + 0.5f);
}
private void UpdateAverageDeltaTime(float newDeltaTime)
{
// Add newDeltaTime to ring buffer:
if (deltaTimeSamples_.Count < filterLength_)
deltaTimeSamples_.Add(newDeltaTime);
else
{
deltaTimeSamples_[samplesIterator_] = newDeltaTime;
samplesIterator_++;
if (samplesIterator_ >= deltaTimeSamples_.Count)
samplesIterator_ = 0;
}
// Compute average delta time:
float deltaTimeSum = 0.0f;
foreach (float value in deltaTimeSamples_)
deltaTimeSum += value;
AverageDeltaTime = deltaTimeSum / deltaTimeSamples_.Count;
}
[SerializeField] private float updatesPerSecond_ = 60.0f;
[SerializeField] private int maxUpdatesPerFrame_ = 2;
[SerializeField] private float noiseWidth_ = 0.0005f;
[SerializeField] private int filterLength_ = 60;
private float updatePeriod_ = 1.0f / 60.0f;
private float unprocessedTime_ = 0.0f;
private float thresholdOffset_ = 0.00025f;
private int samplesIterator_ = 0; // For ring buffer implementation
private List<float> deltaTimeSamples_ = new List<float>(60);
}
// https://forum.unity.com/threads/stable-fixedupdate-scheduling-plus-motion-interpolation-global-solution.1547513/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// How to use TransformInterpolator properly:
/// 0. Make sure the gameobject executes its mechanics (transform-manipulations)
/// in FixedUpdate().
/// 1. Make sure VSYNC is enabled.
/// 2. Set the execution order for this script BEFORE all the other scripts
/// that execute mechanics.
/// 3. Attach (and enable) this component to every gameobject that you want to interpolate
/// (including the camera).
/// </summary>
public class TransformInterpolator : MonoBehaviour
{
private struct TransformData
{
public Vector3 position;
public Vector3 scale;
public Quaternion rotation;
}
// Init prevTransformData to interpolate from the correct state in the first frame the interpolation becomes
// active. This can occur when the object is spawned/instantiated.
void OnEnable()
{
prevTransformData.position = transform.localPosition;
prevTransformData.rotation = transform.localRotation;
prevTransformData.scale = transform.localScale;
isTransformInterpolated = false;
}
void FixedUpdate()
{
// Reset transform to its supposed current state just once after each Update/Drawing.
if (isTransformInterpolated)
{
transform.localPosition = transformData.position;
transform.localRotation = transformData.rotation;
transform.localScale = transformData.scale;
isTransformInterpolated = false;
}
// Cache current transform state as previous
// (becomes "previous" by the next transform-manipulation
// in FixedUpdate() of another component).
prevTransformData.position = transform.localPosition;
prevTransformData.rotation = transform.localRotation;
prevTransformData.scale = transform.localScale;
}
void LateUpdate() //Interpolate in Update() or LateUpdate().
{
// Cache the updated transform so that it can be restored in
// FixedUpdate() after drawing.
if (!isTransformInterpolated)
{
transformData.position = transform.localPosition;
transformData.rotation = transform.localRotation;
transformData.scale = transform.localScale;
//This promise matches the execution that follows after that.
isTransformInterpolated = true;
}
float interpolationAlpha = FixedUpdateScheduler.InterpolationAlpha;
//Interpolate transform:
transform.localPosition = Vector3.Lerp(prevTransformData.position,
transformData.position, interpolationAlpha);
transform.localRotation = Quaternion.Slerp(prevTransformData.rotation,
transformData.rotation, interpolationAlpha);
transform.localScale = Vector3.Lerp(prevTransformData.scale,
transformData.scale, interpolationAlpha);
}
private TransformData transformData;
private TransformData prevTransformData;
private bool isTransformInterpolated;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment