Christopher Jones, Games Development
  • Portfolio
    • Unity
    • 2D Games
    • Old Work
    • Source Engine
  • About Me / Contact
  • Blog

Unity and Custom Character Controllers, Revisited

4/30/2016

4 Comments

 
So, some time ago I wrote about the mayhem involved in getting a non-axially aligned character controller to function in Unity.

There were tears, there were shenanigans. By the end of it, there was a functional end product... but with more experience, it's time to look back on it with a more critical eye. For one thing, wouldn't it be amazing if you could just use standard Unity Rigidbody and Colliders to solve your character movement?

It is, in fact, possible, and without all the shaking and falling through mesh colliders that - at least for me - plagued my earlier attempts at solving with that method in the past.

Read on!
So I'm going to assume you've found this article because you're interested in a custom character controller. You've probably read some articles (possibly even my previous one!), probably read Roystan Ross's series of articles on the topic (if you haven't, I'd recommend it). This little followup isn't going to completely cover the nuts and bolts of a character control system with all the 'physics as vague suggestions' shenanigans a good one tends to involve; again, others have covered the topic more thoroughly than I (and my own is half-finished anyway...).

This is going to be about one single subtopic of the Character Controller theory; easily the most technical and - depending on how you approach it, most complex. Collision detection and resolution.

Now, obviously, we don't want characters running through walls or falling through floors. But the question of how to detect those walls and floors, and how to keep out of them, is a tricky one. In Unity, you're hampered mostly by the limitations of the Physics API, and in the end are left with two approaches:

1. 'Manual' collision detection and resolution.
2. Engine-based collision detection and resolution.

Where the 'Manual' approach is to collect all the local colliders yourself using OverlapSphere (or now, OverlapBox) and detect and resolve those collisions with your own code. This is the approach I applied previously and the approach taken by Ross's SuperCharacterController.

I make no joke, this approach is hard. Good for learning the mathematics behind intersections and geometry, but still hard. There is no built-in library for 'what is the closest point on this sphere/capsule/mesh/terrain/box', you will have to write them yourself. The mesh collision resolution in particular is a tricky one - in my previous article I made the foolishly inexperienced decision to try and resolve everything in world space, which meant transforming all the mesh's vertices into world space and- yeah (do your collision maths in the collider's local space, folks; there's so fewer transform operations).

But more than that, it demands you can read the data of any collision mesh from code. One mayor performance optimisation you can do in Unity is to not make your models read/writeable. You can see the problem here.

The alternative approach, and the one I suspect most people have tried, stared at in horror and desperately attempted something else with (I know I sure did), is to try and co-opt the Unity Rigidbody component to do the collision detection/resolution for you. And it can, as long as it is correctly set up, and your code interacts with it correctly.

The first thing is to do with the settings on your Rigidbody. Speaking generally, you'll probably want to disable rotation on all three axes (you can still rotate your character from code; this will just prevent the rigidbody trying to rotate them in response to collisions). The other things to note are the settings for Interpolation and Collision Detection. I have mine set to 'Interpolate' and 'Continuous Dynamic'.

Interpolation is pretty important because, as per the API documentation, it makes the rigidbody interpolate its position between FixedUpdates; if you've been having problems with 'jittery' characters, this will help you. Collision Detection I admittedly I am not completely certain is necessary, but a character controller seems important enough to warrant it; to explain the settings a little, 'Continuous' detection is more expensive, but helps prevent fast objects from passing through colliders, which has long been an issue plaguing my own attempts at a rigidbody character controller.

This feature is of particular importance where MeshColliders are involved, because a mesh is just an infinitely thin surface; there is no 'inside' or 'outside', there is only 'are colliding with the surface?'. They're the easiest to accidentally miss collisions on... and they're quite probably the most common collider used for level geometry. So it's a matter of some importance.

The other thing to handle correctly is how you change your rigidbody's position from code. This more than the Rigidbody's actual settings proved to be the real clincher for getting a rigidbody character controller to work; for one thing, the API is actually misleading.

If, like me, you went through the Rigidbody's scripting API documentation, you probably concluded that the best way to move your character controller was to calculate it's next position, then move the rigidbody with rigidbody.MovePosition(). A little something like this:
    void FixedUpdate()
    {
        Vector3 input = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));

        Vector3 nextPosition = rigidbody.position + transform.TransformDirection(input) * speed * Time.deltaTime;
        rigidbody.MovePosition(nextPosition);
    }
After all, the API documentation says that setting the rigidbody.position property instead would teleport the object to the next location, causing jagged movement, and the other alternative, setting the rigidbody's velocity, has this note in the API:
In most cases you should not modify the velocity directly, as this can result in unrealistic behaviour. Don't set the velocity of an object every physics step, this will lead to unrealistic physics simulation. A typical example where you would change the velocity is when jumping in a first person shooter, because you want an immediate change in velocity.
This... will not work. You will get jagged, 'blobby' movement and your character will pass through walls with just the slightest shove.

