[Released] Character Movement Fundamentals - A Rigidbody-based Character Controller

Ahhh. I was curious as to why my Collider size and Collider height changed every time I played.

@JanOtt can explain better I’m sure, but what I’ve noticed is that I can ignore the actual collider settings and change the ones on mover which will adjust the size of the collider as needed.

@toddkc Thanks for the input.

@JanOtt Not that you need any help with a crouch system but Easy Character Movement [ https://assetstore.unity.com/packages/templates/systems/easy-character-movement-57985 ] does a good job. Granted, I prefer your system since it’s more accurate on difficult terrains. :wink:

That’s exactly right, toddkc. The ‘Mover’ component overrides the collider’s height and offset, based on the values set in its inspector (step height, collider height, collider thickness and collider offset).

Thanks! I’m familiar with ‘Easy Character Movement’, really nice package.

I am using the First person view and was wondering if there was any way to mimic a head bob while walking?

Generally speaking, you’d have to generate a heab bob animation for the camera (either through Unity’s built-in animation tools, an external program like Blender or procedurally creating it through code), then link that animation to the character’s current state (for example, only play the head bob when the character is grounded and its movement speed exceeds a certain threshold).

The controller component of ‘Character Movement Fundmentals’ offers two functions you can use for this purpose:

  • GetVelocity() → Returns the character’s current movement velocity.
  • IsGrounded() → Returns ‘true’ if the character is currently grounded.

You might also want to check out chapter 4.6 of the manual, which has a short code example on this as well.

@JanOtt I’m loving the controllers. Great work.

Just sharing a bit of today’s work.

The goal: I’m working on a test of some RPG mechanics in which the player can switch characters and/or possess other characters. So, I wanted to implement a system in which both the player and npcs use the mover system so the core movement mechanics are the same for both, so when you possess an NPC that was previously AI controlled, we just change the controller to one of the basic walker controllers. So, to make all entities use the mover system, I implemented a simple bridge script for controlling movers with NavMesh agents which will have their destinations set through behavior trees when not possessed by the player.

Hierarchy:

  • The base Gameobject has all the character stats and the like and also includes the bridge component.
  • Navmesh Agent: This must be a separate sibling of the gameobject with the Mover component as the Navmesh agent and non-kinematic rigidbody will push eachother otherwise!
  • Controller: This Gameobject contains the modified controller, collider, rigidbody and Mover (Note: the Mover step and slope should be set equal to those of the NavMesh agent type used to keep them in sync).
  • Model: With smoothtransform component.
    Root: Mesh, animators, etc.

So what I built first is the Bridge:

public class NMAgentControllerBridge : MonoBehaviour
{
    [SerializeField]
    Mover mover; //Mover component on the child
    [SerializeField]
    NavMeshAgent agent;//NavMeshAgent on the sibling of the child with the Mover component
    [SerializeField]
    float leash = 1f;//Acceptable distance between the NavmeshAgent and the Mover before the NavmeshAgent is stopped and waits for the Mover to catch up.

    bool hasAgent = false;
    bool hasMover = false;

    private void Awake()
    {
        if (agent == null)
        {
            agent = GetComponentInChildren<NavMeshAgent>();
        }
        if (mover == null)
        {
            mover = GetComponentInChildren<Mover>();
        }

        if(agent == null)
        { hasAgent = false; }
        else
        { hasAgent = true; }

        if(mover == null)
        {
            hasMover = false;
        }
        else
        { hasMover = true; }
    }

    private void Update()
    {
        if(!hasAgent || !hasMover)//If we're missing and agent or mover, we return to avoid null reference errors.
        { return; }

        if(agent.pathPending)//Don't do anything if the agent is calculating a path
        {
            return;
        }

        if(agent.hasPath)
        {
[INDENT][INDENT]//If the distance between the simulated position of the agent on the x and z axis (agent.nextposition) and Mover position is greater than the length of the leash, stop the NavmeshAgent. When the Mover catches up, let the NavMeshAgent resume along the path.[/INDENT][/INDENT]

            if(Vector3.Distance(new Vector3(agent.nextPosition.x, 0f, agent.nextPosition.z), new Vector3(mover.transform.position.x, 0f, mover.transform.position.z)) > leash)
            {
                agent.isStopped = true;
            }
            else
            {
                agent.isStopped = false;//Resume
            }

        }
    }
}

Then, on the child that contains the Mover component (sibling of the NavMeshAgent), I put this Controller:

public class AIWalkerController : BasicWalkerController
{

    public NavMeshAgent agent;
    bool hasAgent = false;
    [SerializeField]
    float leash = 1f;

    protected override void Setup()
    {
        base.Setup();
        if(agent == null)
        {
            GameObject parent = transform.parent.gameObject;
            agent = parent.GetComponentInChildren<NavMeshAgent>();
        }
        else { hasAgent = true; }

        if (!hasAgent)
        {
            if (agent == null)
            {
                hasAgent = false;
            }
            else { hasAgent = true; }
        }

        if(hasAgent)
        {

            agent.updatePosition = false;
            agent.updateRotation = false;
        }
    }

    protected override Vector3 CalculateMovementDirection()
    {
        Vector3 _movementDirection = Vector3.zero;
        bool isMoving = false;

         if(!hasAgent)
        {
            return _movementDirection;
        }


        if(agent.hasPath && agent.remainingDistance > agent.stoppingDistance)
        {
            isMoving = true;
        }

//If the NavMeshAgent is Moving, the Movement Vector to send to the Mover component is the vector from the our Transform position to the agent's simulated position.

        if(isMoving)
        {

            var heading = agent.nextPosition - transform.position;
            _movementDirection = heading.normalized;
        
        }

        return _movementDirection;
    }
//May implement jumping later
    protected override bool IsJumpKeyPressed()
    {
        return false;
    }
}

Navmesh Agent setup: For optimal results, I set the the Navmesh agent’s speed to about double the Mover’s speed (so the Mover will never be ahead of the agent) and the agent’s acceleration to the movement speed (so it won’t jitter when it stop and resumes waiting for the Mover).

Now the Mover follows smoothly behind the NavMeshAgent (keeping the leash short has the Mover follow closely as the agent avoids obstacles). If anyone else is looking for something similar, I hope it helps. Thanks again for the great system!

2 Likes

@JanOtt Great work on the Character Controller package. Really impressed with the general feel and how robust it is. I’ve been using it for a few weeks and keep running into some issues I was hoping you could help me with.

In general I’m having a lot of problems with manual overrides, like wanting to control the camera rotation manually or
move the character in scripting.

Is it possible to change the facing direction of the character without it being overriden by the previous velocity? I am using topdown with ‘TurnTowardControllerVelocity’ but sometimes I just want to teleport the character to a new location and rotation.

Using the top-down control, is there any way to manually tween the camera rotation between different rotations? I explicitly want to rotate 45 degrees at a time to look around, but I couldn’t figure out a way to make the camera rotate smoothly without it fighting the controls.

Finally, I’d love to see some ‘hold jump to jump higher’ control options.
Thanks for the great work. I’m looking forward to the next update.

Hi @dock !

[USER=3008380][/user]The easiest way to implement something like this would be to add add a simple function to the ‘TurnTowardControllerVelocity’ script that you can call to manually set the characters’ rotation, whenever you want to teleport.

The specifics depend a bit on your individual game though, so it’d probably be best if you contacted me via the support mail (support@j-ott.com) with a more-detailed description of how your teleportation works and I’ll try my best to help!

[USER=3008380][/user]With a bit of custom code, that’s definitely possible! There’s just a few things to consider here first, though:

Do you want the player to only be able to rotate the camera in 45° increments, or should these 45° rotations happen on top of the normal camera rotation? And do you need the camera angle to “snap” to certain angles (0, 45, 90, 135, 180 …)?

Just like before, please feel free to contact me via the support mail with more details and I’m sure we’ll find a good solution!

[USER=3008380][/user]Thanks! A ‘hold to jump higher’ option is actually part of the next update. I just have a few minor code improvements to do, but after that, the update should be released shortly.

If you try to edit certain prefabs with unity 2019.2.5 and have the root of the prefab with the mover component selected, the prefab will try to save 10 times per second!

I found a fix for it.

Go into the MoverInspector.cs Editor script and change the last line to this:

//EditorUtility.SetDirty(mover);
Undo.RecordObject(mover,"mover updated");

Thanks for the suggestion! I’ve already implemented a fix for this bug (which will be part of the next update), but maybe your approach is an even better solution? If that’s the case, I’ll definitely include it in the next update instead!

Thanks again!

@MrJohnWeez Great fix! Thanks for sharing!!

Thank-you, this helped a lot. I was able to write a new version with an override that allowed the new rotation to be manually set.

At the moment my game starts at 0 and I can rotate 45 degrees at a time only. My soluton at the moment is a CameraController hack which specifies a new RotateCamera, but I wasn’t able to allow any sort of smooth movement, such as ‘RotateTowardsVelocity’

Mostly I would love an option to have more freedom to rotate the camera and have the movement input cope with it.

Sounds great! Looking forward to it!!

For smooth camera rotation, the best way would probably be to rotate the camera over a short period of time using coroutines - that way, you could even use custom curves to ease in/out the rotation for a more ‘organic’ game feel.

If the player should only be able to rotate the camera in increments of 45° and shouldn’t be able to move the camera “manually” using the mouse, a camera system like that should be very straightforward to implement.

If that’s what you’re after, feel free to contact me via email and I’ll walk you through the specifics!

So if I understood you correctly, you would prefer a controller system that doesn’t require a ‘CameraController’ component to work?

I’m asking because I wrote a modified controller script a few weeks ago for a different customer who needed something very similar for their project.

Essentially, instead of a ‘Camera Controller’ component, the modified controller only requires a reference to (any) camera in the scene and will use that camera’s view axes to calculate the character’s movement direction.

As a result, you’re free to move and rotate the camera around as you like (it doesn’t even have to be parented to the character).

If that sounds interesting to you, just contact me via email and I’ll send you the script along with some instructions on how to import it into your project.

Hope that helps!

I was able to find a nice solution for my camera control woes. I separated the ability to Reset the camera into a public function on CameraController.cs. These were all taken from Awake()

    // This resets relative controls
    public void ResetFromWorldPosition()
    {
        lastPosition = tr.position;

        //Set angle variables to current rotation angles of this transform;
        currentXAngle = tr.localRotation.eulerAngles.x;
        currentYAngle = tr.localRotation.eulerAngles.y;

        //Execute camera rotation code once to calculate facing and upwards direction;
        RotateCamera(0f, 0f);
    }

Meanwhile I’m using ‘DoTween’ for rotating the camera, on a class which inherits from CameraController. It calls ResetFromWorldPosition() while the rotation happens.

    IEnumerator RotateCameraSequence(float offsetAngle)
    {
        Tween myTween = transform.DOBlendableRotateBy(new Vector3(0, offsetAngle, 0), 0.25f);

        while (myTween != null)
        {
            yield return null;
            ResetFromWorldPosition();
        }
    }

Hello! I’m loving the look of this package. Before I pick it up, I’d like to know a little more about how well it plays alongside Unity’s own AI nav agents. Can the two co-exist in a space quite happily? What happens if the player tries to move their character through a crowd of agents, for example? Or jump on their heads?

Just wanted to quickly announce that version 1.2 has just been approved by Unity. Here’s the changelog:
Version 1.2

  • Fixed a bug that caused visual glitching when viewing controller prefabs in the inspector in newer versions of Unity (2018 and up).
  • Added a public ‘Add Momentum’ function to ‘BasicWalkerController’, which can be used to add a force to the controller (useful for jump pads, dashing, […]).
  • Added an option for more responsive jumping: By holding the ‘jump’ key longer, the character will jump higher.
  • Added two public functions to ‘CameraController’. By calling them from an external script, the camera can be rotated either toward a direction or a specific position (see manual for more details).
  • ‘SideScroller’ controller prefab has been streamlined (code-wise) and is now compatible with the ‘GravityFlipper’ prefabs.
  • Various minor changes to some of the code to improve readability.

If there’s any questions about some of the new features, please feel free to ask in this thread or directly by email (support@j-ott.com).

1 Like

Hi! In allmost all situations, the controllers included in 'Character Movement Fundamentals’ behave very similarly to regular rigidbodies. So when it comes to the interaction between navmesh agents and controllers, it all depends on how you’ve set up the agents.

For example:

  • If your navmesh agents have no collider and rigidbody attached, there won’t be any collision between agents and controllers (since navmesh agents by themselves are not physics objects).
  • If your navmesh agents do have colliders and (kinematic) rigidbodies attached, then the controller will get pushed around by the agents.

I haven’t tested this yet, but I’m fairly sure that if you attach a ‘Nav Mesh Obstacle’ component to a (player) controller, the agents will try to walk around it (which is probably the best solution for crowds).

I hope that helps!

1 Like

Great, makes perfect sense. Thanks for the quick reply!

I have used NavMeshObstacles in the past and yeah that’s exactly how I’d expect it to work.

I’m going to buy the plugin now :slight_smile:

1 Like

Thanks for the update!

Unfortunately I’m having lots of problems.
Firstly, the gravity tunnel is broken in all camera modes.

Secondly, I wasn’t able to get the camera rotation controls working at all, and it’s not clear what the function expects.
Could you show a working code example to press Q and E to rotate the camera in 45 degree steps around the Y axis?

Even when I got it partially working, the movement input wasn’t updating for up/down controls, but the left/right controls were correctly aligned.

I was usually getting the following error when I added camera keys to the Topdown demo:

Hi Hayden!

I’m very sorry for the confusion, I should have explained the two new camera controller functions in more detail in the change log.

Anyway, to clear things up:
The new ‘RotateTowardPosition/Direction’ functions that have been added to the camera controller component are intended for making the camera look at a specific object in the scene (or in a specific direction).
They are really not suited for directly controlling the camera’s angles in a precise way (which is what you need in your project).

Are you still using the ‘GlobalCameraWalkerController’ script I sent you? Because that script currently can’t handle a rotating controller (which is what’s happening in the gravity tunnel and the gravity flippers).

To make the script compatible with controller rotation, you’ll need to replace the code in line 33/34:

Vector3 _cameraForwardVector = Vector3.ProjectOnPlane(globalCameraTransform.forward, Vector3.up).normalized;
Vector3 _cameraRightVector = Vector3.ProjectOnPlane(globalCameraTransform.right, Vector3.up).normalized;

… with this code:

Vector3 _cameraForwardVector = Vector3.ProjectOnPlane(globalCameraTransform.forward, tr.up).normalized;
Vector3 _cameraRightVector = Vector3.ProjectOnPlane(globalCameraTransform.right, tr.up).normalized;

That way, the script will take the current rotation of the controller into account when calculating the new input direction.

Sure!
Please take a look at the script I attached to this post called ‘RotateAroundYAxisInIncrements’.
Try importing it into your project and attach it to the ‘CameraControls’ gameobject in the character’s hierarchy (and make sure to delete/disable the camera controller script, if necessary).

By pressing ‘Q’ and ‘E’, this script will rotate any gameobject in adjustable increments (“Angle Increment”) over time (“Rotation Duration”).
You can also change the “feel” of the rotation by adjusting the “Rotation Curve” or enable “Clamp Angle” to make sure that floating point imprecision won’t affect the rotation results (over time).

I hope that fixes all (or at least most) of the problems you’ve described. If something’s still not working or if anything’s unclear, please contact me via the support mail so we can continue there!

5102399–503225–RotateAroundYAxisInIncrements.cs (2.47 KB)