Shaking GameObject, moving on x-axis in endless runner style game

Hi all,

I’ve got an issue with a character in an Endless Runner style game, from a tutorial I found on YouTube.
(Tutorial here: Subway Skater - YouTube )


The player ‘jitters’ (see video) when it’s not moved into the correct lane absolutely, it’s immediately clear on iOS but I have noticed it occur on Android.

I’ve updated the Player Position into UI text objects to show the desired lane move changes, (x,y and z respectively).

The player (with a character controller attached) shakes when the player moves and doesn’t reach the perfect x-axis values. -2.5, 0 or 2.5 for left, middle or right lanes.

Video (filmed on an iPhone 6, apologies for quality.)
Note the X values changing rapidly when the player is shaking.


The jitter does stop and it often helped by moving along the Y-axis ( Jumping / Sliding in the context of the game).

Has anyone else experienced this issue? Found an elegant solution for it?
I’ve taken a look at the following issues, but nothing I’ve tried has worked.
https://forum.unity.com/threads/how-to-smooth-character-movement-that-is-jittery.322417/
https://forum.unity.com/threads/charactercontroller-movement-jittery.40196/


I’ve seen that some people experience jitters far from the origin, but this occurs close to Zero, so I’ve discounted that idea.
I’ve tried updating the Updates to LateUpdate / FixedUpdate.
I have changed the “Min Move Distance” on the Character Controller to no avail.

I think the key is fixing the issue is either reaching the desired lane value with a different type of Move on the Character Controller or giving it a margin or error, any ideas on how to achieve this would be great.

I’ve also attached my code for the playerMotor below.

Unity Version: 2019.1.0f2