As it transpires, using SetPosition does just that; it sets the position... and then lets the physics system panic, flail and try to handle it. You pass through walls, you visibly push 'into' colliders... it's a mess. It makes no attempt to check if it can move to that position. So what's the solution?

...As it happens, the solution is to ignore what the API says and use rigidbody.velocity instead.
void FixedUpdate()
    {
        Vector3 input = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));

        Vector3 delta = transform.TransformDirection(input) * speed;
        rigidbody.velocity = delta;
    }
This is actually a little more involved; since as noted you're directly overwriting the velocity, you will have to handle gravity and character velocity yourself. By doing a manual track of velocity with a simple verlet integration (a fancy way of saying 'track where we last were and then look at the vector from where we were to where we are'), you can maintain gravity with this system, whilst still having an instantly responsive character controller:
    Vector3 worldVelocity;
    Vector3 lastPosition;
    void Start()
    {
        lastPosition = transform.position; //this is necessary to prevent our starting velocity being 'from 0,0,0 to transform.position'
    }

    void FixedUpdate()
    {
        //verlet integration
        worldVelocity = (rigidbody.position - lastPosition) / Time.deltaTime;
        lastPosition = rigidbody.position;

        //how much of our velocity is in line with the direction of gravity, ie 'how fast are we falling'
        Vector3 fallingVelocity = Vector3.Project(worldVelocity, Physics.gravity);

        Vector3 input = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));

        Vector3 delta = transform.TransformDirection(input) * speed;
        delta += fallingVelocity + Physics.gravity * Time.deltaTime;
        rigidbody.velocity = delta;
    }
, This nets you a character who can fall and responds instantly to input, and gives you other advantages as well (for example, you can modify your input to match the ground normal, or clamp falling velocity to prevent a falling player picking up too much speed). Because we're setting velocity directly, Unity will use proper collision detection in trying to move to the next location, and hence you'll have no 'pushing' into colliders or phasing through walls.

Finally, I'll address some common problems and 'gotchas':

My character is frozen in mid-air! The rigidbody won't move!
Most likely, this is one of two things: either you set your rigidbody to Kinematic, which you shouldn't, or you have an Animator component with root motion enabled. A Kinematic rigidbody will ignore its velocity and an Animator with root motion will actually overwrite it to match the root velocity of your current animation.

Ensure your rigidbody is not Kinematic and either disable Animator root motion or use OnAnimatorMove to combine it with your own calculated velocity. Admittedly my own character controller hasn't reached the stage where this is necessary yet (owing to a lack of animations needing root motion, to be specific), so I may have to get back to you on this one.

My character flies off into the stratosphere / moves at a crawl!
Time shenanigans! This is likely a result of not multiplying / multiplying too often by Time.deltaTime. It's actually a question of units. In short:
Velocity = meters per second
Acceleration = meters per second per second

Each 'per second' can be thought of as a multiplication by Time.deltaTime. All Time.deltaTime is is 'how long this frame is'; at 30 FPS your dT is 1/30 - somewhere around 0.03f - and typical values of Time.deltaTime are even smaller than that. So if you want to move 5 units per second, for a single frame update that's 5 * Time.deltaTime.

Where things get tricky is knowing when a value is already relative to deltaTime and when it is expected to be. rigibody.velocity for example takes a vector that is not relative to deltaTime; it's a true velocity value relative to 1 second. Hence, you'll notice, the Time.deltaTime gets dropped off the input vector calculation compared to setting the position every frame.

Accelerations meanwhile still require a single multiplication of Time.deltaTime; they're the rates of change in a given velocity for this frame. Hence, to apply acceleration under gravity, the fallingVelocity gets the added Physics.gravity * Time.deltaTime.

If you're moving far too quickly, you're either missing a multiplication by Time.deltaTime or possibly dividing by it when you should not. If you're moving far too slowly, you're applying Time.deltaTime more often than is needed.

My character flies off slopes / doesn't hog the ground correctly!

You'll definitely want some robust form of ground detection for your character controller as well. Assuming your method of ground detection can return information about the ground you're on (typically, via a RaycastHit structure), you can modify your velocity and input to hog the ground.

My own character controller, in its current iteration, looks like this:
using UnityEngine;
using System.Collections;

public class Mover : MonoBehaviour {

    public PawnAnimator animator;
    MoveInput input;
    new Rigidbody rigidbody;
    GroundTest groundTest;

    public float speed = 1;

    public Vector3 worldVelocity { get; private set; }
    public Vector3 localVelocity { get; private set; }
    Vector3 lastPosition;

    void Awake()
    {
        input = GetComponent<MoveInput>();
        rigidbody = GetComponent<Rigidbody>();
        groundTest = GetComponent<GroundTest>();
        animator = GetComponentInParent<PawnAnimator>();
    }

    void Start()
    {
        lastPosition = transform.position;
        targetRotation = transform.rotation;
    }

