Staying Grounded

Published January 28, 2015
Advertisement
orc.jpg

We all know how important staying grounded is in life, and it is as important for a character controller, especially in an environment with sloping floors.

The problem with constantly applying gravity to a character when using an algorithm like GJK is that when you get back a separation vector from a sloped face, the vector will push you out in the direction of the slope normal, so you end up moved slightly back down the slope from where you entered it. In other words, you gradually seem to slide down the slope, which is not desirable. We want to be able to move on the sloped surface, up to a certain steepness, the same as we do on a flat surface, and with the same velocity.

My solution is to detect when the character is on the floor and only apply gravity when it is not. Also, because we have to use a small range to test for floor contact due to the wonderful world of floating point numbers, I need to be able to lock the character to a fixed distance from the floor when it is within a certain range, so that the character is always at the same height, relative to the floor, regardless of the precise position it was when it crossed the threshold.

But, depending on the slope of the floor, this distance changes for a character with a spherical base.

circles.jpg

The blue line is the distance we are interested in. This can be thought of as the minimum distance, which if we are equal to or less than, is the distance to lock the character to and the point at which to consider the character grounded and stop applying gravity.

It turns out that if we put on our trig hats, calculating this difference is quite straightforward.

trig.jpg

We are effectively treating the distance line as the hypotenuse of a right angle triangle, so if we know the cosine of a and the length of x, y is just x / cos(a). The cosine of a is, helpfully enough, the dot product of the floor normal and the world up vector, leading to the following method:

float sphericalDistanceToNormal(float r, const Vec3 &n){ float d = dotVectors(Vec3(0, 1, 0), n); return d ? r / d : r;}Simple enough. So now the update for the character controller goes something like this:

1) Use a ray cast to find the floor under the proposed new position
2) Figure out if we are close enough to the floor based on its normal to consider ourselves grounded
3) If not, add some gravity to the vertical velocity
4) Move to the new position, using GJK to handle collisions in the normal way
5) Ray cast again at the final position and check the distance to the floor
6) If within range, update the position to be the floor "locked" position

A final little touch is that in point 2), if we are grounded, we can take the proposed movement vector and rotate it so it runs orthogonal to the surface so that movement speed remains constant on sloped surfaces rather than slowing down as the surface gets steeper. Not very physically accurate but makes for a better character controller.

My particular implementation of this looks like this, with a couple of support functions:

float minFloorDistance(float radius, const Vec3 &normal, float margin){ return sphericalDistanceToNormal(radius, normal) + (margin * 2);}HitResult findFloor(const Body *body, const Vec3 &pos, const Vec3 &offset, float radius, Physics &physics){ RayQueryClosestCallback cb(body); physics.rayQuery(Ray(pos + offset, Vec3(0, -1, 0)), cb); HitResult r = cb.result(); if(r.valid()) { float d = dotVectors(r.normal, Vec3(0, 1, 0)); if(d >= 0.8f && r.distance < minFloorDistance(radius, r.normal, physics.margin()) + 0.25f) { return r; } } return HitResult();}void Kcc::move(const Vec3 &step, Physics &physics, float delta){ Vec3 pos = rep.body->position(); Vec3 vel = step * delta; bool flying = step.y > 0; if(!flying) { HitResult floor = findFloor(rep.body.get(), pos + vel, rep.offset, rep.radius, physics); if(floor.valid()) { vel = transformNormal(vel, rotationToQuaternion(Vec3(0, 1, 0), floor.normal)); } else { vel.y -= 8 * delta; } } Vec3 sep = physics.getSeparationVector(rep.body.get(), pos, vel); vel += sep; rep.body->addPosition(vel); if(!flying) { Vec3 pos = rep.body->position(); HitResult floor = findFloor(rep.body.get(), pos, rep.offset, rep.radius, physics); if(floor.valid()) { pos.y = (pos.y - floor.distance) + minFloorDistance(rep.radius, floor.normal, physics.margin()) + 0.05f; rep.body->setPosition(pos); } }}Flying (i.e. moving upwards) is special cased at the moment. You can move up, jet-pack style, by holding space as things currently stand but this will be replaced by jumping later on.

The next step is to look at maintaining some velocity state from frame to frame so that, when in the air, you can accelerate and slow down.

Anyway, just a bit of insight into the inner workings of my character controller for you there. Hope of interest and thanks for reading.
Previous Entry GJK musings
Next Entry It Lives!
5 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement