Custom FPS Character Controller Doesn't Move initially

So I’ve made a custom character controller that I’ve used in a few projects. The first time I made it, it seems to work fine in that project.

However now, once I set it up and use just the parts I need, It seems like I can’t move at all until I click out of the game window/debug player onto an object in the scene hierarchy, then click back in, and suddenly i can move.

Currently, If I crouch first, then movement starts working.

Other times it does just work fine, but another run it might break again. I can’t pin-point why this is happening.

I’ve been trying to debug but it seems like all values are how they should be, such as my ground check and state management. The player object is definitely above any ground colliders too.

movementBugGif

My script is quite long, so I’ve ommited global variables. Just assume any that aren’t declared in the code exist.


... 
public class PlayerController : MonoBehaviour
{
   //fields/variables declared here

    private void Awake()
    {
        playerInput = GetComponent<PlayerInput>();
        moveAction = playerInput.actions.FindAction("Move");
        lookAction = playerInput.actions.FindAction("Look");
        jumpAction = playerInput.actions.FindAction("Jump");
        sprintAction = playerInput.actions.FindAction("Sprint");
        crouchAction = playerInput.actions.FindAction("Crouch");
        grabAction = playerInput.actions.FindAction("Grab");
        shiftTimeAction = playerInput.actions.FindAction("ShiftTime");


        rb = GetComponent<Rigidbody>();

        playerCollider = playerObject.GetComponent<CapsuleCollider>();

        playerAudioSource = GetComponent<AudioSource>();

        rb.isKinematic = true;
    }

    private void OnEnable()
    {
        moveAction.Enable();
        lookAction.Enable();
        jumpAction.started += Jump;
        sprintAction.started += Sprint;
        sprintAction.canceled += EndSprint;
        crouchAction.started += Crouch;
        crouchAction.canceled += Standup;
        grabAction.started += StartGrab;
        grabAction.canceled += EndGrab;
        shiftTimeAction.started += ShiftTime;
    }

    private void OnDisable()
    {
        moveAction.Disable();
        lookAction.Disable();
        jumpAction.started -= Jump;
        sprintAction.started -= Sprint;
        sprintAction.canceled -= EndSprint;
        crouchAction.started -= Crouch;
        crouchAction.canceled -= Standup;
        grabAction.started -= StartGrab;
        grabAction.canceled -= EndGrab;
        shiftTimeAction.started -= ShiftTime;
    }

    private void Start()
    {
        readyToJump = true;

        startYScale = playerObject.localScale.y;
        camStartPos = playerCamera.transform.position;

        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
    }


    private void Update()
    {
        if(!isFrozen)
        {
            MoveCamera();

            SpeedControl();
            StateHandler();

            stepTimer += Time.deltaTime;
        }
    }

    private void FixedUpdate()
    {
        if (!isFrozen)
        {
            // ground check
            RaycastHit groundHit;
            grounded = Physics.Raycast(transform.position, Vector3.down, out groundHit, playerHeight + 0.5f, whatIsGround);

            if (state != MovementState.air && !isInWater)
            {
                currentPhysicsMaterial = groundHit.collider != null ? groundHit.collider.sharedMaterial : null;
            }
            else
            {
                currentPhysicsMaterial = null;
            }

            // handle drag
            if (grounded)
                rb.linearDamping = groundDrag;
            else
                rb.linearDamping = 0;

            MovePlayer();

            if (enableFoosteps)
            {
                CalculateFootsteps();
            }
            

            if (isGrabbing)
            {
                currentObjectVelocity = currentObject.linearVelocity;
            }      

        }
    }

    private void MoveCamera()
    {
        if (!isFrozen)
        {
            Vector3 oldCamRotation = playerCamera.transform.rotation.eulerAngles;
            lookInput = lookAction.ReadValue<Vector2>();

            float mouseX = lookInput.x / 10 * sensX;
            float mouseY = lookInput.y / 10 * sensY;

            yRotation += mouseX;
            xRotation -= mouseY;
            xRotation = Mathf.Clamp(xRotation, -90f, 90f);

            // rotate cam and orientation
            playerCamera.transform.rotation = Quaternion.Euler(xRotation, yRotation, 0);
            orientation.rotation = Quaternion.Euler(0, yRotation, 0);

            Vector3 newCamRotation = playerCamera.transform.rotation.eulerAngles;
            camVelocity = newCamRotation - oldCamRotation;

            float playerHeadPos = playerCollider.bounds.max.y;
            Vector3 camPos = transform.position;
            camPos.y = playerHeadPos;
            playerCamera.transform.position = camPos;

        }
    }

    private void StateHandler()
    {
        //Mode - Crouching
        if (isCrouching)
        {
            state = MovementState.crouching;
            moveSpeed = crouchSpeed;
        }

        //Mode - Sprinting
        else if (grounded && isSprinting)
        {
            state = MovementState.sprinting;
            moveSpeed = sprintSpeed;
        }

        //Mode - Walking
        else if (grounded && _2DVelocity.sqrMagnitude > 0)
        {
            state = MovementState.walking;
            moveSpeed = walkSpeed;
        }

        //Mode - Air
        else if(grounded && _2DVelocity.sqrMagnitude <= 0)
        {
            state = MovementState.idle;
        }
        else
        {
            state = MovementState.air;
        }
    }

    private void MovePlayer()
    {
        rb.isKinematic = false;

        // calculate movement direction
        moveActionInput = moveAction.ReadValue<Vector2>();
        moveDirection = orientation.forward * moveActionInput.y + orientation.right * moveActionInput.x;

        isIdle = moveDirection.normalized.magnitude <= 0;
        _2DVelocity = Vector2.right * rb.linearVelocity.x + Vector2.up * rb.linearVelocity.z;
        speedToVelocityRatio = (Mathf.Lerp(0, 2, Mathf.InverseLerp(0, (sprintSpeed / 50), _2DVelocity.magnitude)));
        _2DVelocityMag = Mathf.Clamp((walkSpeed / 50) / _2DVelocity.magnitude, 0f, 2f);

        // on slope
        if (OnSlope() && !exitingSlope)
        {
            rb.AddForce(GetSlopeMoveDirection() * moveSpeed * 20f, ForceMode.Force);

            if (rb.linearVelocity.y > 0)
                rb.AddForce(Vector3.down * 80f, ForceMode.Force);
        }

        // on ground
        else if (grounded)
            rb.AddForce(moveDirection.normalized * moveSpeed * 10f, ForceMode.Force);

        // in air
        else if (!grounded)
            rb.AddForce(moveDirection.normalized * moveSpeed * 10f * airMultiplier, ForceMode.Force);

        // turn gravity off while on slope
        rb.useGravity = !OnSlope();
    }

    private void SpeedControl()
    {
        // limiting speed on slope
        if (OnSlope() && !exitingSlope)
        {
            if (rb.linearVelocity.magnitude > moveSpeed)
                rb.linearVelocity = rb.linearVelocity.normalized * moveSpeed;
        }

        // limiting speed on ground or in air
        else
        {
            Vector3 flatVel = new Vector3(rb.linearVelocity.x, 0f, rb.linearVelocity.z);

            // limit velocity if needed
            if (flatVel.magnitude > moveSpeed)
            {
                Vector3 limitedVel = flatVel.normalized * moveSpeed;
                rb.linearVelocity = new Vector3(limitedVel.x, rb.linearVelocity.y, limitedVel.z);
            }
        }
    }

    private void Sprint(InputAction.CallbackContext context)
    {
        if (grounded)
        {
            isSprinting = true;
            state = MovementState.sprinting;
            moveSpeed = sprintSpeed;
        }

    }

    private void EndSprint(InputAction.CallbackContext context)
    {
        isSprinting = false;
        moveSpeed = walkSpeed;
    }

    private void Jump(InputAction.CallbackContext callback)
    {
        if(grounded && readyToJump)
        {
            isJumping = true;
            state = MovementState.air;
            readyToJump = false;

            exitingSlope = true;

            // reset y velocity
            rb.linearVelocity = new Vector3(rb.linearVelocity.x, 0f, rb.linearVelocity.z);

            rb.AddForce(transform.up * jumpForce, ForceMode.Impulse);

            Invoke(nameof(ResetJump), jumpCooldown);
        }
    }

