This post is targeted towards Unity users who want to implement their own deterministic mechanics (using FixedUpdate())
with smooth visual motion interpolation.
I recently started to study Unity to see if I can make the engine work for my upcoming games without working against the engine too much. One of the first things to sort out is some gameloop related stuff. I was expecting to find elegant and commonly known solutions to add motion interpolation. But it doesn't look like it.
I have seen ridiculously convoluted solutions involving multiple scripts, like this one:
But I figured you only need just one little script. And I know there are Unity users out there wanting a simple solution. So, based on the documentation on how Unity works, here is my solution to that. Feel free to use the
TransformInterpolator (and extend if necessary) in your projects.
An advice on performance: If your game has many moving gameobjects in the environment at a given time, then performance might become a concern, in addition because manipulating the transform in Unity is rather expensive. In that case I recommend to just interpolate the camera and the player (whatever the main gameobject/s is). It makes a dramatic difference with practically zero cost.
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 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()
{
if (isTransformInterpolated) //reset transform to its supposed current state just once after each Update/Drawing
{
transform.localPosition = transformData.position;
transform.localRotation = transformData.rotation;
transform.localScale = transformData.scale;
isTransformInterpolated = false;
}
//cache current transform state (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()
{
if (!isTransformInterpolated) //cache the updated transform in transformData so that it can be restored in FixedUpdate() after drawing
{
transformData.position = transform.localPosition;
transformData.rotation = transform.localRotation;
transformData.scale = transform.localScale;
isTransformInterpolated = true; //it's ok to set it here since the anticipation matches the execution that follows
}
float interpolationAlpha = (Time.time - Time.fixedTime) / Time.fixedDeltaTime; //(Time.time - Time.fixedTime) is the "unprocessed" time according to documentation
transform.localPosition = Vector3.Lerp(prevTransformData.position, transformData.position, interpolationAlpha); //interpolate transform
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;
}