├── img ├── plot_data.png ├── update_analysis_bad.png ├── update_analysis_good.png ├── update_analysis_velpos_bad1.png ├── update_analysis_velpos_bad2.png └── update_analysis_velpos_good.png ├── other ├── plot_data.xlsx └── update_analysis.pptx ├── src ├── TimeAlgebraRust │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── TimeAlgebra │ ├── TimeAlgebra │ ├── TimeAlgebra.vcxproj.filters │ ├── TimeAlgebra.cpp │ ├── FloatTime.h │ ├── TimeAlgebra.vcxproj │ └── GameSimulation.h │ └── TimeAlgebra.sln ├── .gitignore ├── LICENSE └── README.md /img/plot_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huwb/jitternator/HEAD/img/plot_data.png -------------------------------------------------------------------------------- /other/plot_data.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huwb/jitternator/HEAD/other/plot_data.xlsx -------------------------------------------------------------------------------- /img/update_analysis_bad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huwb/jitternator/HEAD/img/update_analysis_bad.png -------------------------------------------------------------------------------- /img/update_analysis_good.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huwb/jitternator/HEAD/img/update_analysis_good.png -------------------------------------------------------------------------------- /other/update_analysis.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huwb/jitternator/HEAD/other/update_analysis.pptx -------------------------------------------------------------------------------- /src/TimeAlgebraRust/Cargo.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "time_algebra_rust" 3 | version = "0.1.0" 4 | 5 | -------------------------------------------------------------------------------- /img/update_analysis_velpos_bad1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huwb/jitternator/HEAD/img/update_analysis_velpos_bad1.png -------------------------------------------------------------------------------- /img/update_analysis_velpos_bad2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huwb/jitternator/HEAD/img/update_analysis_velpos_bad2.png -------------------------------------------------------------------------------- /img/update_analysis_velpos_good.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huwb/jitternator/HEAD/img/update_analysis_velpos_good.png -------------------------------------------------------------------------------- /src/TimeAlgebraRust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "time_algebra_rust" 3 | version = "0.1.0" 4 | authors = ["Huw Bowles "] 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/TimeAlgebra/.vs 2 | src/TimeAlgebra/Debug 3 | src/TimeAlgebra/TimeAlgebra/Release 4 | src/TimeAlgebra/TimeAlgebra/Debug 5 | src/TimeAlgebra/TimeAlgebra.VC.VC.opendb 6 | src/TimeAlgebra/TimeAlgebra.VC.db 7 | src/TimeAlgebraRust/target 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Huw Bowles 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/TimeAlgebra/TimeAlgebra/TimeAlgebra.vcxproj.filters: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;hm;inl;inc;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | Source Files 20 | 21 | 22 | 23 | 24 | Header Files 25 | 26 | 27 | Header Files 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/TimeAlgebra/TimeAlgebra.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25123.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TimeAlgebra", "TimeAlgebra\TimeAlgebra.vcxproj", "{9967771F-7ACA-449A-994A-9DFAC8CF0ADF}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Debug|x86 = Debug|x86 12 | Release|x64 = Release|x64 13 | Release|x86 = Release|x86 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {9967771F-7ACA-449A-994A-9DFAC8CF0ADF}.Debug|x64.ActiveCfg = Debug|x64 17 | {9967771F-7ACA-449A-994A-9DFAC8CF0ADF}.Debug|x64.Build.0 = Debug|x64 18 | {9967771F-7ACA-449A-994A-9DFAC8CF0ADF}.Debug|x86.ActiveCfg = Debug|Win32 19 | {9967771F-7ACA-449A-994A-9DFAC8CF0ADF}.Debug|x86.Build.0 = Debug|Win32 20 | {9967771F-7ACA-449A-994A-9DFAC8CF0ADF}.Release|x64.ActiveCfg = Release|x64 21 | {9967771F-7ACA-449A-994A-9DFAC8CF0ADF}.Release|x64.Build.0 = Release|x64 22 | {9967771F-7ACA-449A-994A-9DFAC8CF0ADF}.Release|x86.ActiveCfg = Release|Win32 23 | {9967771F-7ACA-449A-994A-9DFAC8CF0ADF}.Release|x86.Build.0 = Release|Win32 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /src/TimeAlgebra/TimeAlgebra/TimeAlgebra.cpp: -------------------------------------------------------------------------------- 1 | // This file is subject to the MIT License as seen in the root of this folder structure (LICENSE) 2 | // 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "FloatTime.h" 9 | #include "GameSimulation.h" 10 | 11 | void TestGame() 12 | { 13 | printf( "\n== TestGame ==\n" ); 14 | 15 | GameSimulation game; 16 | 17 | FloatTime frameDt = FloatTime::SimStartValue( 1.0f / 30.0f ); 18 | 19 | for( int i = 0; i < 10; i++ ) 20 | { 21 | game.Update( frameDt ); 22 | 23 | game.Render( frameDt ); 24 | 25 | // modify dt each frame slightly so that it is not uniform 26 | FloatTime newDt = frameDt + FloatTime( 0.001f, frameDt ); 27 | 28 | // advance time by current dt, and use new dt next frame 29 | AdvanceDt( frameDt, newDt ); 30 | } 31 | } 32 | 33 | void TestSimple() 34 | { 35 | printf( "\n== TestSimple ==\n" ); 36 | 37 | FloatTime dt = FloatTime::SimStartValue( 1.0f / 32.0f ); 38 | 39 | FloatTime pos = FloatTime::SimStartValue( 1.0f ); 40 | FloatTime vel = FloatTime::SimStartValue( 2.0f ); 41 | 42 | FloatTime lastTarget = FloatTime::SimStartValue( 0.0f ); 43 | bool lastTargetValid = false; 44 | 45 | for( int i = 0; i < 10; i++ ) 46 | { 47 | // test - taking animated value at end of frame 48 | FloatTime target = FloatTime( 10.0f, dt ); 49 | // artificially push dt forwards - to simulate getting a value at end frame time 50 | target.FinishedUpdate( dt ); 51 | 52 | // compute accel before updating pos! 53 | // this default value has time=0 and would only work on the first frame. on subsequent 54 | // frames it is overwritten with the calculation in the branch. 55 | FloatTime accel = FloatTime::SimStartValue( 0.0f ); 56 | if( lastTargetValid ) 57 | { 58 | accel = lastTarget - pos; 59 | accel *= FloatTime( 4.0f, dt ); 60 | } 61 | 62 | // integrate pos before vel! 63 | pos.Integrate( vel, dt ); 64 | 65 | printf( "%f\n", pos.Value() ); 66 | 67 | vel.Integrate( accel, dt ); 68 | 69 | lastTarget = target; 70 | lastTargetValid = true; 71 | 72 | AdvanceDt( dt ); 73 | } 74 | } 75 | 76 | int main() 77 | { 78 | TestGame(); 79 | 80 | TestSimple(); 81 | 82 | return 0; 83 | } 84 | -------------------------------------------------------------------------------- /src/TimeAlgebra/TimeAlgebra/FloatTime.h: -------------------------------------------------------------------------------- 1 | // This file is subject to the MIT License as seen in the root of this folder structure (LICENSE) 2 | // 3 | 4 | #pragma once 5 | 6 | class FloatTime; 7 | // could this be automatic through a conversion constructor? 8 | void CheckConsistency( const FloatTime& a, const FloatTime& b ); 9 | 10 | 11 | /** 12 | * A float value with a timestamp attached 13 | */ 14 | class FloatTime 15 | { 16 | private: 17 | FloatTime() 18 | { 19 | } 20 | 21 | public: 22 | // creates a float with timestamp, using the timestamp from an existing value. using the 23 | // frame dt is a common pattern here 24 | explicit FloatTime( float value, const FloatTime& timeGiver ) 25 | { 26 | _value = value; 27 | _time = timeGiver.Time(); 28 | } 29 | 30 | FloatTime operator+( const FloatTime& other ) const 31 | { 32 | CheckConsistency( *this, other ); 33 | FloatTime result; 34 | result._value = _value + other._value; 35 | result._time = _time; 36 | return result; 37 | } 38 | FloatTime operator-( const FloatTime& other ) const 39 | { 40 | CheckConsistency( *this, other ); 41 | FloatTime result; 42 | result._value = _value - other._value; 43 | result._time = _time; 44 | return result; 45 | } 46 | FloatTime operator*( const FloatTime& other ) const 47 | { 48 | CheckConsistency( *this, other ); 49 | FloatTime result; 50 | result._value = _value * other._value; 51 | result._time = _time; 52 | return result; 53 | } 54 | FloatTime operator/( const FloatTime& other ) const 55 | { 56 | CheckConsistency( *this, other ); 57 | FloatTime result; 58 | result._value = _value / other._value; 59 | result._time = _time; 60 | return result; 61 | } 62 | 63 | FloatTime& operator+=( const FloatTime& other ) 64 | { 65 | CheckConsistency( *this, other ); 66 | _value += other.Value(); 67 | return *this; 68 | } 69 | FloatTime& operator-=( const FloatTime& other ) 70 | { 71 | CheckConsistency( *this, other ); 72 | _value -= other.Value(); 73 | return *this; 74 | } 75 | FloatTime& operator*=( const FloatTime& other ) 76 | { 77 | CheckConsistency( *this, other ); 78 | _value *= other._value; 79 | return *this; 80 | } 81 | 82 | void Integrate( const FloatTime& rateOfChange, const FloatTime& dt ) 83 | { 84 | CheckConsistency( *this, rateOfChange ); 85 | CheckConsistency( *this, dt ); 86 | 87 | *this += rateOfChange * dt; 88 | 89 | _time = Time() + dt.Value(); 90 | } 91 | 92 | void FinishedUpdate( const FloatTime& dt ) 93 | { 94 | CheckConsistency( *this, dt ); 95 | 96 | _time = Time() + dt.Value(); 97 | } 98 | 99 | float Value() const 100 | { 101 | return _value; 102 | } 103 | 104 | float Time() const 105 | { 106 | return _time; 107 | } 108 | 109 | // straightforward lerp of two values, given a lerp parameter 110 | static FloatTime Lerp( const FloatTime& a, const FloatTime& b, const FloatTime& lerpParam ) 111 | { 112 | return (FloatTime( 1.0f, lerpParam ) - lerpParam) * a + lerpParam * b; 113 | } 114 | 115 | // lerps between two values at different simulation times, to give an interpolated value 116 | // at the provided target time. this would probably only be used in special cases, such as 117 | // in code that runs simulations. 118 | static FloatTime LerpToTime( const FloatTime& a, const FloatTime& b, const FloatTime& lerpTargetTime ) 119 | { 120 | FloatTime result; 121 | // strip time as we are interpolating across multiple times 122 | float s = (lerpTargetTime.Value() - a.Time()) / (b.Time() - a.Time()); 123 | // lerp manually (unprotected) 124 | result._value = (1.0f - s) * a.Value() + s * b.Value(); 125 | result._time = (1.0f - s) * a.Time() + s * b.Time(); 126 | return result; 127 | } 128 | 129 | static FloatTime SimStartValue( float value ) 130 | { 131 | FloatTime result; 132 | result._value = value; 133 | result._time = 0.0f; 134 | return result; 135 | } 136 | 137 | private: 138 | float _value = 0.0f; 139 | float _time = 0.0f; 140 | }; 141 | 142 | bool ApproxEqual( float a, float b, float eps = 0.0001f ) 143 | { 144 | return abs( a - b ) < eps; 145 | } 146 | 147 | void CheckConsistency( const FloatTime& a, const FloatTime& b ) 148 | { 149 | bool consistent = ApproxEqual( a.Time(), b.Time() ); 150 | if( !consistent ) 151 | __debugbreak(); 152 | } 153 | 154 | void AdvanceDt( FloatTime& io_dt, const FloatTime& newDt ) 155 | { 156 | CheckConsistency( io_dt, newDt ); 157 | 158 | // save current dt 159 | FloatTime curDt = io_dt; 160 | // apply new dt value for next frame 161 | io_dt = newDt; 162 | // advance time by the current dt 163 | io_dt.FinishedUpdate( curDt ); 164 | } 165 | 166 | void AdvanceDt( FloatTime& io_dt ) 167 | { 168 | AdvanceDt( io_dt, io_dt ); 169 | } 170 | 171 | // Vel is interesting as we have timestamps for the values. This eliminates an issue i've seen where a vel is 172 | // computed through finite differences but with an incorrect dt. 173 | FloatTime Vel( const FloatTime& val_t0, const FloatTime& val_t1 ) 174 | { 175 | if( val_t1.Time() < val_t0.Time() ) 176 | __debugbreak(); // expected arg order violated. this could be gracefully handled but erring on strictness for now.. 177 | 178 | //// returning 0 is not ideal, so do nothing for now 179 | //if( ApproxEqual( val_t0.Time(), val_t1.Time() ) ) 180 | // return 0.0f; 181 | 182 | float vel = (val_t1.Value() - val_t0.Value()) / (val_t1.Time() - val_t0.Time()); 183 | 184 | return FloatTime( vel, val_t1 ); 185 | } 186 | -------------------------------------------------------------------------------- /src/TimeAlgebraRust/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub fn run() { 2 | println!("Hello"); 3 | } 4 | 5 | #[derive(Debug, Copy, Clone)] 6 | pub struct Timed { 7 | value: T, 8 | time: f32, 9 | } 10 | 11 | pub trait Linear { 12 | type Output; 13 | fn mul(&self, other: f32) -> Self::Output; 14 | } 15 | impl Linear for f32 { 16 | type Output = f32; 17 | fn mul(&self, other: f32) -> Self::Output { 18 | self * other 19 | } 20 | } 21 | 22 | // general types 23 | impl Timed { 24 | pub fn sim_start(value: T) -> Timed { 25 | Timed { value, time: 0.0 } 26 | } 27 | 28 | pub fn new(value: T, dt: &Dt) -> Timed { 29 | Timed { 30 | value, 31 | time: dt.time, 32 | } 33 | } 34 | } 35 | 36 | // math ops 37 | impl Timed 38 | where 39 | T: Add + Mul + Linear + Copy, 40 | { 41 | pub fn integrate(&mut self, deriv: &Timed, dt: &Dt) { 42 | check_times(self, deriv); 43 | check_times(self, dt); 44 | self.value = self.value + Linear::mul(&deriv.value, dt.value); 45 | self.time = self.time + dt.value; 46 | } 47 | } 48 | 49 | 50 | type Dt = Timed; 51 | 52 | impl Dt { 53 | pub fn advance(&mut self) { 54 | self.time += self.value; 55 | } 56 | 57 | pub fn finished_update(&mut self, dt: &Dt) { 58 | check_times(self, dt); 59 | self.time = self.time + dt.value; 60 | } 61 | } 62 | 63 | use std::ops::{Add, Div, Mul, Sub}; 64 | 65 | fn check_times(a: &Timed, b: &Timed) { 66 | assert!( 67 | a.time == b.time, 68 | "Mismatched times: {} != {}", 69 | a.time, 70 | b.time 71 | ); 72 | } 73 | 74 | #[cfg(test)] 75 | mod tests { 76 | use super::*; 77 | 78 | #[test] 79 | fn dt_advance() { 80 | let mut dt = Dt::sim_start(1.0 / 32.0); 81 | dt.advance(); 82 | assert_eq!(dt.time, dt.value); 83 | } 84 | 85 | #[test] 86 | fn simple_sim() { 87 | let mut dt = Dt::sim_start(1.0); 88 | let mut vel = Timed::sim_start(2.0); 89 | let mut pos = Timed::sim_start(1.0); 90 | 91 | for _i in 0..10 { 92 | // target comes from user input or animation, etc 93 | let target = Timed::new(10.0, &dt); 94 | 95 | let accel = target - pos; 96 | pos.integrate(&vel, &dt); 97 | vel.integrate(&accel, &dt); 98 | 99 | dt.advance(); 100 | } 101 | 102 | assert_eq!( 103 | pos, 104 | Timed { 105 | value: 74.0, 106 | time: 10.0, 107 | } 108 | ); 109 | assert_eq!( 110 | vel, 111 | Timed { 112 | value: 288.0, 113 | time: 10.0, 114 | } 115 | ); 116 | } 117 | 118 | #[test] 119 | fn simple_sim_target_ahead() { 120 | let mut dt = Dt::sim_start(1.0 / 32.0); 121 | let mut vel = Timed::sim_start(2.0); 122 | let mut pos = Timed::sim_start(1.0); 123 | 124 | let mut target_last = Timed::sim_start(0.0); 125 | let mut target_last_valid = false; 126 | 127 | for _i in 0..10 { 128 | // target comes from user input or animation, etc 129 | let mut target = Timed::new(10.0, &dt); 130 | target.finished_update(&dt); 131 | 132 | // compute acceleration. defaults to 0 at time 0 - only valid 133 | // on first simulation step! 134 | let mut accel = Timed::sim_start(0.0); 135 | if target_last_valid { 136 | accel = target_last - pos; 137 | } 138 | target_last_valid = true; 139 | target_last = target; 140 | 141 | pos.integrate(&vel, &dt); 142 | vel.integrate(&accel, &dt); 143 | 144 | dt.advance(); 145 | 146 | // (pretend to) modify dt based on measured real time passed in last iteration 147 | dt = Timed::new(dt.value + 0.1, &dt); 148 | } 149 | } 150 | 151 | #[test] 152 | fn add() { 153 | let s = Timed::sim_start(5); 154 | let t = Timed::sim_start(6); 155 | assert_eq!(s + t, Timed::sim_start(11)); 156 | } 157 | 158 | #[test] 159 | fn sub() { 160 | let s = Timed::sim_start(5); 161 | let t = Timed::sim_start(6); 162 | assert_eq!(s - t, Timed::sim_start(-1)); 163 | } 164 | 165 | #[test] 166 | fn mul() { 167 | let s = Timed::sim_start(5); 168 | let t = Timed::sim_start(6); 169 | assert_eq!(s * t, Timed::sim_start(30)); 170 | } 171 | 172 | #[test] 173 | fn div() { 174 | let s = Timed::sim_start(6); 175 | let t = Timed::sim_start(2); 176 | assert_eq!(s / t, Timed::sim_start(3)); 177 | } 178 | 179 | #[test] 180 | #[should_panic] 181 | fn add_mismatched_times_panics() { 182 | let s = Timed::sim_start(2.0); 183 | let mut t = Timed::sim_start(4.0); 184 | let dt = Dt::sim_start(1.0 / 32.0); 185 | t.finished_update(&dt); 186 | s + t; 187 | } 188 | 189 | #[test] 190 | #[should_panic] 191 | fn eq_mismatched_times_panics() { 192 | let s = Timed::sim_start(2.0); 193 | let mut t = Timed::sim_start(4.0); 194 | let dt = Dt::sim_start(1.0 / 32.0); 195 | t.finished_update(&dt); 196 | s == t; 197 | } 198 | } 199 | 200 | 201 | impl Add for Timed 202 | where 203 | T: Add, 204 | { 205 | type Output = Timed; 206 | 207 | fn add(self, other: Timed) -> Timed { 208 | check_times(&self, &other); 209 | Timed { 210 | value: self.value.add(other.value), 211 | time: self.time, 212 | } 213 | } 214 | } 215 | 216 | impl Sub for Timed 217 | where 218 | T: Sub, 219 | { 220 | type Output = Timed; 221 | 222 | fn sub(self, other: Timed) -> Timed { 223 | check_times(&self, &other); 224 | Timed { 225 | value: self.value.sub(other.value), 226 | time: self.time, 227 | } 228 | } 229 | } 230 | 231 | impl Mul for Timed 232 | where 233 | T: Mul, 234 | { 235 | type Output = Timed; 236 | 237 | fn mul(self, other: Timed) -> Timed { 238 | check_times(&self, &other); 239 | Timed { 240 | value: self.value.mul(other.value), 241 | time: self.time, 242 | } 243 | } 244 | } 245 | 246 | impl Div for Timed 247 | where 248 | T: Div, 249 | { 250 | type Output = Timed; 251 | 252 | fn div(self, other: Timed) -> Timed { 253 | check_times(&self, &other); 254 | Timed { 255 | value: self.value.div(other.value), 256 | time: self.time, 257 | } 258 | } 259 | } 260 | 261 | impl PartialEq for Timed 262 | where 263 | T: PartialEq, 264 | { 265 | fn eq(&self, other: &Timed) -> bool { 266 | check_times(&self, &other); 267 | self.value == other.value 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/TimeAlgebra/TimeAlgebra/TimeAlgebra.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | {9967771F-7ACA-449A-994A-9DFAC8CF0ADF} 23 | Win32Proj 24 | TimeAlgebra 25 | 8.1 26 | 27 | 28 | 29 | Application 30 | true 31 | v140 32 | Unicode 33 | 34 | 35 | Application 36 | false 37 | v140 38 | true 39 | Unicode 40 | 41 | 42 | Application 43 | true 44 | v140 45 | Unicode 46 | 47 | 48 | Application 49 | false 50 | v140 51 | true 52 | Unicode 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | true 74 | 75 | 76 | true 77 | 78 | 79 | false 80 | 81 | 82 | false 83 | 84 | 85 | 86 | 87 | 88 | Level3 89 | Disabled 90 | WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 91 | 92 | 93 | Console 94 | true 95 | 96 | 97 | 98 | 99 | 100 | 101 | Level3 102 | Disabled 103 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 104 | 105 | 106 | Console 107 | true 108 | 109 | 110 | 111 | 112 | Level3 113 | 114 | 115 | MaxSpeed 116 | true 117 | true 118 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 119 | 120 | 121 | Console 122 | true 123 | true 124 | true 125 | 126 | 127 | 128 | 129 | Level3 130 | 131 | 132 | MaxSpeed 133 | true 134 | true 135 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 136 | 137 | 138 | Console 139 | true 140 | true 141 | true 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /src/TimeAlgebra/TimeAlgebra/GameSimulation.h: -------------------------------------------------------------------------------- 1 | // This file is subject to the MIT License as seen in the root of this folder structure (LICENSE) 2 | // 3 | 4 | #pragma once 5 | 6 | #include "FloatTime.h" 7 | 8 | /** 9 | * Mock game simulation 10 | */ 11 | class GameSimulation 12 | { 13 | public: 14 | void Update( const FloatTime& frameDt ) 15 | { 16 | InputsUpdate( frameDt ); 17 | AnimationUpdate( frameDt ); 18 | //CameraUpdateWithRestOfGame( frameDt ); 19 | PhysicsUpdate( frameDt ); 20 | MainUpdate( frameDt ); 21 | 22 | //CameraUpdateNoSim( frameDt ); 23 | CameraUpdateEndFrame( frameDt ); 24 | } 25 | 26 | void InputsUpdate( const FloatTime& frameDt ) 27 | { 28 | _inputValLast = _inputVal; 29 | 30 | // get keyboard input - here just use dt as an arbitrary mock input. 31 | // assume our input value comes from the frame start time. may not always be the case! 32 | _inputVal = FloatTime( 30.0f, frameDt ); 33 | } 34 | 35 | FloatTime SampleAnimation( const FloatTime& frameDt ) 36 | { 37 | // evaluate animation at end frame time - I've seen this on previous projects 38 | 39 | // animated value is just a linear curve 40 | FloatTime val = FloatTime( 5.0f * frameDt.Time(), frameDt ); 41 | 42 | // now move val forward to end frame time 43 | val.FinishedUpdate( frameDt ); 44 | 45 | return val; 46 | } 47 | 48 | void AnimationUpdate( const FloatTime& frameDt ) 49 | { 50 | // assume that the sampled animation gives the end-frame (rendered) values 51 | 52 | // in this case, the start frame values are the end frame values from the previous frame 53 | _carAnimTargetPos = _carAnimTargetPosEndFrame; 54 | 55 | // compute a new end frame value 56 | _carAnimTargetPosEndFrame = SampleAnimation( frameDt ); 57 | } 58 | 59 | void PhysicsUpdate( const FloatTime& frameDt ) 60 | { 61 | // _physTimeBalance is the cumulative delta between game update time and physics update time. this could be changed 62 | // to be just a float instead of a FloatTime, but putting this delta on the physics time gives a little bit of 63 | // additional validation that the simulation is consistent. 64 | _physTimeBalance += FloatTime( frameDt.Value(), _physicsDt ); 65 | 66 | if( _physTimeBalance.Value() <= 0.0f ) 67 | { 68 | // nothing to be done - physics is already up to date 69 | return; 70 | } 71 | 72 | // this will be used to interpolate physics -> camera time 73 | CarState lastState; 74 | 75 | // loop while we still have outstanding time to simulate - while physics is behind camera shutter time 76 | do 77 | { 78 | lastState = _carStateLatest; 79 | 80 | PhysicsUpdateStep( frameDt, _physicsDt ); 81 | 82 | // update balance and move value forward in time in one fell swoop, by using the integrate function 83 | // with a velocity of -1, which will make the value decrease by the amount of time moved forward. 84 | _physTimeBalance.Integrate( FloatTime( -1.0f, _physicsDt ), _physicsDt ); 85 | 86 | AdvanceDt( _physicsDt ); 87 | 88 | } while( _physTimeBalance.Value() > 0.0f ); 89 | 90 | // now interpolate the physics state at the camera shutter time 91 | 92 | // cam shutter time is current time + delta time 93 | FloatTime camShutterTime = FloatTime( frameDt.Time(), frameDt ) + frameDt; 94 | 95 | _carStateCurrent._pos = FloatTime::LerpToTime( lastState._pos, _carStateLatest._pos, camShutterTime ); 96 | _carStateCurrent._vel = FloatTime::LerpToTime( lastState._vel, _carStateLatest._vel, camShutterTime ); 97 | 98 | // optional assert to ensure two separate values are in sync 99 | CheckConsistency( _carStateCurrent._pos, _carStateCurrent._vel ); 100 | } 101 | 102 | void PhysicsUpdateStep( const FloatTime& frameDt, const FloatTime& physicsDt ) 103 | { 104 | // we do multiple physics updates in a frame. here the update takes values from 2 sources: 105 | 106 | // - animation data - we could potentially subsample the animation to give fresh data for each physics step. instead we 107 | // knowingly and explicitly use the state start frame value by resampling at the current physics time: 108 | CheckConsistency( _carAnimTargetPos, frameDt ); 109 | FloatTime carAnimTargetPos_const = FloatTime( _carAnimTargetPos.Value(), physicsDt ); 110 | 111 | // - input values - again we explicitly reuse stale data, but in some scnarios (VR) we may sample up to date fresh values here 112 | // and would not need to hack this: 113 | CheckConsistency( _inputVal, frameDt ); 114 | FloatTime inputVal_const = FloatTime( _inputVal.Value(), physicsDt ); 115 | 116 | FloatTime accel = inputVal_const + (carAnimTargetPos_const - _carStateLatest._pos); 117 | 118 | _carStateLatest._pos.Integrate( _carStateLatest._vel, physicsDt ); 119 | _carStateLatest._vel.Integrate( accel, physicsDt ); 120 | } 121 | 122 | void MainUpdate( const FloatTime& frameDt ) 123 | { 124 | // systems update - ai, logic, etc 125 | } 126 | 127 | // this can be run after the other bits in the game are updated. it doesn't use the dt value in the time update. 128 | void CameraUpdateNoSim( const FloatTime& frameDt ) 129 | { 130 | // use the frame time giver, advanced to the end of the frame 131 | FloatTime cameraDt = frameDt; 132 | AdvanceDt( cameraDt ); 133 | 134 | // place camera two units behind car (locked - no dynamics!) 135 | _cameraPos = _carStateCurrent._pos - FloatTime( 2.0f, cameraDt ); 136 | 137 | // add a bit of user input. we decide here to take the start frame input values, and therefore set the time manually 138 | _cameraPos += FloatTime( 0.1f * _inputVal.Value(), cameraDt ); 139 | } 140 | 141 | // this one should be run with start frame data (i.e. before car updates) 142 | void CameraUpdateWithRestOfGame( const FloatTime& frameDt ) 143 | { 144 | // lerp camera towards car 145 | _cameraPos = FloatTime::Lerp( _cameraPos, _carStateCurrent._pos, FloatTime( 6.0f, frameDt ) * frameDt ); 146 | 147 | // add influence from changing input 148 | if( _inputVal.Time() > _inputValLast.Time() ) 149 | { 150 | _cameraPos += FloatTime( 0.2f, frameDt ) * Vel( _inputValLast, _inputVal ); 151 | } 152 | 153 | // add influence from speed 154 | _cameraPos -= _carStateCurrent._vel * FloatTime( 0.1f, frameDt ); 155 | 156 | _cameraPos.FinishedUpdate( frameDt ); 157 | } 158 | 159 | // a scheme i often see is cameras are updated at the end of the frame, using end frame values. this function tries to implement this, 160 | // but is not consistent, see the comments below. 161 | void CameraUpdateEndFrame( const FloatTime& frameDt ) 162 | { 163 | // SCHEME: sample car position etc at the end frame values, and then simulate forwards from end this frame state. so the from-time is 164 | // the end frame time, and to to-time must then be 1 frame ahead. unfortunately this does not work in this strict framework because 165 | // we don't know how far forward to advance the camera sim, because we don't know the next frame dt (this is typically measured from 166 | // real time at the end of the frame). 167 | 168 | // so instead we take the end frme state but set the times to be the start frame state, and then update from there. this feels similar to 169 | // operator splitting for solving differential equations. there is some error from this but i dont have a clear understanding 170 | // of if or when this error would manifest as jitter. 171 | 172 | // sample at start frame time 173 | FloatTime camCarPos = FloatTime( _carStateCurrent._pos.Value(), frameDt ); 174 | FloatTime camCarVel = FloatTime( _carStateCurrent._vel.Value(), frameDt ); 175 | 176 | // lerp camera towards car 177 | _cameraPos = FloatTime::Lerp( _cameraPos, camCarPos, FloatTime( 6.0f, frameDt ) * frameDt ); 178 | 179 | // add influence from changing input 180 | if( _inputVal.Time() > _inputValLast.Time() ) 181 | { 182 | _cameraPos += FloatTime( 0.2f, frameDt ) * Vel( _inputValLast, _inputVal ); 183 | } 184 | 185 | // add influence from speed 186 | _cameraPos -= camCarVel * FloatTime( 0.1f, frameDt ); 187 | 188 | _cameraPos.FinishedUpdate( frameDt ); 189 | } 190 | 191 | // sample end frame state and render 192 | void Render( const FloatTime& frameDt ) 193 | { 194 | // sample simulation state 195 | const FloatTime renderCarPos = _carStateCurrent._pos; 196 | const FloatTime renderCarVel = _carStateCurrent._vel; 197 | const FloatTime renderCamPos = _cameraPos; 198 | 199 | // strong check - everything should be at end frame time 200 | FloatTime endFrameTime = frameDt; 201 | AdvanceDt( endFrameTime ); 202 | 203 | CheckConsistency( renderCarPos, endFrameTime ); 204 | CheckConsistency( renderCarVel, endFrameTime ); 205 | CheckConsistency( renderCamPos, endFrameTime ); 206 | 207 | // the 'render' 208 | printf( "Car pos: %f\n", _carStateCurrent._pos.Value() ); 209 | } 210 | 211 | struct CarState 212 | { 213 | FloatTime _pos = FloatTime::SimStartValue( 0.0f ); 214 | FloatTime _vel = FloatTime::SimStartValue( 0.0f ); 215 | }; 216 | 217 | CarState _carStateLatest; 218 | CarState _carStateCurrent; 219 | 220 | FloatTime _inputVal = FloatTime::SimStartValue( 0.0f ); 221 | FloatTime _inputValLast = FloatTime::SimStartValue( 0.0f ); 222 | 223 | FloatTime _carAnimTargetPos = FloatTime::SimStartValue( 0.0f ); 224 | FloatTime _carAnimTargetPosEndFrame = FloatTime::SimStartValue( 0.0f ); 225 | 226 | FloatTime _physTimeBalance = FloatTime::SimStartValue( 0.0f ); 227 | FloatTime _physicsDt = FloatTime::SimStartValue( 1.0f / 64.0f ); 228 | 229 | FloatTime _cameraPos = FloatTime::SimStartValue( 0.0f ); 230 | }; 231 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jitternator 2 | 3 | Lessons learnt from hunting jitter issues! 4 | 5 | 6 | ## Introduction 7 | 8 | In the past I've spent weeks painstakingly hunting down jitter issues - visible stutter in games. I decided to document the lessons and techniques I picked up along the way in the hope that it might help others. 9 | 10 | If some element of your game is jittering, the first thing to check is if you are updating the element (or the camera!) in fixed time steps? This is a specific type of jitter related to fixed time steps that is a subset of general jitter issues, but happen frequently enough that they are worth calling out specifically (if you have a camera moving in Update() chasing a moving rigidbody in Unity, it will jitter by default). There are notes and links in the Misc Notes section below about this specific case. 11 | 12 | If the jitter is not related to fixed update, get ready to dig in for the long haul, and persevere. If jitter is occurring, there will be a good reason! The gameplay may break down at low/unsteady FPS, but there should *never* be visible jitter! 13 | 14 | I have also added an experimental implementation of attaching timestamps to data to enforce consistency and detect timing issues at runtime, find details below. 15 | 16 | 17 | ## Preparation 18 | 19 | The following preparation will help greatly when diagnosing jitter issues. 20 | 21 | Check how dt is computed in the game update loop - typically the dt for frame N is the measured real time that passed while processing the previous frame N-1. 22 | This time might be clamped, filtered, etc. 23 | It can be useful to turn any such frame time modifications off when debugging jitter, to simplify the behaviour and avoid confusion. 24 | Similarly if there is a max update dt or max substep count for physics, consider removing it, or checking that the game won't hit this limit for you while testing, as this may cause "legitimate" jitter which would not occur in practice. 25 | 26 | Obtain an unsteady FPS. 27 | Many/most jitter issues will not be visible when framerate is constant and consistent. UE4 has a great feature for this - t.UnsteadyFPS, which will set the frame dt to a random value each frame. 28 | Do all of the tests below with unsteady FPS. 29 | Something else I've done in the past is to insert a really long frame every 5 seconds using a sleep statement or equivalent at the beginning of the update, which might give more predictable/repeatable results. 30 | 31 | 32 | ## Plot Data vs Time 33 | 34 | A good quick sanity check to see if some game data is smooth/not jittering is printing it out each frame to the output log, together with associated times. For example print out the end frame data values along with the end frame time. Keep in mind the physics time and frame end time may not coincide. I frequently hack in something like the below which generates an output that is very quick to paste into Excel and plot: 35 | 36 | ```cpp 37 | static bool doPrint = false; 38 | if( doPrint ) 39 | { 40 | printf("time:\t$f\tvalue1:\t%f\tvalue2:\t%f\n", endFrameTime, value1, value2 ); 41 | } 42 | ``` 43 | 44 | The static can be switched on at run time by setting a breakpoint and then poking a value in through the debugger (in VS mouse over doPrint and click the Pin, then set the value to 0 or 1 to turn it on and off). 45 | 46 | Copy-paste the output onto an Excel spreadsheet, select the time and value columns, and insert an **X Y (Scatter)** chart. Be sure to use this chart type, not a Line chart which won't use the timestamps from the data. 47 | 48 | Once plotted, visually inspect the graph for any non-smooth looking behaviour. As an example that may not be an obvious jitter problem at first, in the left plot below, there is a long frame around 0.5s, but an incorrect dt is being used and a discontinuity appears in the plot as a step. The right hand side shows the correct result - the trajectory is continuous and smooth despite the long frame. 49 | 50 | ![PlotData](https://raw.githubusercontent.com/huwb/jitternator/master/img/plot_data.png) 51 | 52 | If there are multiple things moving together (such as a vehicle and a camera), make sure they both are smooth like the good graph above, and that they both take the jump across the long frame together at the same time. 53 | 54 | 55 | ## Update Analysis 56 | 57 | A good way to build a picture of a game update is to put breakpoints in all the update functions for major parts of the engine, as well as a breakpoint in the highest level game loop, and check the order in which updates get called. Some good suggestions for things to breakpoint include physics update, normal actor update, camera update, blueprint update, animation system update, etc (when learning a codebase I like to step through all of he codebase and generated an indented bullet list of all the major update steps in the order they happen). 58 | 59 | To analyse the flow of data during the update, write down each major component of the update in order in a list (left hand side of diagram below). Next to the list draw two vertical lines, one for the start frame time, one for the end frame time. Draw left-to-right arrows which depicts how each component updates (gray below). Now draw arrows (red) for all dependencies - when one component reads data from another. 60 | 61 | ![UpdateAnalysisBad](https://raw.githubusercontent.com/huwb/jitternator/master/img/update_analysis_bad.png) 62 | 63 | The above engine evaluates animations at the end frame time first, then updates physics, then all gameplay code, and finally starts the render process. There are a few issues here, that will be addressed afterwards: 64 | 65 | * The physics update is pulling animation data at the end frame time. If the animated value is a target position for PD control of a rigidbody (i.e. use a damped spring to match a position and velocity), this will jitter. 66 | * The physics update is also pulling data from gameplay code like forces that influence the physics at the start frame time only. It is getting stale data in the second physics update, and the fact that this component pulls data from both the start frame time and end frame time has a bad smell. 67 | * The render process takes the latest physics state for rendering. This data has a slightly different time - the timestamp of the latest physics data is greater than the end frame time/shutter time. This will jitter. 68 | 69 | The following update order looks much healthier: 70 | 71 | ![UpdateAnalysisGood](https://raw.githubusercontent.com/huwb/jitternator/master/img/update_analysis_good.png) 72 | 73 | * An interpolator has been added to provide the correct animation data at every point in time. 74 | * The portion of the gameplay code that affects physics has been moved from Update() to PhysicsUpdate()/FixedUpdate(), and gives each physics update step fresh data. 75 | * An interpolator has been added to provide the physics state at the camera shutter time. 76 | 77 | The goal here is to have the right order of update of the components, with each update step for each component taking data for the current time (vertical red arrows on the diagram). 78 | 79 | Ideally, the entire game state would be cached off at the beginning of the frame, and all game components/systems would read from the left line, and then all use the start frame data to move themselves across to the end frame state. 80 | In practice this is usually not the case - typically the physics is ticked near the beginning of the frame, and then everything else in the frame is then taking the end frame state of any physics objects. 81 | This is not necessarily a problem - but whenever one finds arrows drawn from both start frame AND end frame states, this is typically bad news - the system is sampling data from two different points in simulated time and this kind of situation leads to jitter. 82 | 83 | 84 | ## Debugging Experiments / Tests 85 | 86 | There are a number of experiments that can be performed to probe the system and validate each part of the update flow. Often when I'm investigating jitter I find a number of simultaneous issues which conspire to produce complex and confusing behaviour. The below might help to validate each part of the update one at a time. All of these should be performed with an unsteady framerate - as described above. 87 | 88 | 1. Check debug drawing works. At the end of the camera update, draw a debug sphere somewhere in front of the final camera viewpoint, perhaps in the lower half of the screen. Run the game - the sphere should be absolutely, 100% stable. If not, investigate further. Perhaps the debug draw command is jumping the render queue - try drawing the sphere in front of the camera a frame or two later. Debug draw is invaluable for solving jitter issues - don't proceed further until you have a stable sphere at unsteady FPS. 89 | 2. Test the camera can move at a constant velocity. Using a simple bit of code in the game update, move a debug cube through the world. Use a constant velocity vector and integrate it onto a position each frame by multiplying by the total frame dt. Now run the game and follow the cube with the in game camera. Is the cube jitter free? If it is not, something is wrong with the camera update - it's not updating to the shutter time/render time/end frame time. Strip away layers/behaviours from the camera until it becomes very simple. Is it taking the correct dt? If so, then perhaps some input to the camera update is at the wrong time. The following points test whether the character/actor is updating properly, and whether values coming from the animation system have the correct timestamps. 90 | 3. Knock out all of the camera code, then add a few lines that move the camera programmatically at a fixed velocity (similar to how the cube was moved in item 2). This will put the camera at a known correct end-frame position ready for rendering. Now move the character/vehicle/etc in front of the camera, while the camera moving at constant speed. Does the character appear to jitter? If so, it is not being updated to its correct end frame position. Perhaps it is updating with the wrong dt, or the renderer is not taking its final position for some reason - perhaps draw a debug sphere at the characters position to verify it is not a problem with the rendering of the character. If the character is physics-driven (its position is updated during physics update), then it will be on a different update type (fixed steps) compared to the frame update and the state (pos, vel, orient) and it will need an interpolator as was done above. See Misc Notes below for more on physics related jitter. 91 | 4. The hacked camera on a rail from the previous item can be used to verify the animation system is producing values at the correct time. Animate a cube nearby the camera using keyframes. The cube should appear frame-perfect without jitter. If the cube appears to jitter, the animation is not being evaluated at the camera shutter time, which means something is broken. 92 | 93 | Each of the above tests have revealed bugs for me in the past. 94 | 95 | 96 | ## Adding Timestamps To Data 97 | 98 | After seeing the damage that mixing data from different times causes, I had the idea to associate a time with each bit of data in the simulation, and then to check consistency when data is manipulated to automatically detect many of the issues described above. This idea was inspired somewhat by the data ownership patterns that *Rust* enforces. There is a prototype in this repos which tests this concept, albeit in a hacky way (and floats only for now). The format is a VS2015 project. 99 | 100 | The results were interesting - I mocked up some update code to test it out and it detected issues where the update flow was not strictly correct and forces the developer to either fix the issue, or explicitly acknowledge and workaround these issues in the code (the system must be intentionally overridden). 101 | 102 | As a simple example of the kinds of issues this will catch, I mocked up some code below to compute an acceleration to integrate onto a velocity, which in turn is integrated onto a position, as follows: 103 | 104 | ```cpp 105 | // compute acceleration by comparing a target position with the current position 106 | FloatTime accel = targetPos - pos; 107 | 108 | // move state of vel forward in time 109 | vel.Integrate( accel, dt ); 110 | 111 | // move state of pos forward in time 112 | pos.Integrate( vel, dt ); 113 | ``` 114 | 115 | This is wrong because the position and velocity should both be updated from the start frame state - it's wrong to update vel and then used the updated vel to update pos: 116 | 117 | ![UpdateAnalysisVelPosBad1](https://raw.githubusercontent.com/huwb/jitternator/master/img/update_analysis_velpos_bad1.png) 118 | 119 | Both should be updated purely from the start frame state. In this case, the consistency checking throws an error and it can be seen that the data timestamps don't match in the pos.Integrate() line. I tried to switch the order of the pos and vel integration as follows, which I felt confident would fix it: 120 | 121 | ```cpp 122 | // move state of pos forward in time 123 | pos.Integrate( vel, dt ); 124 | 125 | // compute acceleration by comparing a target position with the current position 126 | FloatTime accel = targetPos - pos; 127 | 128 | // move state of vel forward in time 129 | vel.Integrate( accel, dt ); 130 | ``` 131 | 132 | However this also throws an error because the accel computation is now using the end frame position, instead of taking consistent, start frame state. Diagram: 133 | 134 | ![UpdateAnalysisVelPosBad2](https://raw.githubusercontent.com/huwb/jitternator/master/img/update_analysis_velpos_bad2.png) 135 | 136 | The real fix is this: 137 | 138 | ```cpp 139 | // compute acceleration by comparing a target position with the current position 140 | FloatTime accel = targetPos - pos; 141 | 142 | // move state of pos forward in time 143 | pos.Integrate( vel, dt ); 144 | 145 | // move state of vel forward in time 146 | vel.Integrate( accel, dt ); 147 | ``` 148 | 149 | Now the accel has a start frame timestamp because both targetPos and pos are at start frame values, and both pos and vel update from start frame value to end frame value. Final update diagram: 150 | 151 | ![UpdateAnalysisVelPosGood](https://raw.githubusercontent.com/huwb/jitternator/master/img/update_analysis_velpos_good.png) 152 | 153 | This final fix is unintuitive and surprised me, and is *really* easy to accidentally get wrong in practice. 154 | 155 | Implementing this into a C++ engine would be super-invasive and does not feel practical, at least in its current form. It might be an easier fit to other languages though. 156 | 157 | Such validation could probably be built into the compiler and could be an interesting direction for future work. 158 | 159 | 160 | ## Misc Notes 161 | 162 | * As detailed above, anything on fixed update/physics update will jitter if its position is not interpolated at the end. Some engines will update the physics past the camera shutter time, so that the state at the shutter time can be interpolated and used for rendering, as I propose above in the *Update Analysis* section. 163 | * Unity specific behaviour: rigidbodies are updated to within one fixed timestep of the target time (not past the target time), and then: 164 | * If the *interpolation* property is set to *Extrapolate*, the rigidbody state is extrapolated to *shutter time* for rendering. Such linear extrapolation may be wrong under high accelerations and which may produce jitter (I have not encountered this though). 165 | * If the property is set to *Interpolate*, the render state will then be interpolated to *shutter time* - *fixed delta time*. This produces a jitter-free result because the render state is at a *constant* offset back in time. The side effect though is that it will render slightly behind its actual end frame state, due to the offset. 166 | * There is a nice article about physics-related jitter by Gaffer On Games: https://gafferongames.com/post/fix_your_timestep/ . Note that this implements the equivalent of the Unity Interpolate mode described in the previous note, and therefore interpolates the physics to *target time* minus *fixed dt*. 167 | * Rigidbodies should be controlled using forces (you can prescribe a target position and velocity using PD control which essentially a damped spring) instead of placing them directly using their transform component, unless the rigidbody is set to be kinematic. I've seen jitter from a dynamic rigidbody being placed manually each frame in a position that intersects static collision or other rigidbodies. Dumping out collision pairs from the physics engine can help to identify this. 168 | * If you have known update dependencies between bits of code, **do** explicitly set the update order (*Script Execution Order* in Unity, *tick groups* in UE4), and put comments next to each update function recording the dependency, as Future You or someone else may move the update code elsewhere later on. Or even better, document the update flow, something as simple as an indented list works well IMO (see [example](https://github.com/huwb/crest-oceanrender/blob/master/README.md#update-order)). 169 | --------------------------------------------------------------------------------