    private void Crouch(InputAction.CallbackContext callback)
    {
        isCrouching = true;
        playerObject.localScale = new Vector3(playerObject.localScale.x, crouchYScale, playerObject.localScale.z);

        rb.AddForce(Vector3.down * 2f, ForceMode.Force);
    }

    private void Standup(InputAction.CallbackContext context)
    {
        isCrouching = false;
        playerObject.localScale = new Vector3(playerObject.localScale.x, startYScale, playerObject.localScale.z);
        playerCamera.transform.position = camStartPos;
    }


    private void StartGrab(InputAction.CallbackContext callback)
    {
        if (!isGrabbing)
        {
            RaycastHit hit;
            if (Physics.Raycast(Camera.main.transform.position, Camera.main.transform.TransformDirection(Vector3.forward), out hit, grabDistance, Grabbable))
            {
                currentObject = hit.transform.GetComponent<Rigidbody>();
                currentObjectParent = currentObject.transform.parent;

                currentObject.transform.position = ObjectGrabPoint.position;
                currentObject.transform.parent = ObjectGrabPoint;

                currentObject.useGravity = false;
                currentObject.constraints = RigidbodyConstraints.FreezeAll;
                currentObject.linearDamping = 0;
                currentObject.angularDamping = 10f;

                isGrabbing = true;
            }
        } 
    }

    private void EndGrab(InputAction.CallbackContext callback)
    {
        if (isGrabbing)
        {
            currentObject.transform.parent = WorldController.Instance.CurrentTimeParent();

            currentObject.useGravity = true;
            currentObject.constraints = RigidbodyConstraints.None;
            currentObject.linearDamping = 0.5f;
            currentObject.angularDamping = 0.05f;

            Vector3 force = orientation.forward + rb.linearVelocity + camVelocity;
            currentObject.AddForce(force, ForceMode.Impulse);

            currentObject = null;
            currentObjectParent = null;

            isGrabbing = false;
        } 
    }

    private void ShiftTime(InputAction.CallbackContext callback)
    {
        if (canTimeShift)
        {
            StartCoroutine(ShiftTimeCoroutine());
        }    
    }

    IEnumerator ShiftTimeCoroutine()
    {
        timeShiftCooldownRemaining = timeShiftCooldown;

        if (!timeShiftReady)
        {
            yield return new WaitForSeconds(timeShiftCooldown);        
            timeShiftReady = true;
        }
        else
        {
            WorldController.Instance.ShiftTime();
            timeShiftReady = false;
        }
    }


    private void ResetJump()
    {
        state = MovementState.idle;
        isJumping = false;
        readyToJump = true;
        exitingSlope = false;
    }

    private bool OnSlope()
    {
        if (Physics.Raycast(transform.position, Vector3.down, out slopeHit, playerHeight * 0.5f + 0.3f))
        {
            float angle = Vector3.Angle(Vector3.up, slopeHit.normal);
            return angle < maxSlopeAngle && angle != 0;
        }

        return false;
    }

    private Vector3 GetSlopeMoveDirection()
    {
        return Vector3.ProjectOnPlane(moveDirection, slopeHit.normal).normalized;
    }

    public void CalculateFootsteps()
    {
        if(_2DVelocity.magnitude > (moveSpeed / 100) && !isIdle && currentPhysicsMaterial != null)
        {
            if (stepTimer > stepTiming && state != MovementState.air && state != MovementState.crouching)
            {
                CallFootstepClip();

                if (state == MovementState.walking)
                {
                    stepTimer = 0;
                }
                else if (state == MovementState.sprinting)
                {
                    stepTimer = (stepTiming / 2 + _2DVelocityMag * 2);
                }
            }
        }
    }

    public void CallFootstepClip()
    {
        if (playerAudioSource)
        {
            if (enableFoosteps && foostepProfiles.Any())
            {
                for (int i = 0; i < foostepProfiles.Count(); i++)
                {
                    if (foostepProfiles[i]._physicMaterials.Contains(currentPhysicsMaterial))
                    {
                        currentFootstepClips = foostepProfiles[i].footstepClips;
                        break;
                    }
                    else if (i == foostepProfiles.Count - 1)
                    {
                        currentFootstepClips = null;
                    }
                }

                if (currentFootstepClips != null && currentFootstepClips.Any())
                {
                    playerAudioSource.PlayOneShot(currentFootstepClips[Random.Range(0, currentFootstepClips.Count())]);
                }
            }
        }
    }

