Changing Character controller to match the animations

I need help with a few things mainly how to adjust the charcter controller/capsule collider to match the animation.

My problem: when i enter water obviously when I swim I am now my player obviously rotates to match the animation but it seems the charcter controller does not do the same:

Then I have another problem of even if im looking up and down with the camera and move forward it does not move to the camera posistion.

    private Vector3 CalculateMovement()
    {
        Vector3 cameraForward = stateMachine.MainCameraTransform.forward;
        Vector3 cameraRight = stateMachine.MainCameraTransform.right;


        cameraForward.Normalize();
        cameraRight.Normalize();

        return cameraForward * stateMachine.InputReader.MovementValue.y +
        cameraRight * stateMachine.InputReader.MovementValue.x;
    }

    private void FaceMovementDirection(Vector3 movement, float deltaTime)
    {
        
        stateMachine.transform.rotation =  Quaternion.Lerp(
            stateMachine.transform.rotation,
            Quaternion.LookRotation(movement),
            deltaTime * stateMachine.RotationDamping);

    }

I am assuming that my mainCameraTransform.forward , is always where it is facing?

Is the charcter controller limited to this? is there a diffrent approach by changing my movement code to a ridgid body or something?

as it feels like I am not moving but only moving the charcter controller which just stays on the ground

Unfortunately, this is a limitation of the CharacterController. It simply wasn’t designed for flying or swimming. You can shrink the height so that at the very least you can take advantage of the isGrounded functionality, but you’ll need to use extra colliders to block movement.

Thanks for the response @Brian_Trotter !, I assumed this after some initial testing.

What would you recommend me using instead a ridgid body , with a capsule collider attached to the player?, and use my movments with the ridgidbody.

If you could point me in the right direction/ Assist me to get started in a player that will be able to swim would be a great help.

I haven’t acutally done swimming (largely because of this issue).

So a Capsule Collider will move with the transform’s rotation, unlike the Character Controller, but most swimming animations actually assume that the transform is still oriented upright. So for this. But… since we know that the character is swimming, you could orient the capsule collider DIRECTION to the Z axis.

So first, you need to detect you’re swimming… looks like you’ve got that part figured out…
Then in your Swimming state, I would set the CharacterConroller’s height to .4 and turn on the Capsule Collider. You’ll also want to turn gravity off in the ForceReceiver (write simple methods to turn off a new bool useGravity and only apply gravity if the bool is active).

You can then still use the CharacterController for basic movment through the water (it will still prevent you from going under the ground, for example). The tricky part is preventing movement via the Capsule Collider. For that, you’ll probably need to have a class that listens for OnTriggerEnter, but only from the ground layer and then issues an event when a collision occurs so you can move the character to the previous location.

Ok I will give your method and try and see what works, Although since the movement is going to be basic other than in the water, Default move no jump or other commands, Maybe its best to use a ridgidbody along with a capsule colllider on its on, the just tell unity to move with translate.

I don’t know if this will work as a solution but give it a try honestly will probably, come up with a better solution than using the unity built in character controller.

Im having better results with your method @Brian_Trotter , as the ridgidbody seems fairly fittery but adding another capsule collider that i activate when in water and changing the values like you mentioned, seems to give an ok result .

I am still confused about how moving on the Y works which is what I want not by force but obviously when the camera is looking up and down you can move up and down but this does not seem to be the case and the character refuses to move in that direction.

So i calculate the movment and rotation


    private Vector3 CalculateMovement()
    {
        Vector3 cameraForward = stateMachine.MainCameraTransform.forward;
        Vector3 cameraRight = stateMachine.MainCameraTransform.right;


        cameraForward.Normalize();
        cameraRight.Normalize();

        return cameraForward * stateMachine.InputReader.MovementValue.y +
        cameraRight * stateMachine.InputReader.MovementValue.x;
    }

    private void FaceMovementDirection(Vector3 movement, float deltaTime)
    {
        
        stateMachine.transform.rotation =  Quaternion.Lerp(
            stateMachine.transform.rotation,
            Quaternion.LookRotation(movement),
            deltaTime * stateMachine.RotationDamping);

    }

I want to take in all the vectors usually I would 0 = y

then on tick i call these methods:
Vector3 movement = CalculateMovement();
Move(movement * stateMachine.SwimSpeed, deltaTime);

    protected void Move(Vector3 motion,float deltaTime)
    {
        stateMachine.CharacterController.Move((motion + stateMachine.forceReciever.movment) * deltaTime );
    }
}

even if i remove the force reciever to remove all the function of that and just have motion * deltatime it still has no avail. Im getting confused on how camera relative movment works even after watching videos on it.

using System.Collections;
using System.Collections.Generic;

using UnityEngine;

public class ForceReciever : MonoBehaviour
{
    [SerializeField] private CharacterController controller;
    [SerializeField] private PlayerStateMachine playerStateMachine;
    

    private float verticalVelocity;

    public Vector3 movment => Vector3.up * verticalVelocity;

    private void Update()
    {
        if(!playerStateMachine.isInWater)
        {
            ApplyGravity();
        }
    }

    private void ApplyGravity()
    {
        if (verticalVelocity < 0f && controller.isGrounded)
        {
            verticalVelocity = Physics.gravity.y * Time.deltaTime;
        }
        else
        {
            verticalVelocity += Physics.gravity.y * Time.deltaTime;
        }
    }
}

Any help would be greatly appreicited after trying different things.

For Y, you’ll need to assign a new set of keys to represet up and down (perhaps Q and Z)… for these the movement is fairly simple, Vector3.up and Vector3.down. The camera really doesn’t focus into it much at all.

Wait now I can move on that direction dependent on where Im looking. I truns out I wasn’t entering the correct state. Now if i look down and up Iand press the forward key I will go in that direction.

Now I can use your advice for changing the charcter collider to handle when im In water. And then I can progress from here.

But I was thinking maybe to use your method, for when im on the surface, so I constrain to the x, when you are on the surface to dive under.,

the other alternative is just the way i have it now you just dive under if you are on free look, so i will need to experiment for my game design.

Thanks

1 Like

I moved this to talk as it’s not directly course related,

Your method works, and I can enable and disable the collider when I enter each state, Now the problem acts very weird whenever I changed the Values of the character controller i.e Height, slope height etc, On enter each state. They automatically keep triggering the change of states on trigger enter, and exit.
Now of course this only happens when I am at the very edge of the trigger but this causes A problem as I am swithcing states constantly causing a knockon effect. - This does not happen if i just have colliders when i enable and disable, I enter the state the way im supposed to.

I have tried multiple ways to sort this, I have tried setting a flag, to change values only if it can change values but to no avail.

Although with the changes so far, the collider does not interact with any movment for this, so once I can change the values somethly without changing state again then all be good.

So if you have any suggestions would be great.

This is tricky…
I’m not sure the specific conditions that you’re using to switch to swimming.

You might consider something similar to the TargetFinder… (If you’re doing my 3rd Person->RPG, it would be a child class of the RangeFinder). With this you would have a separate GameObject under the Player that only intersects with the water layer. Put this with a small collider up around chest level. Use that to determine if you should or shouldn’t be swimming…
It’ll take some fine tuning.

I had the system work, ontrigger if(!isInWater) - switch to floating state, once in the floating state I used a raycast from a headposistion that i calculated with an offset of the height of the character controller, Obviously i had to remove this way, as I am going to adjust the height of the character controller, to match the fit inside the horizontal capsule collider. This looked like a good solution untill Once I made a change to the character controller i.e height while in the trigger, it would automatically trigger the change of states again.

So I have decided to try and use a Ridgidbody with a capsule collider as this would probably be more effecient and easier to get working as I can Just adjust the orientation of the capsule collider, When I switch states.

Though I am having problems with using a ridgidbody with a capsule collider, as I am not familiar with how that interacts and handles is causing some Jittering etc. I have posted this in the unity help. with the breakdown and code of trying to use the ridgidbody.

Another reason we don’t like to use the rigidbody. It fights aggressively with the other components (NavMeshAgents, CharacterControllers, our own ForceReceiver).