    void UpdateVelocity()
    {
        worldVelocity = (transform.position - lastPosition) / Time.deltaTime;
        lastPosition = transform.position;

        localVelocity = transform.InverseTransformDirection(worldVelocity);
    }

    Vector3 moveDelta;

    void ClampToGround()
    {
        Vector3 dir = groundTest.groundHit.point - transform.position;
        dir = Vector3.Project(dir, transform.up);

        moveDelta += dir;
    }

    void Move()
    {
        if (groundTest.isGrounded)
        {
            Vector3 forward = Vector3.ProjectOnPlane(transform.forward, groundTest.groundNormal).normalized * input.localInput.z * input.localInput.z * speed;
            moveDelta = forward;

            Vector3 gravity = Physics.gravity * Time.deltaTime;
            moveDelta += gravity;
            
            ClampToGround();
        }
        else
        {
            moveDelta = input.worldInput * speed;
            Vector3 gravity = Vector3.Project(worldVelocity, Physics.gravity) + Physics.gravity * Time.deltaTime;
            moveDelta += gravity;
        }
    }

    Quaternion targetRotation;
    public float turnRate = 100;
    public float turningAngle;
    void Turn()
    {
        if (input.hasInput) targetRotation = Quaternion.LookRotation(input.worldInput, transform.up);

        turningAngle = Quaternion.Angle(rigidbody.rotation, targetRotation);
        rigidbody.rotation = Quaternion.RotateTowards(rigidbody.rotation, targetRotation, Time.deltaTime*turnRate);
    }

    void FixedUpdate()
    {
        moveDelta = Vector3.zero;
        UpdateVelocity();
        groundTest.TestGround();

        Move();
        Turn();

        rigidbody.velocity = moveDelta;
        DebugEX.DrawArrow(rigidbody.position, rigidbody.position + rigidbody.velocity, 0.1f, Color.magenta);

        if (animator) AnimationUpdate();
    }

    void AnimationUpdate()
    {
        animator.currentController.SetFloat("velocityX", localVelocity.x);
        animator.currentController.SetFloat("velocityY", localVelocity.y);
        animator.currentController.SetFloat("velocityZ", localVelocity.z);

        animator.currentController.SetBool("isGrounded", groundTest.isGrounded);
        animator.currentController.SetBool("isMoving", input.hasInput && (localVelocity.x+localVelocity.z) > 0.1f);
    }
}
(Where PawnAnimator handles interactions with the Animator component, - I'm using Playables API to blend between override controllers for weapon swaps and so on - the GroundTest component handles testing if we are grounded or not and can provide a RaycastHit describing the 'best' point of ground beneath us and MoveInput is the character's reciever for input, following UE4's 'Pawn, Controller' model.)

Hope this helps!
4 Comments
Leo
8/18/2016 04:16:42 pm

Thank you, amazing read.. indeed, been struggling for more than 1 year now, with no near to usable solution, all the things we have to do only to get the "slide " and "is grounded", trying with sphere cast and unity's character controller... buggy asfk... Are you making a packet soon? would like to try, then pay for somthing like this..

Reply
Christopher
8/19/2016 03:03:51 am

No packet yet, but I can give a rough overview of the components I use and how they work. That'd be a second blog post though haha.

Unity's CharacterController is naff, to be honest. I cannot honestly recommend it.

Implementing your own sliding takes a bit of work and I haven't got it down perfectly yet.

Ground detection is a bit of a funny one; I've wound up using SphereCast with a sphere that starts within the character capsule, fires down and uses a smaller radius. It works, but has to be set up carefully to prevent problems. Using a simple 'raycast downwards' ground test often results in "hangs" where the controller things it's in the air (especially on slopes).

For the actual movement and state code, I use a set of different components:

MoverInput receives move input and does various operations with it (ie detecting if it currently wants to move, detecting 'flicks' / very short inputs that can be used to turn the character in a direction rather than move it). Can be subclassed for extra behaviours ie JumpMoveInput and so on.

GroundTest handles ground checking and information.

Then we have a Mover component and various subclasses of MoverState component, ie GroundedState, FallingState.
Mover acts as a 'hub', taking data from MoverInput and GroundTest to determine which MoverState should be 'active'. It queries the 'active' MoverState for what the next velocity vector should be and applies it to the rigidbody.

Main advantage to this system is that you can set up wildly different movement states with relative ease, and it's neatly exposed through the Inspector (ie all the controls for how the character falls is in the FallingState component and so on).

Reply
MckinneyVia link
11/5/2021 03:53:37 am

Very much appreciated. Thank you for this excellent article. Keep posting!

Reply
Incall Massage England link
12/30/2022 04:05:18 am

Interesting thoughts.

Reply



Leave a Reply.

    Author

    A UK-based amateur game developer.

    Archives

    May 2017
    May 2016
    April 2016
    October 2014
    December 2013
    November 2013

    Categories

    All
    Scripting And Theory
    Shaders Are Fun
    Shaders Are Hilarious
    Unity 4

    RSS Feed

Powered by Create your own unique website with customizable templates.