A dynamic character controller for Bullet?

Or, how I tried to create a dynamic character controller for Bullet, but eventually (almost?) gave up.

Motivations

I like spending some time using the Bullet physics library, I already wrote some articles about my experiments with it. This time, I wanted to create a custom character controller.

Bullet alread provides a (kinematic) character controller, but in general it is not regarded as a good one. I also tried to study a bit its code, but, in my opinion, it adds many complications without reason, and even something like managing in the correct way the velocity is a big problem with it. Indeed, Erwin Coumans himself (the main Bullet developer) said that btKinematicCharacterController is not supported anymore.

Therefore I decided to write my own one.

There are two kinds of character controller: kinematic ones and dynamic ones.

The former use the Physics engine, if any, just for collisions, and compute all the movements by themselves. This was the only way at the beginning of the video games, since there was not any Physics engine at all, but only some code for specific purposes.

With dynamic character controllers, the character is a normal rigid body, with some constraints, e.g. the impulses/forces are set not to make it rotate. The controller sets the dynamic properties of the body, then the engine computes the rest.

The general wisdom is to stick with kinematic controllers: they are a bit more difficult to start with (you need to deal with everything), but you have the complete control, which allows you to reach exaclty the results you want. In this way, for example, you can create a more relaxed and/or forgiving controller, that might improve the gameplay, or you can to create some strange effects more easily.

At least, these are the reasons that got me to start with a dynamic controller, and then (almost?) give up with it. It was easy to start with, it required some hacks with ramps and stairs were a nightmare.

Nevertheless, I decided to write this article to leave some notes about my attempt, or as a base, if somebody wants to improve and fix it. This few thoughts could also be helpful also to start with a kinematic controller.

The interface

Bullet has a btCharacterControllerInterface but I am quite sure it is part of the not supported code. Indeed, the rest of the code never mention it, whereas it refers its parent, btActionInterface (e.g. in btDyanmicsWorld::addAction), which I decided to extend.

The only two methods we need to implement are updateAction, which is called by the simulation process, and debugDraw, that can be used to draw some debug graphics, but is not essential to the controller.

In addition to that, we are going to create a method to set the movement direction, one to verify whether it is possible to jump, and one to eventually do that. However these methods are just going to set some status members, that can be cleared with the method resetStatus.

I decided to follow the Bullet approach: it is possible to change the character behavior by setting directly a series of public members, e.g. maximum speed, jump speed, acceleration, movement damping, etc.

As an exception, I decided not to make also the movement direction public because I wanted it to be always normalized. Still, it is protected, so child classes can modify it without calling the method to do so, as long as the resulting vector has norm 1.

Please notice that what follows assumes a Z-up reference frame.

Character construction

I decided that the owner of the character should own also its body and its shape.

The reasons for this are that so the creator of the character can decide to create the body as it wants, with all the various parameters. And by owning also the shape, it can reused it also for other characters, since Bullet suggests to do so whenever possible.

For reasons I will explain later, I eventually decided to impose the strong requirement of a capsule shape, of which the controller needs to know the radius and the height of the cylindric part, but will not do anything with the shape object itself.

Another approach would be pass the parameters to create the body (i.e. the btRigidBody::btRigidBodyConstructionInfo object), and a series of shapes, to implement also ducking or similar features, without giving their ownership to the controller.

But before doing anything with this rigid body, the controller needs to change a pair of setting of the body, e.g. disable unwanted rotations:

mRigidBody->setSleepingThresholds(0.0, 0.0);
mRigidBody->setAngularFactor(0.0);

Finding ground

The first step to do during the update is finding ground. This influences all the rest of the actions.

The simplest way to detect ground is to cast a ray, see whether it intersects other objects and the distance at which it does that.

Initially I used this approach, but it leads to known problems on ramps and on platforms (the character might be detected not on ground if the lowest points of the capsule is outside the platform).

I decided to use another approach, then: to look for collisions, that is useful also for other purposes, e.g. looking for stairs.

The character may collide with many objects, or also collide with other objects but not with ground. Therefore I decided to consider only the collisions in a certain area as collisions to ground.

