Created
June 19, 2024 05:56
-
-
Save unitycoder/63536f6fdf34b33953c6bbe219f1c620 to your computer and use it in GitHub Desktop.
Better FixedUpdate
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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