Welcome, Guest. Please login or register.

Login with username, password and session length

 
Advanced search

1412192 Posts in 69756 Topics- by 58694 Members - Latest Member: Ron Pang

January 24, 2025, 06:06:52 AM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsDeveloperTechnical (Moderator: ThemsAllTook)Custom Motion Interpolation in Unity (plus stable FixedUpdate())
Pages: [1]
Print
Author Topic: Custom Motion Interpolation in Unity (plus stable FixedUpdate())  (Read 7340 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 useful for those who are interested in a custom solution for motion-interpolation in Unity. The goal is to achieve stutter-free visuals for deterministic game logic that runs in FixedUpdate(). This is easily achieved by a single TransformInterpolator script. It is also a general one-size-fits-all solution that aims to automate all of the caretaking to prevent interpolation glitches from happening, in any edge case. Just use it as documented in few lines and you pretty much cannot go wrong. It will “just work”. :slight_smile:

If you are new to the topic of fixed timestep loops and motion-interpolation, here is an introduction to that:
https://gafferongames.com/post/fix_your_timestep/

And here is the complementary thread that shows how to make fixed timestep logic more responsive and potentially even more performant:
https://forum.unity.com/threads/sta...motion-interpolation-global-solution.1547513/

And this here documents how FixedUpdate() is timed in Unity:
https://docs.unity3d.com/Manual/TimeFrameManagement.html

And here is the script:

Code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public static class TransformUtils // Convenience functions:
{
    /// <summary>
    /// A SetParent() wrapper that works in a safe way and doesn't expose interpolation glitches.
    /// </summary>
    /// <param name="t"></param>
    /// <param name="parent"></param>
    /// <param name="transformLocalSpace"></param>
    public static void SetParentInterpolated(this Transform t, Transform parent, bool transformLocalSpace = true)
    {
        if (parent == t.parent) return;

        TransformInterpolator transformInterpolator = t.GetComponent<TransformInterpolator>();

        if (transformInterpolator != null && transformInterpolator.enabled)
        {
            // Skip interpolation for this frame:
            transformInterpolator.enabled = false;
            t.SetParent(parent, transformLocalSpace);
            transformInterpolator.enabled = true;
        }
        else
            t.SetParent(parent, transformLocalSpace);
    }

    /// <summary>
    /// A SetLocalPositionAndRotation()/SetPositionAndRotation() wrapper that suppresses interpolation.
    /// </summary>
    /// <param name="t"></param>
    /// <param name="position"></param>
    /// <param name="rotation"></param>
    /// <param name="transformLocalSpace"></param>
    public static void Teleport(this Transform t, Vector3 position, Quaternion rotation, bool transformLocalSpace = true)
    {
        TransformInterpolator transformInterpolator = t.GetComponent<TransformInterpolator>();

        if (transformInterpolator != null && transformInterpolator.enabled)
        {
            // Skip interpolation for this frame:
            transformInterpolator.enabled = false;

            if (transformLocalSpace)
                t.SetLocalPositionAndRotation(position, rotation);
            else
                t.SetPositionAndRotation(position, rotation);

            transformInterpolator.enabled = true;
        }
        else
        {
            if (transformLocalSpace)
                t.SetLocalPositionAndRotation(position, rotation);
            else
                t.SetPositionAndRotation(position, rotation);
        }
    }

}

/// <summary>
/// How to use TransformInterpolator properly:
/// 0. Make sure the gameobject executes its mechanics (transform-manipulations)
/// in FixedUpdate().
/// 1. Set the execution order for this script BEFORE all the other scripts
/// that execute mechanics.
/// 2. Attach (and enable) this component to every gameobject that you want to interpolate
/// (including the camera).
/// </summary>
public class TransformInterpolator : MonoBehaviour
{
    void OnDisable() // Restore current transform state:
    {
        if (isTransformInterpolated_)
        {
            transform.localPosition = transformData_.Position;
            transform.localRotation = transformData_.Rotation;
            transform.localScale = transformData_.Scale;

            isTransformInterpolated_ = false;
        }

        isEnabled_ = false;
    }

    void FixedUpdate()
    {
        // Restore current transform state in the first FixedUpdate() call of the frame.
        if (isTransformInterpolated_)
        {
            transform.localPosition = transformData_.Position;
            transform.localRotation = transformData_.Rotation;
            transform.localScale = transformData_.Scale;

            isTransformInterpolated_ = false;
        }
        // Cache current transform as the starting point for interpolation.
        prevTransformData_.Position = transform.localPosition;
        prevTransformData_.Rotation = transform.localRotation;
        prevTransformData_.Scale = transform.localScale;
    }

    void LateUpdate()   // Interpolate in Update() or LateUpdate().
    {
        // The TransformInterpolator could get enabled and then modified in this frame.
        // So we postpone the enabling procedure to refresh the starting point of interpolation.
        // And we just skip interpolation for the first frame.
        if (!isEnabled_)
        {
            OnEnabledProcedure();
            return;
        }

        // Cache the final transform state as the end point of interpolation.
        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;
        }

        // (Time.time - Time.fixedTime) is the "unprocessed" time according to documentation.
         float interpolationAlpha = (Time.time - Time.fixedTime) / Time.fixedDeltaTime;
        

        // 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 void OnEnabledProcedure() // Captures initial transform state.
    {
        prevTransformData_.Position = transform.localPosition;
        prevTransformData_.Rotation = transform.localRotation;
        prevTransformData_.Scale = transform.localScale;

        isEnabled_ = true;
    }

    private struct TransformData
    {
        public Vector3 Position;
        public Vector3 Scale;
        public Quaternion Rotation;
    }

    private TransformData transformData_;
    private TransformData prevTransformData_;
    private bool isTransformInterpolated_ = false;
    private bool isEnabled_ = false;
}

The alternative to custom motion-interpolation is Rigidbody.interpolation, that is natively provided by Unity. Here are the pros and cons of custom motion-interpolation so that you can make your decision.

Custom motion-interpolation pros:

  • You can interpolate animations that are running in FixedUpdate() (AnimatePhysics-mode).
  • You can turn off the TransformInterpolator for offscreen objects to achieve significantly better performance than doing so by using Rigidbody.
  • Transform is in sync with its state (not so otherwise: changes to Transform and Rigidbody.position are not immediate when moved by RigidBody.MovePosition()).

Custom motion-interpolation cons:

  • Less performant than Rigidbody.interpolation in general


Performance tips:

  • Don't interpolate Transform.localScale if it remains constant, as most objects don't grow in size. You might gain about 10% performance just by doing that if you have many objects on the screen. Engine calls can add up and get expensive.
  • Turn off interpolation for offscreen objects.
  • If the processing budget is too tight, it can still be Ok to just interpolate the camera and the main character/s. Running the TransformInterpolator for few objects won't be the bottleneck.


« Last Edit: October 18, 2024, 10:04:55 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 #1 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 #2 on: March 12, 2024, 04:11:01 AM »

I have complemented this thread by a stable FixedUpdate() scheduler, as some wanted to know how to transfer an abstract solution in the paper into Unity's concrete engine framework. This is also a great example for "Why reinventing the wheel?". Simply because we don't always get good wheels, but good wheels are fundamental. So here it goes:
https://discussions.unity.com/t/stable-fixedupdate-scheduler-with-motion-interpolation/940117
« Last Edit: January 23, 2025, 04:05:07 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 #3 on: January 23, 2025, 12:16:57 PM »





So I created a complete game that serves as a Unity showcase and test that implements the hysteresis loop exactly like presented. Strangler is possibly the first game that implements truly stable fixed timestepping. If you are interested, check out this page again for more info:
https://discussions.unity.com/t/stable-fixedupdate-scheduler-with-motion-interpolation/940117/8

But here is the TL;DR:

Since this hysteresis solution is implemented on top of Unity’s frame pacing mechanism, it can become counterproductive when Unity’s frame pacing itself becomes unstable. So, when you experience stutter either way, switching to Unity’s fixed timestepping is the recommended choice. However, if you use a game-manager architecture that calls everything from Update(), the hysteresis loop can be implemented more stably on there since it bypasses Unity’s timing logic entirely.

So for my game Strangler, that implements the hysteresis loop exactly like presented, I included the hysteresis loop as an optional setting—enabled by default. This might be the first game (and hopefully the last) to offer such an option, but I wanted to implement at least one complete game using Unity’s default GameObject/Monobehaviour workflow.

STABILITY TEST (and PERFORMANCE):

Strangler also serves as a performance test for Unity’s default GameObject workflow and incremental garbage collection (no object pooling is used). And the game implements a feature to test game loop stability at 60Hz. To run it:

    1. During gameplay, press Right Ctrl + T to start the stability test.
    2. Errors will display in real-time and stop automatically when gameplay is paused.
    3. Results are saved in the following files:

    FixedUpdateSequence.txt (always accurate).
    FixedUpdateErrors.txt (accurate only at ~60Hz refresh rate, otherwise contains garbage data).

The files are located in:
C:\Users\Username\AppData\LocalLow\RetroMasters82\Strangler\
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 #4 on: January 23, 2025, 03:18:28 PM »

As a game:

Looks cool, the trailer is a little long, but the art is nice and fun looking. Could use more levels (assorted color palates and baddies.) and meta game stuff (if you want to do mobile, they all expect daily challenges, leaderboards, etc.)

As a tech thing:

I'm not so sure why this shows off your need to interpolate between the fixed steps properly. If I were doing this game in unity, I wouldn't use fixed timesteps at all, I would just use the regular update and interpolate based on the time delta between updates (which is not fixed.)

Perhaps to showcase your power level, you might want to make a video of all the various types of interpolation and show that one is smoother than the other?

edit: oh yeah, levels with holes in them!
« Last Edit: January 23, 2025, 03:24:49 PM by michaelplzno » Logged

J-Snake
Level 10
*****


A fool with a tool is still a fool.


View Profile WWW
« Reply #5 on: January 23, 2025, 03:42:23 PM »

The point of fixed timestepping is determinism. Determinism is necessary to keep mechanics consistent, enabling them to be flawless and perfectly reproduceable. Your suggestion is variable timestepping, which is non-deterministic.

In short:
Now that determinism is a given, the main purpose of the hysteresis solution is to keep input-response more consistent (about 6%-10% more consistent timing windows on most setups), not just smooth visuals. The latter can be achieved either way with variable timestepping, as you suggested, or with Unity's fixed timestepping plus interpolation.

If your monitor supports a 60Hz mode and you'd like to run the test, here's what you can expect:
Play the game for about 10 minutes. If Unity's frame pacing remains stable, the hysteresis loop will produce zero update errors. With Unity's loop setting, you'll typically see 5+ errors.

This is reflected in FixedUpdateSequence.txt. Using Unity's setting, you'll find entries like "0" and "2" (indicating 0 or 2 FixedUpdate() calls within respective frames). With the hysteresis loop, you'll see a clean sequence of "...1111111...", confirming consistent timing of FixedUpdate() calls.

if you want to do mobile, they all expect daily challenges, leaderboards, etc.)
Thanks for the tip. Mobile is definitely the best fit for a game like that to easily kill time. Mobile hasn't been my world but I want to step into it with Strangler next and see how it controls and performs on there.

