Custom Character Controller in Unity: Part 2 – Implementation

Now that we’ve gone over the basics of character controller collision resolution, I’m going to demonstrate how to implement the last presented example (the pushback method) into Unity.

To start off, make sure you have Unity downloaded and installed. For this article I am using Unity 4.3.4f1. (To check your version of Unity, go HelpAbout Unity…) Open an existing project or create a new one for this tutorial. Create a new scene and create a Cube and a Sphere Game Object within it. Although we’ll eventually move on to using a Capsule shape for our controller, we’ll start with a Sphere to keep it simple. Rename the Sphere to Player and the Cube to Wall. Change the Wall’s scaling factor to 6 on each axis. To ease visualization, I also added a blue transparent material to the player and a green transparent material to the wall. Remove the Sphere Collider component from the player.

This sure beats making those dumb diagrams in Photoshop

This sure beats making those dumb diagrams in Photoshop

Create a new C# script and name it SuperCharacterController.cs, to express our dominance as the alpha character controller. Assign this script to our player, and then copy and paste the following code into it:

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

public class SuperCharacterController : MonoBehaviour {

 [SerializeField]
 float radius = 0.5f;

 private bool contact;

 // Update is called once per frame
 void Update () {

 contact = false;

 foreach (Collider col in Physics.OverlapSphere(transform.position, radius))
 {
 Vector3 contactPoint = col.ClosestPointOnBounds(transform.position);

 Vector3 v = transform.position - contactPoint;

 transform.position += Vector3.ClampMagnitude(v, Mathf.Clamp(radius - v.magnitude, 0, radius));

 contact = true;
 }
 }

 void OnDrawGizmos()
 {
 Gizmos.color = contact ? Color.cyan : Color.yellow;
 Gizmos.DrawWireSphere(transform.position, radius);
 }
}

…and that’s all, really. Run the project and open the Scene window, while it’s still running. Drag the player around the edges of the wall and attempt to slowly push him into it. You’ll notice the wall resists, and keeps the player flush against it’s edge. So what are we actually doing here?

Physics.OverlapSphere returns an array of Colliders that are contacted by the sphere. It’s a great function in that it doesn’t come with any of the caveats of the other various methods in the Physics class (which we’ll inspect in more detail later). You define an origin and a radius and it gives you the colliders, no frills.

With any collisions detected, we now need to perform resolution. To retrieve the closest point on the surface of the box collider, we use the ClosestPointOnBounds method. We then take a vector that points from the contactPoint to our location. The vector’s magnitude is then clamped and our position is “pushed” out of the collider the proper amount.

You’ll also notice that I implement OnDrawGizmos so that it’s easy to see when the OverlapSphere is colliding with an object.

 Two frames demonstrating the collision being detected, and then resolved

Two frames demonstrating the collision being detected, and then resolved

Fairly simple. Unfortunately our success up until this point has been…an illusion. Create a new class named DebugDraw.cs, and add in the following code.

using UnityEngine;
using System.Collections;

public static class DebugDraw {

 public static void DrawMarker(Vector3 position, float size, Color color, float duration, bool depthTest = true)
 {
 Vector3 line1PosA = position + Vector3.up * size * 0.5f;
 Vector3 line1PosB = position - Vector3.up * size * 0.5f;

 Vector3 line2PosA = position + Vector3.right * size * 0.5f;
 Vector3 line2PosB = position - Vector3.right * size * 0.5f;

 Vector3 line3PosA = position + Vector3.forward * size * 0.5f;
 Vector3 line3PosB = position - Vector3.forward * size * 0.5f;

 Debug.DrawLine(line1PosA, line1PosB, color, duration, depthTest);
 Debug.DrawLine(line2PosA, line2PosB, color, duration, depthTest);
 Debug.DrawLine(line3PosA, line3PosB, color, duration, depthTest);
 }
}

This is a useful helper function of mine that allows us to draw markers in the editor from anywhere in the code (as opposed to just the OnDrawGizmos function). Modify the foreach loop to look like this.

foreach (Collider col in Physics.OverlapSphere(transform.position, radius))
{
Vector3 contactPoint = col.ClosestPointOnBounds(transform.position);

DebugDraw.DrawMarker(contactPoint, 2.0f, Color.red, 0.0f, false);

Vector3 v = transform.position - contactPoint;

transform.position += Vector3.ClampMagnitude(v, Mathf.Clamp(radius - v.magnitude, 0, radius));

contact = true;
}

Run the code, and you’ll notice when a collision happens a large red cross hair is drawn on it’s location. Now, drag the player inside the wall and observe that the marker follows the player. This isn’t necessarily wrong of the ClosestPointOnBounds function, but to match our pushback model from the previous section we really wanted a ClosestPointOnSurfaceOfBoundsOrSomething.

I can't believe this free game engine doesn't do exactly everything I want all the time

I can’t believe this free game engine doesn’t do exactly everything I want all the time

The main issue here is that we cannot properly resolve collisions when our character’s origin is inside a collider, as we do not have a function that will correctly find the nearest point on the surface. For now however, we’re going to move on to the next problem with our current implementation.

Rotate the wall about 20 degrees either way on it’s y axis and then run the scene. You’ll notice nothing seems to work properly anymore. This is because ClosestPointOnBounds returns the closest point on the axis-aligned bounding box, not the object-oriented bounding box.

Axis-aligned bounding bound on the left, with object-oriented on the right

Axis-aligned bounding bound on the left, with object-oriented on the right

You can already imagine how this problem will extend beyond just Box Colliders. Since the function is only capable of returning the axis-aligned bounding box, it clearly will not give us the closest point on the surface if we’re colliding with any other collider type (Sphere, Capsule, Mesh, etc.). Unfortunately, there is no silver bullet for this issue (or not one I’m aware of); we’ll need to implement a separate algorithm for each collider type.

Let’s start with the easiest first: Sphere Colliders. Create a new Sphere game object in the scene. There are a few steps to finding the nearest point on the surface, none of which are too complicated. To know which direction to push the player, we calculate the direction from our position to the Sphere’s centre. Since every point on a Sphere’s surface is the same distance from the origin, we normalize our direction and then multiply it by our radius and our local scale factor.

The following code implements the above. You’ll notice that in addition to the new method I’ve also added in a conditional check to see what kind of collider our OverlapSphere has detected.

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

public class SuperCharacterController : MonoBehaviour {

 [SerializeField]
 float radius = 0.5f;

 private bool contact;

 // Update is called once per frame
 void Update () {

 contact = false;

 foreach (Collider col in Physics.OverlapSphere(transform.position, radius))
 {
 Vector3 contactPoint = Vector3.zero;

 if (col is BoxCollider)
 {
 contactPoint = col.ClosestPointOnBounds(transform.position);
 }
 else if (col is SphereCollider)
 {
 contactPoint = ClosestPointOn((SphereCollider)col, transform.position);
 }

 DebugDraw.DrawMarker(contactPoint, 2.0f, Color.red, 0.0f, false);

 Vector3 v = transform.position - contactPoint;

 transform.position += Vector3.ClampMagnitude(v, Mathf.Clamp(radius - v.magnitude, 0, radius));

 contact = true;
 }
 }

 Vector3 ClosestPointOn(SphereCollider collider, Vector3 to)
 {
 Vector3 p;

 p = to - collider.transform.position;
 p.Normalize();

 p *= collider.radius * collider.transform.localScale.x;
 p += collider.transform.position;

 return p;
 }

 void OnDrawGizmos()
 {
 Gizmos.color = contact ? Color.cyan : Color.yellow;
 Gizmos.DrawWireSphere(transform.position, radius);
 }
}

The astute reader may have noticed that this ClosestPointOn method actually returns the closest point on the surface of the Sphere, unlike the ClosestPointOnBounds which returns the closest point within the bounds. This is handy, but we have a few hurdles to jump before we’re able to make much use of this. For now, let’s tackle the second (and final for today) type of Collider we’ll implement: object-oriented bounding boxes.

Image demonstrates how the vector direction between the origin of the sphere and the location of our controller is extrapolated to give us our nearest point

Image demonstrates how the vector direction between the origin of the sphere and the location of our controller is extrapolated to give us our nearest point

Our general approach to this algorithm will be to take the input point and clamp it within the extents of the box. This will give us the same behaviour as the built in ClosestPointOnBounds, except we’ll ensure that it works even if the box has a rotation other than the identity.

The extents of the Box Collider are defined as it’s local size in x, y, and z. In order to clamp our point to the local extents of the Box Collider, we need to transform it’s position from world coordinates to the local coordinates of the Box Collider’s transform. Once we do that, we can clamp the point’s position within the bounds. To get our final point we then transform it back into world coordinates. The final code for the day looks like this.

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

public class SuperCharacterController : MonoBehaviour {

 [SerializeField]
 float radius = 0.5f;

 private bool contact;

 // Update is called once per frame
 void Update () {

 contact = false;

 foreach (Collider col in Physics.OverlapSphere(transform.position, radius))
 {
 Vector3 contactPoint = Vector3.zero;

 if (col is BoxCollider)
 {
 contactPoint = ClosestPointOn((BoxCollider)col, transform.position);
 }
 else if (col is SphereCollider)
 {
 contactPoint = ClosestPointOn((SphereCollider)col, transform.position);
 }

 DebugDraw.DrawMarker(contactPoint, 2.0f, Color.red, 0.0f, false);

 Vector3 v = transform.position - contactPoint;

 transform.position += Vector3.ClampMagnitude(v, Mathf.Clamp(radius - v.magnitude, 0, radius));

 contact = true;
 }
 }

 Vector3 ClosestPointOn(BoxCollider collider, Vector3 to)
 {
 if (collider.transform.rotation == Quaternion.identity)
 {
 return collider.ClosestPointOnBounds(to);
 }

 return closestPointOnOBB(collider, to);
 }

 Vector3 ClosestPointOn(SphereCollider collider, Vector3 to)
 {
 Vector3 p;

 p = to - collider.transform.position;
 p.Normalize();

 p *= collider.radius * collider.transform.localScale.x;
 p += collider.transform.position;

 return p;
 }