    private void OnTriggerStay(Collider other)
    {
        if (other.CompareTag("Water"))
        {
            currentPhysicsMaterial = other.GetComponent<Collider>().sharedMaterial;
            
            isInWater = true;
            rb.linearDamping = 10;
        }
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.CompareTag("Water"))
        {
            currentPhysicsMaterial = null;
            isInWater = false;
            rb.linearDamping = groundDrag;
        }
    }

}

I’m at a loss and just can’t figure out why my movement doesn’t work on inital runs/loads.

If anyone can see something I can’t or needs the full script let me know.

First hunch: focus. Perhaps the input isn’t registered? Did you log input events to see whether they are captured as expected?

You know how to debug code? Because first instance I’d get where the player wouldn’t move I’d fire up the debugger and step through the code to see where it’s passing by a conditional for instance. Could be one of those flags’ initial states.

Most likely candidates would be isCrouching, state and moveSpeed if crouching once fixes the issue.

Also by applying a force to the rigidbody you are going to wake it up. Check if perhaps the body starts sleeping (happens when it’s initially at rest) and won’t wake up otherwise. You could drag the player start location 1 unit above ground to ensure it’s going to fall - if that consistently fixes it it could be related to a sleeping body. Check the IsSleeping flag of the body.

Yea, I’ve breakpointed every point of entry in the player controller and checked all these values and on crouch. Nothing seems out of the ordinary. Even when stepping through.

I wasn’t aware about isSleeping on a rigidbody though, that sounds promising. I’ll have a look at at that.

Edit: So the rigidbody is not sleeping when the Awake() method is called on the player controller. So It doesn’t seem to be that.

Dropping the player from above the ground on start, also doesn’t work.

All my inputs are enabled and being read, I can see the move direction and input being changed as I press the movement keys, it just doesn’t move.

Okay, my final try was to check the moveSpeed. My SpeedControl method seems to be the issue. (I think i forgot to set a slope limit)

I commented it out, and it works fine but the speed isn’t limited. So I just need to refine that method and make sure it doesn’t limit the speed to 0 on start.

Thanks for the help, I think I just needed to “ask aloud” haha. Rubber ducking I guess.

Scratch all this. I still have the bug after commenting this out. I just got lucky a few times :confused:

I see you using AddForce() to move stuff… that seems more suited to a spaceship than a player.

Why not just set the velocity directly on the Rigidbody based on inputs, perhaps filtering it for rate of change (acceleration / deceleration)? That gives you absolute control of it without having to ask the RB if it is faster than X, then limiting it by magnitude, etc.

If you must use AddForce() you might wanna consider a PD filter mechanism where the force is generated by feeding the error term between the desired speed and current speed into the PD filter and having it generate the force for you, then you can play with the filter numbers.

I use a PDFilter for the yaw / pitch / roll rates on my Spaceship3D demo here:

It’s all one big hairy class, broken up into partial class files. Lines 30-32 compute the yaw/pitch/roll error terms and they are applied a few lines later.

I scribbed more about this in the past:

and also later in that same thread.

I gave that a go but it seems like far more work to dampen player movement. Adding force means it starts from 0 then increases until I limit the speed. This allows for a far more natural feeling of movement, directly setting velocities will set them to a top speed straight away, so you would need to do as you say to gradully change the speed over time. To me it’s more effort to basically do what the physics engine does for us anyway when we just add force to a rigidbody.

(it also doesn’t help my bug with no movement at the start)

I will try this in future though, I like the idea.

Okay, so it was something stupid.

Because first instance I’d get where the player wouldn’t move I’d fire up the debugger and step through the code to see where it’s passing by a conditional for instance. Could be one of those flags’ initial states.

I forgot to set my moveSpeed when setting my state to idle. this wasn’t obivous when debugging.

So inital state is idle, so moveSpeed isn’t set.

Once i do any other movement that changes state, it works.

That’s the issue.

Thanks for all input