Character Jitters During Movement in Endless Runner with cinemachine

Hi there so recently i have encountered an issue where the character jitters when move the character, since this game is an endless runner, the character is moving forward or backward, just left, right, up and down.

So for the camera follow that i am currently using now is Cinemachine Virtual Camera. I have tried almost every solution that i could find on the web and unity forums, which includes ~

  • Ensuring my cinemachine camera and player movement is in a one consistent update method, both of them are in fixed update
  • Used Interpolate in my rigidbody
  • Using rb.MovePosition instead of using transform.position
  • I have also checked with 0 damping to see whether it is a update issue but with 0 damping the character will still jitter.

Here is an example video of the jittery -------

Here is the player movement script ------

using UnityEngine;
using System;
using System.Collections;

public enum SIDE { Left, Mid, Right }
public enum HitX { Left, Mid, Right, None }
public enum HitY { Up, Mid, Down, None }
public enum HitZ { Forward, Mid, Backward, None }

public class Player : MonoBehaviour
{
    [Header("Instance")]
    public static Player get;

    [Header("Movement")]
    public float moveSpeed = 10f;
    public float XValue = 2.5f;
    public float speedDodge = 10f;
    private float NewXPos = 0;
    private float x;

    [Header("Jump Settings")]
    public float jumpForce = 7f;
    private Rigidbody rb;
    public bool isGrounded;
    public bool inJump;

    [SerializeField] private float gravityMultiplier = 2.5f;
    [SerializeField] private float jumpBufferTime = 0.2f;
    [SerializeField] private float coyoteTime = 0.1f;
    private float lastGroundedTime;
    private float lastJumpTime;

    [Header("Roll Settings")]
    public bool InRoll;
    private float rollDuration = 0.7f;
    private float rollCounter;
    private CapsuleCollider playerCol;
    private float originalColliderHeight;
    private float originalColliderCenter;

    [Header("Animation")]
    private Animator anim;
    public bool dontPlay;

    private void Awake()
    {
        get = this;
    }

    private void Start()
    {
        rb = GetComponent<Rigidbody>();
        anim = GetComponentInChildren<Animator>();
        playerCol = GetComponent<CapsuleCollider>();

        if (playerCol != null)
        {
            originalColliderHeight = playerCol.height;
            originalColliderCenter = playerCol.center.y;
        }

        x = 0;
        NewXPos = 0;
        dontPlay = false;
    }

    private void Update()
    {
        HandleMobileInput();

        if (isGrounded)
        {
            lastGroundedTime = Time.time;
        }

        CheckRoll();
    }

    private void HandleMobileInput()
    {
        if (Input.touchCount > 0)
        {
            Touch touch = Input.GetTouch(0);

            if (touch.phase == TouchPhase.Began)
            {
                swipeStart = touch.position;
            }
            else if (touch.phase == TouchPhase.Ended)
            {
                swipeEnd = touch.position;
                HandleSwipe();
            }
        }
    }

    private Vector2 swipeStart, swipeEnd;
    private void HandleSwipe()
    {
        float swipeX = swipeEnd.x - swipeStart.x;
        float swipeY = swipeEnd.y - swipeStart.y;

        if (Mathf.Abs(swipeX) > Mathf.Abs(swipeY))  // Horizontal swipe
        {
            if (Mathf.Abs(swipeX) > 50f)
            {
                if (swipeX > 0 && NewXPos < XValue)
                {
                    NewXPos += XValue;
                    if (!dontPlay && anim != null) anim.Play("dR");
                }
                else if (swipeX < 0 && NewXPos > -XValue)
                {
                    NewXPos -= XValue;
                    if (!dontPlay && anim != null) anim.Play("dL");
                }
            }
        }
        else // Vertical swipe
        {
            if (swipeY > 50f) // Swipe up - Jump
            {
                lastJumpTime = Time.time;
            }
            else if (swipeY < -50f && isGrounded) // Swipe down - Roll
            {
                Roll();
            }
        }
    }

    private bool CanJump()
    {
        return Time.time - lastGroundedTime <= coyoteTime;
    }

    private void Jump()
    {
        if (CanJump() && !inJump && !InRoll)
        {
            float calculatedJumpForce = Mathf.Sqrt(jumpForce * -2f * Physics.gravity.y);
            Debug.Log("Jump Force Applied: " + calculatedJumpForce);

            rb.AddForce(Vector3.up * calculatedJumpForce, ForceMode.Impulse);

            inJump = true;
            isGrounded = false;  // Prevent multiple jumps
            lastJumpTime = Time.time; // Store jump time
            anim.SetBool("Run", false);
            if (anim != null) anim.Play("Jump");
        }
    }