PlayerMotor.cs:


    using UnityEngine;
    using UnityEngine.UI;
     
    public class PlayerMotor : MonoBehaviour
    {
        // PARAMETERS
        private const float LANE_DISTANCE = 2.5f;
        private const float TURN_SPEED = 0.05f;
     
        // BOOL
        private bool isRunning = false;
     
        // ANIMATION
        private Animator anim;
     
        // MOVEMENT
        private CharacterController controller;
        private float jumpForce = 4.0f;
        private float gravity = 12.0f;
        private float verticalVelocity;
     
        // SPEED
        private float originalSpeed = 7.0f;
        private float speed;
        private float speedIncreaseLastTick;
        private float speedIncreaseTime = 2.5f;
        private float speedIncreaseAmount = 0.1f;
     
        // DEBUG
        public Text xpos, ypos, zpos;
     
        // LANE FOR PLAYER
        private int desiredLane = 1; // 0 = Left, 1 = Middle, 2 = Right
     
        private void Start()
        {
            speed = originalSpeed;
            controller = GetComponent<CharacterController>();
            anim = GetComponent<Animator>();
        }
     
        private void Update()
        {
            // CHECK FOR BOOLEAN IN GAME MANAGER
            if (!isRunning)
                return;
     
            if(Time.time - speedIncreaseLastTick > speedIncreaseTime){
                speedIncreaseLastTick = Time.time;
                speed += speedIncreaseAmount;
                // CHANGE THE MODIFIER TEXT
                GameManager.Instance.UpdateModifier(speed - originalSpeed);
            }
     
            if (MobileInput.Instance.SwipeLeft)
            {
                MoveLane(false);
            }
     
            if (MobileInput.Instance.SwipeRight)
            {
                MoveLane(true);
            }
     
            // Calculate where we should be next
            Vector3 targetPosition = transform.position.z * Vector3.forward;
     
            // SWITCH LANES
            if (desiredLane == 0)
            {
                targetPosition += Vector3.left * LANE_DISTANCE;
                //Debug.Log(targetPosition);
            }
            else if (desiredLane == 2)
            {
                targetPosition += Vector3.right * LANE_DISTANCE;
                //Debug.Log(targetPosition);
            }
     
            // THIS DIDN'T FIX IT
            // ===============================
            //switch(desiredLane){
            //    case 0:
            //        targetPosition = Vector3.left * LANE_DISTANCE;
            //        targetPosition = new Vector3(targetPosition.x, targetPosition.y, transform.position.z);
            //        break;
            //    case 1:
            //        targetPosition = Vector3.zero;
            //        targetPosition = new Vector3(targetPosition.x, targetPosition.y, transform.position.z);
            //        break;
            //    case 2:
            //        targetPosition = Vector3.right * LANE_DISTANCE;
            //        targetPosition = new Vector3(targetPosition.x, targetPosition.y, transform.position.z);
            //        break;
            //}
     
            // CALCULATE THE MOVE DELTA
            Vector3 moveVector = Vector3.zero;
     
            moveVector.x = (targetPosition - transform.position).normalized.x * speed;
     
            // DEVELOPMENT
            Vector3 playerPosition = this.transform.position;
            xpos.text = playerPosition.x.ToString();
            ypos.text = playerPosition.y.ToString();
            zpos.text = playerPosition.z.ToString();
     
            // CALL THE FUNCTION HERE AND ASSIGN TO VARIABLE SO WE'RE NOT CALCULATING TWICE
            bool isGrounded = IsGrounded();
            anim.SetBool("Grounded", isGrounded);
               
            // CALCULATE GRAVITY
            // IS CHARACTER ON THE GROUND
            if (isGrounded)
            {
                verticalVelocity = -0.1f;
                // IF PLAYER JUMPS
                //if (Input.GetKeyDown(KeyCode.Space) || MobileInput.Instance.SwipeUp)
                if (MobileInput.Instance.SwipeUp)
                {
                    anim.SetTrigger("Jump");
                    verticalVelocity = jumpForce;
                } else if (MobileInput.Instance.SwipeDown){
                    // SLIDE
                    StartSliding();
                    Invoke("StopSliding", 1.0f);
                }
            }
            else
            {
                // ALWAYS PULL THE CHARACTER DOWN
                verticalVelocity -= (gravity * Time.deltaTime);
     
                // FAST FALLING
                //if (Input.GetKeyDown(KeyCode.Space) || MobileInput.Instance.SwipeDown)
                if (MobileInput.Instance.SwipeDown)
                {
                    verticalVelocity = -jumpForce;
                }
            }
     
            moveVector.y = verticalVelocity;
     
            moveVector.z = speed;
     
            // USE THE MOVE DELTA AND APPLY TO CONTROLLER
            // NEED TO MOVE THE PLAYER TO MOVE VECTOR WITHIN MARGIN OF ERROR?!
            controller.Move(moveVector * Time.deltaTime);
     
            // ROTATE THE PLAYER TO WHERE HE IS GOING
            Vector3 dir = controller.velocity;
     
            if (dir != Vector3.zero)
            {
                dir.y = 0;
                transform.forward = Vector3.Lerp(transform.forward, dir, TURN_SPEED);
            }
     
        }
     
        private void MoveLane(bool goingRight)
        {
            // DESIRED LANE TO MOVE TO
            desiredLane += (goingRight) ? 1 : -1;
            desiredLane = Mathf.Clamp(desiredLane, 0, 2);
        }
     
        private bool IsGrounded()
        {
            Ray groundRay = new Ray(new Vector3(controller.bounds.center.x,
                                                (controller.bounds.center.y - controller.bounds.extents.y) + 0.2f,
                                                controller.bounds.center.z), Vector3.down);
            Debug.DrawRay(groundRay.origin, groundRay.direction, Color.cyan, 1.0f);
     
            return (Physics.Raycast(groundRay, 0.2f + 0.1f));
     
        }
     
        public void StartRunning(){
            isRunning = true;
            anim.SetTrigger("StartRunning");
        }
     
        private void StartSliding(){
            anim.SetBool("Sliding", true);
            controller.height /= 2;
            controller.center = new Vector3(controller.center.x, controller.center.y / 2, controller.center.z);
        }
     
        private void StopSliding(){
            anim.SetBool("Sliding", false);
            controller.height *= 2;
            controller.center = new Vector3(controller.center.x, controller.center.y * 2, controller.center.z);
        }
     
        private void OnControllerColliderHit(ControllerColliderHit hit)
        {
            switch(hit.gameObject.tag){
                case "Obstacle":
                    Crash();
                break;
            }
        }
     
        private void Crash(){
            // PLAYER DIES
            anim.SetTrigger("Death");
            isRunning = false;
            GameManager.Instance.OnDeath();
        }
     
    }

Did you find a solution for this?

Hi @TomGa83 - I’m afraid I had to rewrite a lot of this project - once the X values became large enough this issue persisted.
If you think of something like Temple Run - you’re always turning and going back on yourself (so the values remain small).
Changing my Update to LateUpdate helped too - but ultimately I couldn’t avoid the issue and so had to rewrite. Sorry I couldn’t be more help.