This is not difficult to do, but it requires some knowledge about the shape of the body, and this is the main reason for the capsule shape constraint.

class FindGround : public btCollisionWorld::ContactResultCallback {
public:
	btScalar FindGround::addSingleResult(btManifoldPoint &cp,
		const btCollisionObjectWrapper *colObj0, int partId0, int index0,
		const btCollisionObjectWrapper *colObj1, int partId1, int index1)
	{
		if (colObj0->m_collisionObject == mMe && !mHaveGround) {
			const btTransform &transform = mMe->getWorldTransform();
			// Orthonormal basis (just rotations) => can just transpose to invert
			btMatrix3x3 invBasis = transform.getBasis().transpose();
			btVector3 localPoint = invBasis * (cp.m_positionWorldOnB - transform.getOrigin());
			localPoint[2] += mShapeHalfHeight;
			float r = localPoint.length();
			float cosTheta = localPoint[2] / r;

			if (fabs(r - mController->mShapeRadius) <= mRadiusThreshold && cosTheta < mMaxCosGround) {
				mHaveGround = true;
				mGroundPoint = cp.m_positionWorldOnB;
			}
		}
		return 0;
	}

	btRigidBody *mMe;
	// Assign some values, in some way
	float mShapeHalfHeight;
	float mRadiusThreshold;
	float mMaxCosGround;
	bool mHaveGround = false;
	btVector3 mGroundPoint;
}

// ...

void DynamicCharacterController::updateAction(btCollisionWorld *collisionWorld,
	btScalar deltaTimeStep)
{
	FindGround callback;
	collisionWorld->contactTest(mRigidBody, callback);
	mOnGround = callback.mHaveGround;
	mGroundPoint = callback.mGroundPoint;

	// ...
}

Basic movement

Next, we need to implement the basic movements. We wanted to know whether we are at ground because the character can accelerate only on it, and if it is not moving, we need damp the movement.

// In the DynamicCharacterController::updateAction
btVector3 linearVelocity = invBasis * mRigidBody->getLinearVelocity();
if (mMoveDirection.fuzzyZero() && mOnGround) {
	linearVelocity *= mSpeedDamping;
} else if (mOnGround) {
	btVector3 dv = mMoveDirection * (mWalkAccel * deltaTimeStep);
	linearVelocity += dv;
	// We do not really need to be that picky, but we can do that. The important is to ignore the up direction
	btScalar speed2 = pow(linearVelocity.x(), 2) + pow(linearVelocity.y(), 2);
	if (speed2 > mMaxLinearVelocity2) {
		btScalar correction = sqrt(mMaxLinearVelocity2 / speed2);
		linearVelocity[0] *= correction;
		linearVelocity[1] *= correction;
	}
}

// TODO: Jump

mRigidBody->setLinearVelocity(basis * linearVelocity);

As I commented, this code is missing the jump. Since we are using a rigid body, we have to implement it dynamically. There are several way to do that: applying an impulse, a force, or change the vertical velocity.

I decided for the latter, as it is independent from the mass and the behavior is easily predictable, i.e. it is possible to compute the speed from the jump duration/height.

To jump I created a pair of method, like the ones of the btCharacterControllerInterface:

bool DynamicCharacterController::canJump() const
{
	return mOnGround;
}

void DynamicCharacterController::jump(const btVector3 &dir)
{
	if (!canJump()) {
		return;
	}

	mJump = true;

	mJumpDir = dir;
	if (dir.fuzzyZero()) {
		mJumpDir.setValue(0, 0, 1);
	}
	mJumpDir.normalize();
}

so the jump() is not really immediate, rather it queues up a jump request for the next update:

// Replaces the previous TODO
if (mJump) {
	linearVelocity += mJumpSpeed * mJumpDir;
	mJump = false;
}

Actually, to me this implementation does not feel very natural: if a character starts jumping, its direction cannot be changed.