    private void ApplyBetterGravity()
    {
        if (!isGrounded && rb.velocity.y < 0)  // Apply only when falling
        {
            rb.velocity += Vector3.up * Physics.gravity.y * (gravityMultiplier - 1) * Time.deltaTime;
        }
    }



    private void FixedUpdate()
    {
        Vector3 targetPosition = new Vector3(
            Mathf.Lerp(rb.position.x, NewXPos, Time.fixedDeltaTime * speedDodge),
            rb.position.y,
            rb.position.z
        );
        rb.MovePosition(targetPosition);

        if (Time.time - lastJumpTime <= jumpBufferTime && CanJump())
        {
            Jump();
        }
        ApplyBetterGravity();
        CheckGroundStatus(); // More accurate ground detection
    }

    private void CheckGroundStatus()
    {
        float groundCheckDistance = 0.8f; // Reduce distance for better accuracy
        Vector3 origin = transform.position + Vector3.up * 0.5f; // Start slightly above feet
        LayerMask groundLayer = LayerMask.GetMask("Ground");

        Debug.DrawRay(origin, Vector3.down * groundCheckDistance, Color.green, 2);

        bool wasGrounded = isGrounded;
        isGrounded = Physics.Raycast(origin, Vector3.down, out RaycastHit hit, groundCheckDistance, groundLayer);

        if (isGrounded)
        {
            if (!wasGrounded) // Just landed
            {
                Debug.Log("Landed on: " + hit.collider.gameObject.name);
                inJump = false;  // Reset jump state
            }

            anim.SetBool("Run", true);
        }
        else
        {
            Debug.Log("No Ground Detected!");
        }
    }





    private void Roll()
    {
        if (!InRoll && isGrounded)
        {
            InRoll = true;
            rollCounter = rollDuration;

            if (playerCol != null)
            {
                playerCol.height = originalColliderHeight / 2f;
                playerCol.center = new Vector3(0, originalColliderCenter / 2f, 0);
            }

            if (anim != null) anim.Play("Roll");
        }
    }

    private void CheckRoll()
    {
        if (rollCounter > 0)
        {
            rollCounter -= Time.deltaTime;
            if (rollCounter <= 0)
            {
                if (playerCol != null)
                {
                    playerCol.height = originalColliderHeight;
                    playerCol.center = new Vector3(0, originalColliderCenter, 0);
                }
                InRoll = false;
            }
        }
    }


    //public void hitByCar()
    //{
    //    deathAudioSource.PlayOneShot(deathAudioSource.clip);
    //}

    //public void CollectCoin()
    //{
    //    lastCoinTime = Time.time;
    //    currentPitch = Mathf.Clamp(currentPitch + pitchIncrement, 1.0f, maxPitch);
    //    coinAudioSource.pitch = currentPitch;
    //    coinAudioSource.PlayOneShot(coinAudioSource.clip);
    //    m_Coins += addAmount * tm.multiplier;
    //}

    //private void calculateTime()
    //{
    //    if (Time.time - lastCoinTime > pitchResetDelay && currentPitch > 1.0f)
    //    {
    //        currentPitch = Mathf.Lerp(currentPitch, 1.0f, Time.deltaTime * 2.0f);
    //        coinAudioSource.pitch = currentPitch;
    //    }
    //}
}

So i am all out of ideas to fix this issues, any help would be appreciated. Thank you very much!

The jitter is likely due to aliasing between FixedUpdate frames and render frames.

The best way to avoid this is to do all of the following:

  • Enable Interpolation on the Rigidbody
  • Set the CM Brain mode to LateUpdate
  • In your controller code, NEVER use Rigidbody.transform, ALWAYS use the RigidBody API (e.g. MovePosition, position, velocity) to access or modify the position/rotation. Any violation of this will break interpolation.
  • In your controller code, never modify the Rigidbody outside of FixedUpdate

Your code generally looks ok, although I’m a little nervous because your roll handling is done from Update. If you do all of the above and are still getting jitter, try commenting out your roll handling code.

1 Like

Thank you for the response, alrite so i have double check but it seems that i am following all of the suggestions that you have gave but the problem still exists, even with the roll handling commented out. :frowning:

On line 174 you’re using MovePosition from within FixedUpdate on what I assume is a dynamic rigidbody. This will result in movement that isn’t smooth. MovePosition is for moving kinematic rigidbodies. So instead use AddForce or just set the velocity directly if you don’t like inertia.

2 Likes