Unity Rigidbody character jitters with camera follow: Update vs FixedUpdate
Our player character stuttered against the background whenever the camera followed it, worst on 144 Hz monitors. Movement wrote the transform in Update, the camera read physics positions, and interpolation was off. How we measured the jitter and fixed all three.
We were two months from a demo build of a small third-person exploration game. Playtesters kept describing the same thing in different words: "the character vibrates", "the hero stutters when you run", "the world is smooth but the player shakes". It was barely visible on a 60 Hz office monitor and obvious on the 144 Hz and 165 Hz gaming monitors most testers used. There was no error or warning in the Console; the bug was purely visual.
Recording the Game view at 144 fps and stepping through frames showed the pattern: while running at a constant speed, the player moved across the screen in uneven steps (some frames it moved, some it did not, some it moved twice as far) while the terrain scrolled smoothly.
Reproducing it
The jitter appeared every time under these conditions:
- Run the game with VSync on, on a monitor above 60 Hz (or with
Application.targetFrameRate = 144in the editor). - Hold forward so the player runs in a straight line on flat ground.
- Watch the player against the background, or log its screen-space position each frame.
To put a number on it, we logged how far the player moved in screen space per rendered frame:
// Debug only: attach to the camera
using UnityEngine;
public class JitterProbe : MonoBehaviour
{
[SerializeField] Transform target;
Vector3 lastScreenPos;
void LateUpdate()
{
Vector3 sp = GetComponent<Camera>().WorldToScreenPoint(target.position);
Debug.Log($"{Time.frameCount},{(sp - lastScreenPos).magnitude:F2}");
lastScreenPos = sp;
}
}
At a steady run the player should stay in roughly the same screen position frame to frame. Instead the per-frame screen displacement alternated between about 0 and 9 pixels.
What we ruled out
- Animation root motion. Disabling the Animator and moving a capsule gave the same jitter.
- Frame-time spikes. The Profiler showed steady frame times around 6.9 ms with no GC spikes, and the terrain itself scrolled smoothly, so the frames were arriving on time.
- Floating-point precision far from the origin. The test scene was within 200 units of the origin, nowhere near where precision becomes visible.
Root cause
Three things combined. The player had a non-kinematic Rigidbody, but our controller moved it by
writing transform.position in Update, every rendered frame, while the physics engine also moved
it in its own fixed steps from velocity and collisions. The camera follow script read
rigidbody.position, which is the physics-step position and only changes 50 times a second with the
default fixed timestep. And Rigidbody.interpolation was None. With rendering at 144 Hz and
physics at 50 Hz, about two out of three frames showed no physics movement and the third showed a
jump, and the transform writes from Update fought with physics on top of that.
What is happening under the hood
Unity runs two clocks. Update and LateUpdate run once per rendered frame. FixedUpdate and the
physics simulation run on a fixed timestep (Time.fixedDeltaTime, 0.02 s by default), zero, one or
several times per rendered frame depending on how much time has passed.
| Refresh rate | Rendered frames per second | Physics steps per second | Physics steps per frame |
|---|---|---|---|
| 60 Hz | 60 | 50 | 0 or 1, mostly 1 |
| 144 Hz | 144 | 50 | 0 or 1, mostly 0 |
| 165 Hz | 165 | 50 | 0 or 1, mostly 0 |
If something moves only during physics steps and is drawn without interpolation, it moves only on the frames that contain a step. At 144 Hz that is about one frame in three, so motion looks like a staircase. A camera that follows smoothly every frame makes the staircase visible as jitter, because the background moves evenly while the player does not.
Rigidbody interpolation fixes this for rendering: with Interpolate enabled, Unity draws the
body's transform between its last two physics poses, so the visible position changes every frame.
The physics pose (Rigidbody.position) still changes only per step; the interpolated pose is what
you read from transform.position. Unity's documentation also notes that while interpolation is on,
the physics system controls the transform, so writing the transform directly works against it.
Environment
| Component | Version / value |
|---|---|
| Engine | Unity 6.3 LTS (6000.3) |
| Render pipeline | URP |
| Physics | Built-in 3D physics (PhysX), Time.fixedDeltaTime = 0.02 |
| Player | Capsule with non-kinematic Rigidbody, interpolation None |
| Camera | Custom follow script in Update reading Rigidbody.position |
| Target platforms | Windows 11 desktop, VSync on, 60 to 165 Hz monitors |
Minimal reproduction
// PlayerMover.cs (broken)
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class PlayerMover : MonoBehaviour
{
[SerializeField] float speed = 6f;
Rigidbody rb;
void Awake() => rb = GetComponent<Rigidbody>();
void Update()
{
Vector3 input = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
transform.position += input.normalized * speed * Time.deltaTime; // fights the physics engine
}
}
// CameraFollow.cs (broken)
public class CameraFollow : MonoBehaviour
{
[SerializeField] Rigidbody target;
[SerializeField] Vector3 offset = new Vector3(0f, 3f, -6f);
void Update()
{
transform.position = target.position + offset; // physics-step position, no interpolation
transform.LookAt(target.position);
}
}
Who is affected
Any Unity project where a physics body is followed by a camera and either the body is moved outside
FixedUpdate, interpolation is off, or the camera follows the physics position instead of the
rendered transform. It is most visible on high-refresh displays and with fast-moving characters, and
it also affects 2D projects using Rigidbody2D.
The fix
We changed three things: move the body through physics in FixedUpdate, turn on interpolation, and
make the camera follow the interpolated transform in LateUpdate.
1. Read input every frame, apply it in physics steps.
// PlayerMover.cs (fixed)
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class PlayerMover : MonoBehaviour
{
[SerializeField] float speed = 6f;
Rigidbody rb;
Vector3 moveInput;
void Awake()
{
rb = GetComponent<Rigidbody>();
rb.interpolation = RigidbodyInterpolation.Interpolate;
}
void Update()
{
// Input is sampled per rendered frame so no presses are missed.
moveInput = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
moveInput = Vector3.ClampMagnitude(moveInput, 1f);
}
void FixedUpdate()
{
// Keep gravity's vertical component, drive horizontal velocity from input.
Vector3 v = moveInput * speed;
v.y = rb.linearVelocity.y;
rb.linearVelocity = v;
}
}
In Unity 6 the property is Rigidbody.linearVelocity; the older velocity name is obsolete. For a
kinematic body the equivalent is rb.MovePosition(rb.position + delta) in FixedUpdate, which also
respects interpolation.
2. Interpolation on the Rigidbody. Set in code above, and in the prefab's Rigidbody component as Interpolate so designers see the same behaviour in the Inspector.
3. Camera follows the rendered transform after everything has moved.
// CameraFollow.cs (fixed)
using UnityEngine;
public class CameraFollow : MonoBehaviour
{
[SerializeField] Transform target; // the player's Transform, not its Rigidbody
[SerializeField] Vector3 offset = new Vector3(0f, 3f, -6f);
[SerializeField] float smoothTime = 0.08f;
Vector3 velocity;
void LateUpdate()
{
Vector3 desired = target.position + offset; // interpolated pose
transform.position = Vector3.SmoothDamp(transform.position, desired, ref velocity, smoothTime);
transform.LookAt(target.position + Vector3.up * 1.2f);
}
}
LateUpdate runs after all Update calls and after the interpolated transforms for the frame are
set, so the camera always sees the pose that will be rendered.
Alternatives and trade-offs
- Cinemachine. We later replaced the custom follow script with a Cinemachine camera. The Cinemachine Brain's Smart Update method (the recommended default) updates each camera according to how its target moves, which handles interpolated Rigidbody targets well. If you use Cinemachine, keep Rigidbody interpolation on; the camera cannot smooth a target that only moves in steps.
- Raising the physics rate. Setting
Time.fixedDeltaTimeto 1/144 hides the jitter on 144 Hz monitors at a large CPU cost, and breaks again on 165 Hz or 240 Hz displays. - Extrapolate instead of Interpolate. It removes the one-step visual lag but overshoots when velocity changes, which looked wrong for a character that stops and turns often.
- A kinematic character controller. Unity's
CharacterControllermoved inUpdateavoids the two-clock problem entirely, at the cost of handling physics interactions yourself.
How to test the fix
We repeated the JitterProbe measurement on the same 144 Hz setup and saved the CSV:
# Player.log from a development build; parse probe lines and print the stddev of per-frame movement
grep -E '^[0-9]+,[0-9.]+$' Player.log | awk -F, '{s+=$2; ss+=$2*$2; n++} END {m=s/n; print "mean", m, "stddev", sqrt(ss/n-m*m)}'
Before the fix the standard deviation of per-frame screen movement was 4.1 px at a steady run; after it, 0.3 px. We also added a PlayMode test that runs the player forward for two seconds at a forced frame rate and checks that the transform moves on every rendered frame:
// PlayMode test excerpt: playerPrefab is a serialized test field, SetTestInput a test-only hook
[UnityTest]
public IEnumerator PlayerTransformMovesEveryFrame()
{
Application.targetFrameRate = 144;
var player = Object.Instantiate(playerPrefab).GetComponent<PlayerMover>();
player.SetTestInput(Vector3.forward);
yield return new WaitForSeconds(0.5f); // reach steady speed
Vector3 last = player.transform.position;
int stalledFrames = 0;
for (int i = 0; i < 144; i++)
{
yield return null;
if ((player.transform.position - last).sqrMagnitude < 1e-8f) stalledFrames++;
last = player.transform.position;
}
Assert.Less(stalledFrames, 3);
}
Confirming in production
- In the next playtest round, 0 of 14 testers mentioned stutter, against 9 of 12 in the previous round. Most of them played on 144 Hz or 165 Hz monitors.
- Frame-stepped recordings at 144 fps now show the player at a stable screen position while running.
- The Profiler showed no measurable change in physics time; interpolation is cheap for a single character.
Regression guard
- The PlayMode test above runs in CI for every change to the player or camera prefabs.
- A Roslyn analyzer rule in our project flags assignments to
transform.positionortransform.rotationin any class that also has aRigidbodyfield, pointing to the movement guidelines. - Our prefab checklist requires Interpolate on any Rigidbody that the camera follows.
Security implications
None. This is a rendering and simulation timing issue with no effect on data, networking or user input handling. In a networked game, the same principle (simulate on a fixed tick, render interpolated state) applies, and getting it wrong there also affects fairness, but that was outside this project.
Prevention checklist
- Move Rigidbodies only through physics APIs, in
FixedUpdate. - Read input in
Update, store it, and apply it inFixedUpdate. - Enable interpolation on any Rigidbody the player watches closely.
- Have cameras follow
transform.positioninLateUpdate, or use Cinemachine's Smart Update. - Test on high-refresh monitors, not just at 60 Hz.
References
- Unity Manual (6.3): Rigidbody interpolation — https://docs.unity3d.com/6000.3/Documentation/Manual/rigidbody-interpolation.html
- Unity Scripting API (6.3):
Rigidbody.interpolation— https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Rigidbody-interpolation.html - Unity Scripting API (6.3):
Rigidbody.MovePosition— https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Rigidbody.MovePosition.html - Unity Scripting API (6.3):
MonoBehaviour.FixedUpdate— https://docs.unity3d.com/6000.3/Documentation/ScriptReference/MonoBehaviour.FixedUpdate.html - Unity Manual (6.3): Order of execution for event functions — https://docs.unity3d.com/6000.3/Documentation/Manual/execution-order.html
- Cinemachine 3.1: Cinemachine Brain component — https://docs.unity3d.com/Packages/com.unity.cinemachine@3.1/manual/CinemachineBrain.html
Add a label
Embed this post
<iframe src="https://hibug.me/unity-character-jitter-update-fixedupdate" width="700" height="420" frameborder="0" loading="lazy"></iframe>
I have spent most of my career on the side of software that pages people at night: payment integrations, web platforms with real traffic, CI/CD pipelines, Kubernetes clusters and the databases underneath them. Somewhere along the way I started keeping a notebook of every bug that cost more than an hour — the exact error, what we ruled out, the real root cause and how we proved the fix. Hibug is where those notes live. Every post follows the same path: Investigate, Explain, Fix, Verify — with the environment spelled out, the fix tested, and the security angle checked.
Related Stories
Canary releases with automated analysis: what to measure and when to abort
Comments
0No comments yet. Be the first to comment!
Sign In to post a comment.