For now, I changed the } else if (mOnGround) { to } else if (mOnGround || linearVelocity[2] > 0) {. In this way, after the character starts jumping, players can still change its direction, but they cannot do that when they are falling. An alternative would be to keep track of the time that the character has been jumping, and allowing the change of direction only for a certain time period.

Ramps

Up until here it was quite easy. The trickiest part, in my opinion, was detecting ground, althoguh a simple ray would have worked in a flat world.

But ramps introduce also some other problems. One is that if you leave the character there, it will start sliding down.

I managed to solve this with a small hack: if we are already on ground, we set the gravity to 0. Notice that bullet provides a btRigidBody::clearGravity, but that is not enough.

So, after the jump code we should add something like this:

if (mOnGround) {
	mRigidBody->setGravity({ 0, 0, 0 });
} else {
	mRigidBody->setGravity(mGravity);
}

And there is another problem, with ramps (the highest the ramp, the more apparent): when passing the peak, the character jumps. I have not resolved this problem, yet.

Stairs

Stairs are a very interesting element for levels, but, in my opinion, they are also the real problem for dynamic controllers.

Actually, there is an easy solution: in the Physics engine representation, replace all the stairs with ramps.

Step detection

But if you do not want to do that, you need to introduce some specialized code. I adapted some suggestions I found on the net.

As I mentioned before, when we are looking for ground, we can also check if there are some collisions that might be steps.

To actually decide whether the step is valid, you need to know the ground position, and we have two options. One is saving all the collisions in a list (and a linked list is better than an array for this task, in my opinion), then checking all of them after we checked that we are on ground. The other one is to approximate ground with the lowest point of the capsule, also on ramps, after all the error might be considered negligible, and at the end consider the step valid only if we had also detected ground. The rest of the algorithm is the same in the two cases.

One of the most useful tricks of the article I linked is that, given a collision point, to be a valid stair, its normal must have a small up component.

If that is the case, we can look for a point up on the step, to measure its height. We we move a little bit (e.g. a pair of centimeters) along the direction to the step, and go up to the maximum height we want to automatically step in the vertical direction. From this point we cast a vertical ray.

Finally, we check if the character fits on the step. I did this with a pair of rays, but I think that this is not very robust. I tried also with btCollisionWorld::convexSweepTest but I did not have any luck.

Step climbing

This is where I have stopped for a long time. I actually found also some solutions, but none was perfect. Many had an overshoot, so the character went higher than the real point, and with a camera attached to it, the effect was unpleasant.

The focal point is that with a dynamic character, the engine responds to every change you do, either directly or indirectly (e.g. if you move the character, it will compute the speed corresponding to this displacement).

Eventually, while writing this article, I came up with the ultimate hack: it is possible to toggle the status of kinematic object to a btRigidBody, by using btCollisionObject::setCollisionFlags!

The auto step is not that trivial, and if the cancel/stop condition is not well made, the character might fly. Or, if the character is not moved also horizontally, at the end it may be not on ground, and it may fall to the initial point because of gravity, which might even end in an infinite loop. Of course, the movement direction must be checked before stepping, otherwise the character might rise while going on the opposite direction.

I also encountered a nasty problem: in some circumstances the character entered the step, but I still have not understood if the problem is in my code, or something else, e.g. a wrong collision. Luckily, turning the rigid body dynamic again also solves all penetrations. But in some cases it might also assign an angular velocity to the body, that is not damped. So, before restoring the initial flags of the body, we’d better set any angular velocity to 0.

The code for step management is the only long part of the controller, so you can find it directly on the downloads.

Conclusions and download

Well, conclusions are a bit too difficult to draw, yet. Eventually I obtained a sort of hybrid character controller, and it took me a lot of time. I do not like some of the solutions (toggling the gravity and the flags), but in my small tests they seem to work.

I want to move on with some other topics, so for now I will keep the character as is, but I do not exclude to reimplement it as kinematic, only tests will tell me about that.

After all having a dynamic character makes possible some interesting developments, e.g. using soft bodies for bouncing falls and similar stuff.

Finally, you can download my attempts. They come without any warranty, but also without any restriction: I dedicate them to the public domain.