@bahaa appears to have gotten a working swimming solution, if I remember correctly. I just can’t remember the details of what he did to get there (beyond my WaterFinder as an aid to know when to switch.

I’ll share them tomorrow when I’m home

1 Like

I mean this is ok and I assumed as much and was going to handle the forces I needed with the ridgidbody, I.e drag and such when I enter the water , As this Is not done on my main RPG this is a more streamlined version and It will be using some of the stuff learned in the lectures, like inventory.

Thanks @Bahaa I want to see what sort of way you implemented it and decided how to handle the collider situation.

It had a trigger box collider on the player’s head, and a water finder event that was subbed and unsubbed, with switches to and from swimming and freelook states.

It’s not 100% clean, because force receiver is a mess, but it’ll do fairly decently if you’re not trying to drive any boats from the water (that’s an entirely different state in and of itself)

I’ll share what I did with you when I’m home

If I don’t respond in 24 hours, tag me

I’ll be a little late on this one, because I’m dealing with two severe problems in my game (both are out of course content), and it’ll take some effort to explain exactly what’s going on in my swimming state (there’s a swim limiter integrated, ignorance of two collision layers and other stuff that you’ll need to understand so you don’t run into any crazy bugs). Please be patient, I’m doing my best :slight_smile:

1 Like

Its ok Just try your best, I can switch state, But when using the character colidor, I have the two colliders then active once I change anything in the character controller like height to basically hide it inside the swimming capsule collider.

So If I can see how you implement it, maybe I can see a different approach to it. And it would be greatly apprecieted.

OK so… This will seem rushed out, and it kind of is, because this topic has been bugging me for a while (I don’t perform well in coding when I know someone is waiting on something from me. It adds pressure on me (no offence by the way)) and I want to focus on my farming system, so please bear with me. I’m sure you’ll find your way around things:

  1. Create a blend tree for your swimming animations (don’t rely on my values, as they’re dedicated to my system)
  2. Place an empty gameObject, called WaterFinder on the player, with a rigidbody (Kinematics on, Gravity off)
  3. Put on a box collider ON the player’s head (not on top of or below it, but on it), hide the mesh renderer and make it a trigger
  4. Add the ‘WaterFinder.cs’ script below on the box collider from step 3 (ignore the Malbers Stuff. This is an interface I’m using to deal with a mathematical issue):
using UnityEngine;
using RPG.Core;
using RPG.States.Player;

public class WaterFinder : RangeFinder<Water>
{

    // This script sits on the Player's 'WaterFinder' component, allowing interaction
    // between the Player and the Water. If true, the player can switch to his 'PlayerSwimmingState.cs'
    // from 'PlayerStateMachine.cs', allowing Swimming in the game

    private PlayerStateMachine playerStateMachine;

    // (TEST) MALBERS TRACKER
    private IMalbersInputHandler inputHandler; // same thing as 'PlayerStateMachine' I guess...

    void Awake()
    {
        playerStateMachine = FindObjectOfType<PlayerStateMachine>();
        
        // TEST:
        inputHandler = GetComponentInParent<PlayerStateMachine>(); // same thing as 'PlayerStateMachine' I guess...
    }

    protected override void AddTarget(Water target)
    {
        base.AddTarget(target);
        playerStateMachine.SetIsSwimmingOrUnderwater(true); // Solution to get the player to stop playing the dodging state whilst swimming, dodging his way to underwater
        target.OnWaterDestroyed += RemoveTarget; // you've added the water, you'll need a pulse trigger when it's gone, as an event
        // GetComponentInParent<MalbersInput>().DisableInput("Call Mount"); // replaced with 'TEST' below
        Debug.Log("Adding WaterTarget");

        // TEST - AVOID THE PLAYER FROM CALLING THE ANIMALS WHEN AT SEA:
        inputHandler.DisableMalbersInput(); // stops the player from being able to call the animal when in sea (it's a tough solution, but it's the only one I can think of)
    }

    protected override void RemoveTarget(Water target)
    {
        base.RemoveTarget(target);
        playerStateMachine.SetIsSwimmingOrUnderwater(false); // Solution to get the player to stop playing the dodging state whilst swimming, dodging his way to underwater
        target.OnWaterDestroyed -= RemoveTarget; // you've already removed the water, you don't need the 'RemoveTarget()' function anymore...
        // GetComponentInParent<MalbersInput>().EnableInput("Call Mount"); // replaced with 'TEST' below
        Debug.Log("Removing WaterTarget");

        // TEST - AVOID THE PLAYER FROM CALLING THE ANIMALS WHEN AT SEA:
        inputHandler.EnableMalbersInput(); // stops the player from being able to call the animal when in sea (it's a tough solution, but it's the only one I can think of)
    }

}
  1. Here’s my ‘PlayerSwimmingState.cs’ script:
using RPG.States.Player;
using UnityEngine;

public class PlayerSwimmingState : PlayerBaseState
{
    // SWIM-LIMITER, ENSURES THE PLAYER DOES NOT REVIVE ABOVE A CERTAIN HEIGHT:
    private BoxCollider swimLimiter;

    // BOAT-FINDER (ALLOWS THE PLAYER TO DRIVE THE NEAREST BOAT):
    private BoatFinder boatFinder;

    // LAYERMASKS TO ENSURE ANIMALS AND SWIMLIMITER (FOR REVIVING) DO NOT INTERACT, CAUSING THE ANIMALS TO WALK ON BLOCKED NAVMESH:
    private int animalLayer;
    private int swimLimiterLayer;

    public PlayerSwimmingState(PlayerStateMachine stateMachine) : base(stateMachine)
    {
        swimLimiter = GameObject.Find("SwimLimiter").GetComponent<BoxCollider>(); // NAME-BASED SEARCH (FOR ONE-OFF OBJECT)
        boatFinder = stateMachine.BoatFinder;

        animalLayer = LayerMask.NameToLayer("Animal");
        swimLimiterLayer = LayerMask.NameToLayer("SwimLimiter");

        // TEST - 24/6/2024 - CHECK BOTH LAYERS EXIST (-1 MEANS THEY DON'T EXIST):
        if (animalLayer == -1 || swimLimiterLayer == -1) 
        {
            // AVOID INTERACTION BETWEEN ANIMALS AND THE SWIM LAYER, IN 'ENTER'
            Debug.Log($"Layers 'Animal' or 'SwimLimiter' not found");
        }
    }

    private static readonly int FreeLookSwimBlendTreeHash = Animator.StringToHash("FreeLookSwimBlendTree");
    private static readonly int FreeLookSwimSpeedHash = Animator.StringToHash("FreeLookSwimSpeed");

    public override void Enter()
    {
        stateMachine.Animator.CrossFadeInFixedTime(FreeLookSwimBlendTreeHash, stateMachine.CrossFadeDuration);
        stateMachine.WaterFinder.OnTargetRemoved += InputReader_HandleFreeLookEvent; // RETURN TO FREELOOK IF NO WATER FOUND
        stateMachine.ForceReceiver.SetIsSwimming(true);
        stateMachine.InputReader.BoatDrivingEvent += InputReader_HandleBoatDrivingEvent; // BUTTON-BASED BOAT DRIVING
        // boatFinder.OnTargetAdded += BoatFinder_OnTargetAdded; // PROXIMITY-BASED BOAT DRIVING

        // TEST - 24/6/2024 - SET COLLISION IGNORANCE (BUT MAKE SURE THEY BOTH EXIST, FIRST):
        if (animalLayer != -1 && swimLimiterLayer != -1) 
        {
            Physics.IgnoreLayerCollision(animalLayer, swimLimiterLayer, true);
        }
    }

    public override void Tick(float deltaTime)
    {
        Vector3 movement = CalculateMovement();
        stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, 1.0f * movement.magnitude, AnimatorDampTime, deltaTime);
        FaceMovementDirection(movement, deltaTime);

        if (stateMachine.InputReader.IsSpeeding)
        {
            // the squared values in 2nd parameter are to compensate for the blend tree variable, some glitch I found (Bahaa stuff, Brian was not involved in this)
            stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, 1.414f * 1.414f * movement.magnitude, AnimatorDampTime, deltaTime); // tuning the animation threshold, so he turns to swimming speed state
            Move(movement * stateMachine.FreeLookSwimmingMovementSpeed * 1.5f, deltaTime); // actual movement speed
            FaceMovementDirection(movement, deltaTime); // turning around when swimming, based on input
            return;
        }

        if (stateMachine.InputReader.MovementValue == Vector2.zero)
        {
            stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, 0f, AnimatorDampTime, deltaTime); // Float, no movement input

            if (stateMachine.Animator.GetFloat(FreeLookSwimSpeedHash) < 0.1f)
            {
                stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, 0f);
            }
            return;
        }

        if (stateMachine.InputReader.IsDiving)
        {
            // You're diving now:
            stateMachine.ForceReceiver.SetIsDiving(true);
            stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, 2f * 2f, AnimatorDampTime, deltaTime);
            Move(movement * stateMachine.FreeLookDivingSpeed + movement * stateMachine.FreeLookSwimmingMovementSpeed, deltaTime);
            FaceMovementDirection(movement, deltaTime);
            return;
        }

        else 
        {
        stateMachine.ForceReceiver.SetIsDiving(false);
        }
        
        if (stateMachine.InputReader.IsReviving)
        {
            swimLimiter.enabled = true;
            if (IsBelowReviveLimit()) 
            {
            stateMachine.ForceReceiver.SetIsReviving(true);
            stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, 5f * 5f, AnimatorDampTime, deltaTime);
            Move(movement * stateMachine.FreeLookDivingSpeed + movement * stateMachine.FreeLookSwimmingMovementSpeed, deltaTime);
            FaceMovementDirection(movement, deltaTime);
            return;
            }

            else 
            {
                stateMachine.ForceReceiver.SetIsReviving(false);
            }

        }

        else
        {
            swimLimiter.enabled = false;
            stateMachine.ForceReceiver.SetIsReviving(false);
        }

        Move(movement * stateMachine.FreeLookSwimmingMovementSpeed, deltaTime); // actual movement speed
    }

    public override void Exit()
    {
        stateMachine.WaterFinder.OnTargetRemoved -= InputReader_HandleFreeLookEvent;
        stateMachine.ForceReceiver.SetIsSwimming(false);
        stateMachine.InputReader.BoatDrivingEvent -= InputReader_HandleBoatDrivingEvent; // BUTTON-BASED BOAT DRIVING
        // boatFinder.OnTargetAdded -= BoatFinder_OnTargetAdded; // PROXIMITY-BASED BOAT DRIVING

        // TEST - 24/6/2024 - RESET COLLISION IGNORE (AGAIN, FIRST MAKE SURE THEY BOTH EXIST):
        if (animalLayer != -1 && swimLimiterLayer != -1)
        {
            Physics.IgnoreLayerCollision(animalLayer, swimLimiterLayer, false);
        }
    }

    private bool IsCloseToBoat()
    {
        Boat nearestBoat = boatFinder.GetNearestTarget();

        if (nearestBoat != null)
        {
            float distanceToBoat = Vector3.Distance(stateMachine.transform.position, nearestBoat.GetComponent<Collider>().ClosestPoint(stateMachine.transform.position));
            float boatBoardingDistanceThreshold = 2f;
            return distanceToBoat <= boatBoardingDistanceThreshold;
        }
        return false; // if you're not close to a boat, return this function as a false
    }

    /// <summary> (TEMPORARILY DISABLED)
    /// Proximity-based Boat Driving Function. When activated, the player just needs to be close to the boat to be able to start driving it
    /// </summary>
    /// <param name="boat"></param>
    private void BoatFinder_OnTargetAdded(Boat boat) 
    {
        // no boat-mounting if you're diving, speeding or reviving (avoids jump and flying to space bugs)
        if (IsCloseToBoat() && !stateMachine.InputReader.IsReviving && !stateMachine.InputReader.IsDiving && !stateMachine.InputReader.IsSpeeding) {
            boat.GetComponent<BoxCollider>().isTrigger = true;
            stateMachine.SwitchState(new PlayerBoatDrivingState(stateMachine));
        }

        // if you're diving or reviving, no boat-driving allowed (so you'll bump into the boat now)
        else if (IsCloseToBoat()) 
        {
            // Write a debug here, asking the player to stop diving, reviving or speeding when arriving to the boat
            boat.GetComponent<BoxCollider>().isTrigger = false;
        }
    }

    private void FaceMovementDirection(Vector3 forward, float deltaTime)
    {
        if (forward == Vector3.zero) return;
        Quaternion desiredRotation = Quaternion.LookRotation(forward, Vector3.up);
        stateMachine.transform.rotation = Quaternion.Slerp(stateMachine.transform.rotation, desiredRotation, stateMachine.FreeLookRotationSpeed * deltaTime);
    }

    private Vector3 CalculateMovement()
    {
        Vector3 forward = stateMachine.MainCameraTransform.forward; // move forward relative to the camera
        Vector3 right = stateMachine.MainCameraTransform.right; // move right relative to the camera
        forward.y = 0; // don't go flying...
        right.y = 0; // again, don't go flying...
        forward.Normalize(); // regardless of the camera's orientation, forward is forward
        right.Normalize(); // regardless of the camera's orientation, right is right
        Vector3 movement = right * stateMachine.InputReader.MovementValue.x; // accumulating the horizontal direction of movement
        movement += forward * stateMachine.InputReader.MovementValue.y; // summing up the horizontal and vertical directions of movement
        return Vector3.Min(movement, movement.normalized); // get the minimum movement value, between the vector and the scalar equivalent
    }

    private bool IsBelowReviveLimit()
    {
        RaycastHit hit;
        if (Physics.Raycast(stateMachine.transform.position, Vector3.up, out hit, stateMachine.ReviveLimitHeight))
        {
            // limit the reviving to the raycast limit (Vector3.Up ensures the Raycast is fired upwards):
            Debug.Log("Can Revive");
            return hit.point.y >= stateMachine.transform.position.y;
        }

        // below revive limit
        Debug.Log("Can't revive");
        return true;
    }

    private void InputReader_HandleFreeLookEvent(Water water)
    {
        stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));
    }

    /// <summary>
    /// Function called to enable driving a boat using the 'BoatDriving' ("F") Button
    /// </summary>
    private void InputReader_HandleBoatDrivingEvent()
    {
        // Hate to break it to you, but you need permission from a button (again) when you find a boat to drive:
        if (stateMachine.BoatFinder.FindNearestTarget())
        {
            if (stateMachine.InputReader.IsReviving) // AVOIDS A BUG OF THE PLAYER POTENTIALLY FLYING WHEN HE DISMOUNTS THE BOAT LATER ON
            {
                MalbersAnimations.InventorySystem.NotificationManager.Instance.OpenNotification("Action Not Allowed: \n", "Stop Reviving First");
                stateMachine.SwitchState(new PlayerSwimmingState(stateMachine));
                return;
            }
            stateMachine.SwitchState(new PlayerBoatDrivingState(stateMachine));
        }
    }
}

You’ll also need a Swim Limiter, so you don’t revive your way into space. It’s a huge, game-world size box collider, which is invisible to the eye, and isn’t supposed to interact with any moving objects, that only activates when you hold the reviving button (i.e: when you go back to surface to catch some air to breathe). Based on my code, just name it “SwimLimiter” (if you get the name wrong, the ‘GameObject.Find()’ which string-searches for it won’t find it)

(You may have noticed there’s boat driving integrated too. That’s a whole other system for another day. It has it’s fair share of bugs, hence why I have disabled some parts of it)

And one last thing. In your Input Reader controls assignment, try stay away from Shift and Control Keys. They get sticky and don’t always respond for some reason (it’s a Unity problem, based on my testing)

1 Like

Thank you for this, I can see sort of an idea on a way to handle it not calling twice now, When changing I had multiple trigger instances when modfying values of the character controller but using methods similiar to your approach could yield me better results.

Privacy & Terms