 Vector3 closestPointOnOBB(BoxCollider collider, Vector3 to)
 {
 // Cache the collider transform
 var ct = collider.transform;

 // Firstly, transform the point into the space of the collider
 var local = ct.InverseTransformPoint(to);

 // Now, shift it to be in the center of the box
 local -= collider.center;

 // Inverse scale it by the colliders scale
 var localNorm =
 new Vector3(
 Mathf.Clamp(local.x, -collider.size.x * 0.5f, collider.size.x * 0.5f),
 Mathf.Clamp(local.y, -collider.size.y * 0.5f, collider.size.y * 0.5f),
 Mathf.Clamp(local.z, -collider.size.z * 0.5f, collider.size.z * 0.5f)
 );

 // Now we undo our transformations
 localNorm += collider.center;

 // Return resulting point
 return ct.TransformPoint(localNorm);
 }

 void OnDrawGizmos()
 {
 Gizmos.color = contact ? Color.cyan : Color.yellow;
 Gizmos.DrawWireSphere(transform.position, radius);
 }
}

You’ll also notice that I made a few changes to the main collision loop, allowing us to call either the axis-aligned or object-oriented ClosestPointOn in the same line. I say “I made a few changes” in a fairly disingenuous sense in that I really mean “I slightly modified the code that I copied and pasted,” as most of the implementation here is taken from fholm’s RPGController package. You can open the RPGCollisions class within it to check out some of the other alterations I made: namely, updating some deprecated code and replacing the matrix multiplications with the more user friendly TransformPoint methods.

Cut me some slack I got like a C minus minus in Linear Algebra I need all the user friendliness I can get.

Cut me some slack I got like a C minus minus in Linear Algebra I need all the user friendliness I can get

This wraps up the first part of our implementation. In future articles I’ll address some of the shortcomings with Unity’s physics API that I’ve alluded to, and begin to outline various components of our ideal Character Controller we will build.

Maybe. Who knows. I’m just making this stuff up as I go.

References

The majority of the code from this article comes from fholm’s RPGController package, specifically the PushBack method from RPGMotor.cs and the closest point methods from RPGCollisions.cs.

13 thoughts on “Custom Character Controller in Unity: Part 2 – Implementation

  1. Pingback: Custom Character Controller in Unity: Part 4 – First Draft | Roystan Ross

  2. Hi, I’ve implemented this collision system in my game and everything is fine untill one of those two things happen:
    – if I run the game on an old laptop, collisions are often not detected, or better, detected, but after a while something goes wrong and it doesn’t work anymore. I’ve noticed that the frame before the collision fails the Time.deltaTime is bigger than before
    – if I run the game on my pc (windowed mode), everything works fine and the collisions are detected properly untill I move the window. Then everything goes banans.

    Both of this cases seem related to the time passed between the frames, any idea about what could be the issue? Thicker box colliders won’t solve the problem, so I think it’s not a phasing issue

    • Hey, when you say you’ve implemented it, do you mean the code from this above tutorial or the code from the GitHub project? In the full SuperCharacterController, you can set a fixed timestep to ensure that even with a low deltaTime a certain amount of iterations are run.

      If it’s just this code there might be an issue with the ClosestPointOn method with BoxColliders, since I think I recall fixing that in the Git repo awhile back but don’t recall if I updated it here.

      • I’ve implemented the code above and I added some of the missing features, like capsule collision, from the git hub repository.
        I’ll study a bit more the code as I think it’s related to what you mentioned about the box colliders.
        Thanks again for the support, I’ll let you know what is wrong (hopefully).

      • I’ve been playing around for a while with your project and it has the same issues.
        I open the TestZone, set the Dubug move Z to 10, start the game, jump over the thing untill you reach a wall and the player collides. Now try to scale the game view tab in the editor, the character should phase through the wall.
        Same if you build the project and you run it in windowed mode; instead of scaling the view just hold your mouse a bit on the top of the window as if you wanted to move it, then release.

        I think we’ve got some unity mistery here

      • I’ve solved the issue.
        The problem was that if the Time.deltaTime value is too high the distance the character moves in one frame is more than it’s radius.
        The solution I found was to test the collisions every half radius along the distance between the start position and the hypotetical end position.
        It turns out that this operation is not at all expensive as most of the time you collide on the first or second iteration and it’s well worth the effort of implementing it as it fully solves phasing, at really high speeds, on really laggy machines,and while manipulating assets, views or windows in the editor, as this causes an increase of the deltaTime value.
        Thanks again for your tutorials and goo luck with your future projects!

        • Hey Bon, I completely missed these messages, just saw them now after seeing a notification about another message. Glad to see that you fixed the issue; if your solution is flexible and robust, feel free to commit a branch to the Git repo!

          • I’ll try to implement my solution into your project as a challenge. I implemented my solution into my own stripped down version of the controller (without all the fancy slope stuff, mesh colliders and additive pushback, I just needed it to work in 2d on mobile platforms, saving as much power as possible) because that’s what I needed, but I don’t really mind to learn something while fitting it inside a more complex project. Challenge accepted : 3

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s