adding holes? Hmmm;)
« Last Edit: January 23, 2025, 04:47:25 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
michaelplzno
Level 10
*****



View Profile WWW
« Reply #6 on: January 23, 2025, 04:37:13 PM »

so, just to emphasize your determinism, perhaps you should add some kind of game play revolving around controlling time IE the speed and direction of time inside your simulaiton. This would emphasize your determinism because if you go backwards, or slower, the game will still run correctly 20 steps in either direction is perfect, as you say, so you could simply mark a frame with the input changes in a list: (Press left-frame20) (press up-frame92) etc and then it would be possible to rewind or speed up the state because your physics is so perfect.

I'm just saying you shouldn't make it difficult to appriciate why this physics timing is better.

In the movie The Prestige he does a trick that is super difficult but because of lack of showing it off as impressive no one claps:





For mobile, there is a standard template that everyone follows where you can unlock different skins etc, and get rewarded for playing every day, that kind of stuff is pretty boring for me but it's a must if that's what you are doing.
Logged

J-Snake
Level 10
*****


A fool with a tool is still a fool.


View Profile WWW
« Reply #7 on: January 23, 2025, 05:52:52 PM »

Think of it this way: if you can play a card safely, why would you choose to play it unsafely? In fact, deterministic systems are often simpler to manage compared to their non-deterministic counterparts.

Back in the early Proto-TrapThem days, I initially implemented the mechanics using variable timestep, just to verify this madness. As expected, this led to some levels being solvable on one machine but not on another—or even varying from one run to the next on the same machine—due to inconsistent mechanics. Many game systems are inherently unstable, where even slight changes in timing can drastically alter their flow and behavior. This is exactly why determinism is essential.
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