Custom Character Controller in Unity: Part 6 – Ground Detection

Despite having written five posts about the Super Character Controller, up until now I’ve only briefly touched on the issue of ground detection. Knowing what your controller is standing on is a hugely important topic, since a great many of your player’s actions will depend on what kind of ground he is standing on, if any at all. Good ground detection can make all the difference between a smooth playing experience and a terrible one.

An example of bad ground detection.

An example of bad ground detection

So what do we want to know about the ground beneath our character? We definitely need to know how far away it is. We’ll want to know if our character has his “feet” touching the surface of ground or if he’s 6 meters above it. We also will want to know the location of the point on the surface of the ground directly below us, as this is important for ground clamping, highlighted in a previous post. Thirdly, we will want to know the normal of the surface directly below us, a direction represented by a Vector3. And lastly it will be necessary that we know the GameObject that the ground belongs to, to allow us to retrieve any attached components to the object that may be relevant to grounding. (In the Super Character Controller, we retrieve a SuperCollisionType component that describes properties on the object).

Looking at an earlier post surveying the Unity Physics API, the most obvious solution to our problem comes in the form of Physics.Raycast. We can fire a ray directly downwards at the ground below us. Through the RaycastHit structure we can retrieve the contacted point, the distance traveled, the normal of the surface, and the object the ray collided with. This seems to fulfill all of our requirements at first glance, but looking more closely reveals a significant problem.

Our character controller is represented by a series of spheres to form a capsule, which means that when he is standing on directly flat ground, the nearest point on the surface of the ground will be exactly radius distance from the center of the lower sphere in the capsule. This is fine, but problems appear once the character is standing on a slope. If we now fire a raycast directly downwards (as before) the point we contact is no longer the closest point to our “feet.” This will cause issues with our ground clamping method (among other things).

Raycast

Ray is cast directly downwards from the center of the lowest sphere in the controller. As the controller is standing on a slope, the point directly below is NOT correct, and when the controller is clamped to that point it introduces an error where the controller is slightly clipping into the slope. For steeper slopes, this error will become more pronounced

Luckily, we have a savior in the form of Physics.SphereCast. Instead of casting a thin ray downwards, we cast a sphere (if it wasn’t already clear enough in the name). This will solve the above issue by ensuring that our controller is properly aligned to the surface below us, regardless of it’s normal.

While SphereCast works extremely well in representing our controller’s “feet,” it comes with a few issues. The first is one you will encounter regardless of what you’re using SphereCast for: when the SphereCast contacts the edge of a collider (rather than a face directly on) the hit.normal that is returned is the interpolation of the two normals of the faces that are joined to that edge. You can think of it similar to the Vector3.Lerp method, with the normals of the two faces being your from and to values. The value would then be represented by how far the contacted point was from the center of the sphere that contacted the surface; if it hit dead center, the from and to would be equally weighted with a value of 0.5, and as you move along the edge it would increase or decrease.

Animation demonstrating SphereCast hit.normal interpolation. Yellow wire sphere is the origin point; red is the target. The green vector represents the hit.normal. Notice how as the SphereCast moves over the edge of the box, the normal is interpolated between the two normals of the joining faces.

Animation demonstrating SphereCast hit.normal interpolation. Yellow wire sphere is the origin point; red is the target. The green vector represents the hit.normal. Notice how as the SphereCast moves over the edge of the box, the hit.normal is interpolated between the two normals of the joining faces

I discovered fairly early on that it was necessary to know the actual normal of the surface you are standing on, rather than just the interpolated normal. To solve the problem presented by SphereCast’s interpolation, I would follow up the SphereCast with two separate Raycasts aimed at each of the two joining faces which would retrieve for me the correct normals. (In the Super Character Controller’s ProbeGround method, these are called nearHit and farHit, representing the normals of the closest and furthest face from the center of the controller, respectively.)

The next issue with SphereCast is specific to using it for ground detection but highly essential to ensuring proper accuracy. We’ve been assuming that anything the SphereCast collides with is valid ground that our character can stand on (or slide or, in the case of some slopes for certain games). In practice however this is not always true! We can reasonably assume that the physical surfaces of a game world (that the controller collides with) can be divided into ground and walls, with the idea that only the ground surfaces should be detected by ground probing methods (like the SphereCast defined above). The simplest way to partition the world into ground and walls would be to say that any surface with any angle (relative to the Vector3.up of the world) less than 90 degrees is ground, while surfaces angled 90 and above are walls and cannot be detected as ground. Therefore we should ensure that we never mistakenly detect a wall in our ground probing method. Naively it looks like this problem is implicitly solved by the nature of our ground probing: we are casting a SphereCast directly downwards, which means it should (in theory) never contact a 90 degrees wall. However, very often the normals of the walls in a game world will only be near 90 degrees, or somewhere between 85 and 90. We want to treat these 85 degree angle surfaces as walls, meaning that they should be ignored in our SphereCast.

SphereCastWallAngle

Our controller is flush up against an 85 degree angle wall. Due to this slight angle in the wall, our SphereCast’s contact point is at the yellow X marker, rather than the surface directly below us. This will cause our controller to believe he is standing on a steep slope, rather than safely on flat ground

The most obvious solution would be to use Physics.SphereCastAll. It would ideally collide with both the steep wall and the flat ground, and we could iterate through all contact points to decide what we are standing on. Unfortunately, SphereCastAll only picks up a single contact point per object, so if the ground and wall are part of the same mesh collider, this solution will not work.

Trying something simpler, we could just reduce the radius of the SphereCast by a small amount to account for the above error. And for very slight errors, this does solve some problems, but not all. We still need to find a way to reliably retrieve the proper ground when flush up against a steep slope.

[ Note: In the Super Character Controller, the angle of a “steep slope” is defined as the value StandAngle in the SuperCollisionType component attached to each object the controller collides with. ]

To do this, let’s make the assumption that there is some sort of ground beneath us, and that if there was not a steep slope blocking us our SphereCast would have contacted it. To retrieve this ground, we can Raycast down the steep slope our SphereCast hit. This is primarily to verify that there is some sort of ground there, and (more importantly) to retrieve it’s normal.

RaycastDownSlope

Initial SphereCast contact point marked in yellow. Because we have contacted a steep slope, we Raycast (shown in red) down the slope to detect the ground that is actually beneath us (Raycast contact point marked in purple)

This tells us what’s below us, but because we used a Raycast, and not a proper SphereCast, we once again run into the problem presented earlier, where the shape of our ground detection does not correctly align with the shape of the bottom of the controller. Given the information we have (the normal of the surface below us), can we somehow transform our Raycast data into an approximation of SphereCast data? Yessir.

When you SphereCast downwards from the bottom of the controller and contact a surface, there exists a relationship between the normal of the surface and the point on the sphere that the contact takes place. When we Raycast down the steep slope have the normal of the proper ground surface, so it’s our job to find the point along the bottom of the controller that a SphereCast would connect with. We also have the planar direction (from top view) towards the point (given by the direction of the slope below us), allowing us to tackle this problem in 2-dimensional space (finding a point on a circle, as opposed to a sphere) and then convert our result to 3-dimensional space.

spherecast_point

Animation showing how the contact point of a SphereCast is directly related to the normal of the surface it collides with. SphereCast origin in yellow, contact in red, with the contact point marked in light blue. Notice how as the slope becomes steeper the point moves further up and along the edge of the red circle

Since we are attempting to find a point in 2d space, we are looking for two values which we can call and y. If calculated properly, x and y will describe a point along the position of our circle. Referring to the diagram above as an example, we would be given the normal of the ground surface, and our job would be to calculate the position of the light blue point.

Luckily, this really isn’t all that difficult. Referring back to grade 8 math, we can use the Sine and Cosine functions to calculate the x and y positions, with the normal of the surface being the angle we pass in.

x = Mathf.Sin(groundAngle);
y = Mathf.Cos(groundAngle);

Pretty handy. Note that in Unity the Sine and Cosine methods require you to pass in the angle in radians, so ensure you convert your angles beforehand.

SinAndCosine

Calculating the contact point (light blue) from the angle of the green slope using the Sine and Cosine functions

We can now use these values to find our point by adding them to the position of our controller (multiplied by it’s radius). Effectively, we’ve now converted our Raycast data in SphereCast data, and nobody is the wiser.

[ Note: In the Super Character Controller, the method for approximating SphereCast data based off a surface normal is called SimulateSphereCast. ]

This sums up the current technique I am using for ground detection (in the Beta controller). Unlike previous articles, the question of finding a solution to the problem of detecting ground is fairly openthe above is by no means the optimal solution, although I’ve found it works very well in practice. I plan to write a follow-up to this showing how to actual use the data we’ve worked so hard to retrieve, since it’s a somewhat involved process.

Advertisement

15 thoughts on “Custom Character Controller in Unity: Part 6 – Ground Detection

  1. Hi, it’s me again.

    I would really like to see more about ground detection.

    I also have a question about this ground detection method.

    If the Spherecast detects a steep slope, the character raycasts down the slope to detect the “correct” ground.

    But what if there was a hole right where the raycast should detect something?
    There would be some sort of ground that is actually beneath the character, but the raycast wouldn’t detect anything.

    In my opinion the character should SPHERECAST down the slope, that would make more sense.

    BUT, the contact point given by that spherecast can be wrong, since you’re not spherecasting directly downwards.

    So, here’s a possible solution.

    If the spherecast detects a steep slope, first spherecast down the slope, and get the contact point.
    Then, to get the correct normal (and to avoid the interpolation between two normals that you mention here) of the “correct” ground, I think you should raycast so that its contact point is equal to the other one given by the spherecast (or maybe spherecast with a radius of like 0.0001 to avoid errors when raycasting directly at an edge of a mesh, as you say in a previous article).

    Once you have the “correct” normal, just “simulate” the spherecast using your method called “SimulateSphereCast”.

    This would be VERY expensive though, as you’re spherecasting twice and raycasting once in a frame, especially if your character has continuos collision detection, which needs a capsule cast every frame.

    So, what do you think about this solution? Please let me know.

    Leonardo.

    • Hey Leon,

      The interpolated normals is the main reason I went with a raycast as opposed to a SphereCast. That said, you are correct that there is an edge case where ground may not be properly detected by a raycast. It’s an interesting solution, and I’ll look into trying an implementation of it sometime (unless you were planning to). The most essential part is ensuring you get the proper normal of the “correct” ground beneath the controller, since that normal is passed into the SimulateSphereCast and defines the grounding data that is actually used in the end.

      Either way I’m going to try to get a cleaned up release version of the V2.0 project (the current project is a mess of prototypes and other nonsense) uploaded sometime next week, so hopefully it will be easier for people to contribute or analyze the code.

      • Hey, thanks for answering.

        Yeah it’s an interesting solution, but, as I said, it’s pretty expensive.

        Also, what if there wasn’t any ground that is actually beneath the character, but there’s some sort of ground on the left of the character, in the picture with the red and yellow crosses? (I’m sorry but I can’t put photos in here)

        The spherecast would detect it, but you don’t want it since it’s not directly below the character, but to the left.

        What you should do is to test if the contactpoint of the sphere is “below” the character.

        To do that, you should use a point-cylinder collision detection algorithm, or a point-capsule collision detection algorithm

        The cylinder (or capsule) starts at the center of the “feet” sphere and then goes infinitely down. (basically a ray cylinder)

        Keep in mind that the cylinder will always be axis-aligned, so the algorithm will be simpler.

        In 2D, this would really easy actually. Instead of a cylinder it would be a, infinite rectangle, and if point.x >= XminRectangle && point.x <= XmaxRectangle && point.y <= YMaxRectangle than the point is inside the infinite rectangle and the point is actually below the character.

        In 3D (cylinder) I think you can just do a double 2D test. (check if top-circle overlaps point, then check if rectangle overlaps point).

        Or maybe it doesn't matter if the contact isn't directly below the character???

        I'm a little confused…

        Also, sorry about my bad english, it's not my native language.

        • Ok I just realized that was a mistake, as it is irrilevant that the contact is “directly” under the player… (So don’t bother read that)

          Anyway, I like your articles, and I would like to see more.

          For example, what about “StepOffset”?? I’m really courious about how you would implement this in your controller.

          Also, what about the ” the furthest we can move is exactly equal to twice our radius” problem? You said: “One solution to this problem is to run your controller’s physics more than once per frame. Alternatively, we can use CapsuleCastAll to check if there are any colliders between our initial and final position every frame. We’ll explore both these options in future articles where we continue to implement the character controller.”

          How would you implement continuos collision detection (“CapsuleCastAll between initial and final position” solution) in the Super Character Controller?

          Leon

          • StepOffset is kind of a weird one, since I’m not really sure how Unity implements it. Since the controller is capsule-shaped, it naturally will slide over small steps. However, this isn’t a “true” StepOffset since in Unity’s Character Controller you can adjust it to any level.

            I don’t think it would be THAT hard to implement, but much like the grounding problem it would likely have lots of edge cases.

            As for the “twice our radius” problem, the current version of the SCC has an option to use a fixed timestep, where it will run it’s internal physics update X times per frame to ensure that even if the framerate is low the physics are constant (which means it could run multiple times per frame).

            For continuous collision detection you would need to CapsuleCast from your origin point to the point you moved to in order to retrieve any colliders the controller “skipped” if it’s moving extremely fast. This is overall pretty easy to implement so it’s really something I should do.

            As always, thanks for your interest and replies.

            • No problem, It’s always a pleasure to help implementing this fantastic (and, most importanty, rotable) Character Controller!

              About continuos collision detection, I think you should CapsuleCast more than once per frame, once to get the colliders from your start position to you end position, but you can’t just stop the character to the contact point of the CapsuleCast.

              You should get the colliders from start to end position, then you should “correct” the movement, so that you get another end position. Then you should CapsuleCast again to see if that position is actually correct and there aren’t colliders between the start position and that one.

              Here’s an article to better explain myself that may help for implementing continuos collision detection: http://www.wildbunny.co.uk/blog/2011/03/25/speculative-contacts-an-continuous-collision-engine-approach-part-1/

              Leon

              • Yea, I’ve looked into continuous collision detection—I actually originally tried to implement the controller purely using CapsuleCasts, which have the obvious problem that they don’t detect anything within the capsule’s origin. I’ll look into that article then have a go at it sometime, thanks for the link.

  2. Hey, your work is amazing, and thank you for sharing it! I have been recently been playing around with your Super Character Controller and evaluating its utility for a project that I’m currently working on it. A requirement that I had was being able to handle the motion and collisions of up to four characters in different formations.

    As I explored the limits of your controller, I attempted to change the radius of the spheres that compose the collision mesh. I was met with spectacular failure, as the capsule continued to fall through the floor. After changing the dimensions of the art object and the capsule collider’s dimensions, I was not met with success. After reviewing your code, it’s still not clear to me how I would go about changing the dimensions of the Player object’s collision mesh without breaking your Super Character Controller. Have you met these problems as well?

    I am currently using the 2.04 Beta Project, at the moment.

    • You have four possibilities:

      1)- Use a rigidbody character. This can be a problem if you have slopes in your game because it slides off slopes, or if it doesn’t, it gets stuck on walls.
      https://unity3d.com/learn/tutorials/modules/beginner/2d/2d-controllers

      2)- Use a raycast-based character controller. I personally don’t like this solution because it is very expensive and not accurate. (and doesn’t work with capsule-shaped characters)
      http://deranged-hermit.blogspot.it/2014/01/2d-platformer-collision-detection-with.html , or
      https://www.youtube.com/watch?v=dqKudBbp3_I , or
      http://www.gamasutra.com/blogs/YoannPignole/20131010/202080/The_hobbyist_coder_1_2D_platformer_controller.php

      3)- You can just use this controller, but “translating” everything to 2D, but this wouldn’t work with box-shaped characters

      4)- You can even have a controller BETTER than this one in 2D. Why? Because you can have a real capsule instead of just three spheres (or circles). So first you need a “OverlapBox” function (that allows rotated box). These are the steps you have to follow to create this sort of function. First, find the coordinates of the four corners of the box, with this function:

      Vector2 localPoint = pointToRotate – pointOrigin;

      Vector2 rotatedLocalPoint = new Vector2 (localPoint.x * Mathf.Cos (rotation) – localPoint.y * Mathf.Sin (rotation),
      localPoint.y * Mathf.Cos (rotation) + localPoint.x * Mathf.Sin (rotation));

      Vector2 rotatedWorldPoint = rotatedLocalPoint + pointOrigin;

      return rotatedWorldPoint;

      PointOrigin is the center of the box and pointToRotate are the non-rotated coordinates of the four corners.

      Once you do that, you need to find the point with the shortest X (using Mathf.Min (point1.x, point2.x, point3.x, point4.x)), the bigger X, the shortest Y and the bigger Y. Now you can use the Unity function “OverlapArea”, to check which colliders are colliding with the Axis-Aligned-Bounding-Box. (Physics2D.OverlapAreaAll (new Vector2 (shortestX, biggerY), new Vector2 (biggerX, shortestY))).

      Now we have to check if the colliders are overlapping the rotated-box too.

      For each collider in the OverlapAreaAll array, you have to check if that collider is a circle, a box, a polygon, or an edge collider. (if (col is CircleCollider2D)).

      If it’s a circle, “un-rotated” center of the circle. (theFunctionThatFindsRotatedCoordinate (circleCenter, centerOfBox, -rotationOfBox))

      And then follow this: http://www.migapro.com/circle-and-rotated-rectangle-collision-detection/

      If it’s a box, since the rectangle is a convex polygon, you can use the SAT (Separating Axis Theorem), which you can find here: http://gamedevelopment.tutsplus.com/tutorials/collision-detection-using-the-separating-axis-theorem–gamedev-169 , or here:
      http://rocketmandevelopment.com/blog/separation-of-axis-theorem-for-collision-detection/

      If it’s a polygon, this would be more complex. First, you need to copy and paste this code translated by me: http://answers.unity3d.com/questions/977416/2d-polygon-convex-decomposition-code.html .

      This code uses Bayazit’s algorithm to decompose a concave polygon into several convex polygons.

      foreach convex-polygon, check if it overlaps with rectangle. If at least one of them does, then the concave polygon overlaps the rotated rectangle.

      Why am I doing this? Because SAT only works with convex polygons.

      If it’s an edge collider, check if each line intersects the box with the SAT. (note that the axes of a line is the Vector perpendicular to the direction from one point to the other).

      You have to deal with collision response too. For this you can use SAT.

      This solution works with both box and capsule shaped characters. (as a 2D capsule is a rectangle with one circle on the top and one circle on the bottom)

      The third and fourth solutions are the best, but if you’re lazy (or beginner), you’re probably going to choose the first or the second ones.

      I hope it helps, good luck.

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 )

Connecting to %s