Welcome, Guest. Please login or register.
Did you miss your activation email?

Login with username, password and session length

 
Advanced search

1405743 Posts in 68539 Topics- by 62234 Members - Latest Member: coolturtle

March 28, 2023, 04:33:38 AM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsDeveloperTechnical (Moderator: ThemsAllTook)Custom Motion Interpolation in Unity
Pages: [1]
Print
Author Topic: Custom Motion Interpolation in Unity  (Read 1464 times)
J-Snake
Level 10
*****


A fool with a tool is still a fool.


View Profile WWW
« on: August 21, 2022, 01:45:45 PM »

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.

Code:
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;
}

« Last Edit: September 13, 2022, 04:11:38 PM by J-Snake » Logged

Independent game developer with an elaborate focus on interesting gameplay, rewarding depth of play and technical quality.<br /><br />Trap Them: http://store.steampowered.com/app/375930
J-Snake
Level 10
*****


A fool with a tool is still a fool.


View Profile WWW
« Reply #1 on: August 26, 2022, 06:34:35 PM »

On a related note, I just posted this here to address Unity's engine team:
https://forum.unity.com/threads/time-deltatime-not-constant-vsync-camerafollow-and-jitter.430339/page-11

For interested people new to the topic of fixed update loops and interpolation, here is an introduction to that:
https://gafferongames.com/post/fix_your_timestep/

For a serious deep dive to a fundamental level, I dedicated an entire Bachelor-Thesis to the stability of fixed update loops (it also includes interpolation, but it is orthogonal to actual stability):
https://drive.google.com/file/d/1VajBHqmsajsU3Gp1ZYFhAPhavFQJC7wL/view?usp=sharing

It essentially reveals why "everyone" is implementing fixed time step loops wrong and offers better (and also simple) solutions. Now I know the paper is hard to read and is in need for a tldr-version. But to get the gist, just understand the graphical instability examples. Then you will understand the "Hysteresis Loop". It's almost as easy to implement as the "Standard Loop" (the one Unity is using). But it is significantly more stable and has no caveats.

Like already suggested, we should not use/rely on raw deltaTime values but on a moving average (take around 30-60 samples and everyone is happy). A moving average doesn't introduce a drift to the wall clock (as shown in the paper), and reduces noise significantly. (Also, the data type float is fine for deltaTime, but time/fixedTime should better be double since it accumulates over time. With float, you will get more noise and loss of information in longer play sesssions.)

You can see Unity's fixed update loop in the flowchart here btw:
https://docs.unity3d.com/Manual/TimeFrameManagement.html
« Last Edit: August 27, 2022, 05:56:56 AM by J-Snake » Logged

Independent game developer with an elaborate focus on interesting gameplay, rewarding depth of play and technical quality.<br /><br />Trap Them: http://store.steampowered.com/app/375930
michaelplzno
Level 10
*****



View Profile WWW
« Reply #2 on: August 27, 2022, 05:41:39 AM »

interesting bit, I'll make a video comparison of some of these time loops based on your code.
Logged

J-Snake
Level 10
*****


A fool with a tool is still a fool.


View Profile WWW
« Reply #3 on: August 27, 2022, 06:41:49 AM »

Then make sure the video itself is consistently paced. Wink

Btw.
A pretty nice (and sufficiently stable) solution is the hysteresis loop, tuned for a moving average filter over deltaTime.
This is what will likely end up in the next shipping code. The game will also include a "loop analyzer" that counts
the "update-errors" to confirm how effective it is to eliminate them. As of now, I cannot rely on Unity's plans so I will  
have to inject it myself into the engine. There is possibly a way to do it without sidestepping Unity's architecture.
The backdoor is manipulating fixedDeltaTime on a frame by frame basis to effectively make FixedUpdate() dance to your beat.
Logged

Independent game developer with an elaborate focus on interesting gameplay, rewarding depth of play and technical quality.<br /><br />Trap Them: http://store.steampowered.com/app/375930
Pages: [1]
Print
Jump to:  

Theme orange-lt created by panic