├── InterpolationFactorController.cs ├── InterpolationObjectController.cs ├── README.md └── Tests └── TestMotion.cs /InterpolationFactorController.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | [DisallowMultipleComponent] 4 | [DefaultExecutionOrder(ORDER_EXECUTION)] 5 | public class InterpolationFactorController : MonoBehaviour 6 | { 7 | public const int ORDER_EXECUTION = -1000; 8 | 9 | private static InterpolationFactorController Instance; 10 | private float[] _lastFixedUpdates = new float[2]; 11 | private int _lastIndex; 12 | 13 | public static float Factor { get; private set; } 14 | 15 | private void Awake() 16 | { 17 | if (Instance) 18 | { 19 | Destroy(this); 20 | Debug.LogWarning($"The '{typeof(InterpolationFactorController).Name}' is a singleton!"); 21 | return; 22 | } 23 | 24 | Instance = this; 25 | Factor = 1; 26 | } 27 | 28 | private void Start() 29 | { 30 | _lastFixedUpdates = new float[2] { Time.fixedTime, Time.fixedTime }; 31 | _lastIndex = 0; 32 | } 33 | 34 | private void FixedUpdate() 35 | { 36 | _lastIndex = NextIndex(); 37 | _lastFixedUpdates[_lastIndex] = Time.fixedTime; 38 | } 39 | 40 | private void Update() 41 | { 42 | float lastTime = _lastFixedUpdates[_lastIndex]; 43 | float prevTime = _lastFixedUpdates[NextIndex()]; 44 | 45 | if (lastTime == prevTime) 46 | { 47 | Factor = 1; 48 | return; 49 | } 50 | 51 | Factor = (Time.time - lastTime) / (lastTime - prevTime); 52 | } 53 | 54 | private int NextIndex() 55 | { 56 | return (_lastIndex == 0) ? 1 : 0; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /InterpolationObjectController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using UnityEngine; 3 | 4 | [DisallowMultipleComponent] 5 | [DefaultExecutionOrder(ORDER_EXECUTION)] 6 | public class InterpolationObjectController : MonoBehaviour 7 | { 8 | public const int ORDER_EXECUTION = InterpolationFactorController.ORDER_EXECUTION - 1; 9 | 10 | private TransformData[] _transforms; 11 | private int _index; 12 | 13 | private void Awake() 14 | { 15 | StartCoroutine(WaitForEndOfFrame()); 16 | StartCoroutine(WaitForFixedUpdate()); 17 | } 18 | 19 | private void OnEnable() 20 | { 21 | ResetTransforms(); 22 | } 23 | 24 | private void BeforeFixedUpdate() 25 | { 26 | // Restoring actual transform for the FixedUpdate() cal where it could be change by the user. 27 | RestoreActualTransform(); 28 | } 29 | 30 | private void AfterFixedUpdate() 31 | { 32 | // Saving actual transform for being restored in the BeforeFixedUpdate() method. 33 | SaveActualTransform(); 34 | } 35 | 36 | private void Update() 37 | { 38 | // Set interpolated transform for being rendered. 39 | SetInterpolatedTransform(); 40 | } 41 | 42 | #region Helpers 43 | 44 | private void RestoreActualTransform() 45 | { 46 | var latest = _transforms[_index]; 47 | transform.localPosition = latest.position; 48 | transform.localScale = latest.scale; 49 | transform.localRotation = latest.rotation; 50 | } 51 | 52 | private void SaveActualTransform() 53 | { 54 | _index = NextIndex(); 55 | _transforms[_index] = CurrentTransformData(); 56 | } 57 | 58 | private void SetInterpolatedTransform() 59 | { 60 | var prev = _transforms[NextIndex()]; 61 | float factor = InterpolationFactorController.Factor; 62 | transform.localPosition = Vector3.Lerp(prev.position, transform.localPosition, factor); 63 | transform.localRotation = Quaternion.Slerp(prev.rotation, transform.localRotation, factor); 64 | transform.localScale = Vector3.Lerp(prev.scale, transform.localScale, factor); 65 | } 66 | 67 | public void ResetTransforms() 68 | { 69 | _index = 0; 70 | var td = CurrentTransformData(); 71 | _transforms = new TransformData[2] { td, td }; 72 | } 73 | 74 | private TransformData CurrentTransformData() 75 | { 76 | return new TransformData(transform.localPosition, transform.localRotation, transform.localScale); 77 | } 78 | 79 | private int NextIndex() 80 | { 81 | return (_index == 0) ? 1 : 0; 82 | } 83 | 84 | private IEnumerator WaitForEndOfFrame() 85 | { 86 | while (true) 87 | { 88 | yield return new WaitForEndOfFrame(); 89 | BeforeFixedUpdate(); 90 | } 91 | } 92 | 93 | private IEnumerator WaitForFixedUpdate() 94 | { 95 | while (true) 96 | { 97 | yield return new WaitForFixedUpdate(); 98 | AfterFixedUpdate(); 99 | } 100 | } 101 | 102 | private struct TransformData 103 | { 104 | public Vector3 position; 105 | public Quaternion rotation; 106 | public Vector3 scale; 107 | 108 | public TransformData(Vector3 position, Quaternion rotation, Vector3 scale) 109 | { 110 | this.position = position; 111 | this.rotation = rotation; 112 | this.scale = scale; 113 | } 114 | } 115 | 116 | #endregion 117 | } 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Smooth Motion in Unity (refactored) 2 | 3 | This is an improved (refactored) version of the code from the ["Timesteps and Achieving Smooth Motion in Unity"](https://www.kinematicsoup.com/news/2016/8/9/rrypp5tkubynjwxhxjzd42s3o034o8?utm_source=youtube&utm_type=SMVideo) article. 4 | 5 | Now, there are only 2 components instead of original 3: 6 | 7 | 1. [`InterpolationFactorController`](https://github.com/DevelAx/Smooth-Motion-in-Unity/blob/master/InterpolationFactorController.cs) a singleton scene component (the original name was `InterpolationController`) that calculates the current *interpolation factor*. 8 | 2. [`InterpolationObjectController`](https://github.com/DevelAx/Smooth-Motion-in-Unity/blob/master/InterpolationObjectController.cs) a component for a moving object (the original name was `InterpolatedTransform`) which calculates the current interpolation for the object in the `Update()` method before it's being rendered and then restores its original `transform` values for the next `FixedUpdate()` call. 9 | 3. The `InterpolatedTransformUpdater` component was removed as unnecessary. 10 | 11 | ## Remarks 12 | For these scripts to work correctly the user's script must change the object's `transform` only in the `FixedUpdate()` method. When teleporting the object the user should call the (`InterpolationObjectController`).`ResetTransforms()` method after it. 13 | -------------------------------------------------------------------------------- /Tests/TestMotion.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | /// 4 | /// Use this component with a game object for demonstration purposes. 5 | /// 6 | [DisallowMultipleComponent] 7 | [RequireComponent(typeof(InterpolationObjectController))] 8 | public class TestMotion : MonoBehaviour 9 | { 10 | [SerializeField] 11 | private float _speed = 2f; 12 | 13 | private void FixedUpdate() 14 | { 15 | transform.position += Vector3.right * _speed * Time.fixedDeltaTime; 16 | transform.rotation = Quaternion.Euler(transform.rotation.eulerAngles.x, transform.rotation.eulerAngles.y + _speed * 30 * Time.fixedDeltaTime, transform.rotation.eulerAngles.z); 17 | } 18 | } 19 | --------------------------------------------------------------------------------