The journey to get Jolt’s Character to not fall to it’s doom

Published by

on

After, 2 weeks of intense debugging I finally have Jolt’s Character class working in my game! Picking up where I left off last, my character had an unfortunate feature of falling through the world. I was convinced this was a problem with how I was loading my FBX landscape mesh. Whenever you are totally convinced something is a problem, the best course of debugging action is to prove it, so you can reduce your problem space.

Proving it was the FBX Mesh

The quickest way to prove it was my landscape that was causing the problem was to replace it with a simple 3d box for the ground. Luckily many examples exist in the Jolt samples application to do this:

Body &Test::CreateFloor(float inSize)
{
	const float scale = GetWorldScale();
	Body &floor = *mBodyInterface->CreateBody(BodyCreationSettings(new BoxShape(scale * Vec3(0.5f * inSize, 1.0f, 0.5f * inSize), 0.0f), RVec3(scale * Vec3(0.0f, -1.0f, 0.0f)), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING));
	mBodyInterface->AddBody(floor.GetID(), EActivation::DontActivate);
	return floor;
}

Easy enough, I removed my complex FBX loading code, replaced it with something similar to above. I re-ran my code in UE5 to make sure my 3d box ground was visible to the physics engine.

Tada!

My character continued to fall through the 3D box. So that sucks, my problem space just incremented by 1.

The necessity of flailing

I like to call this part of the debug process “necessary flailing”. This is where you really don’t know what the problem is so you just kind of re-run the process over and over making minor tweaks, adding print statements, adjusting objects and properties. Over time you start to build up a mental model of potential areas of investigation.

I started to conclude it had something to do with how the memory of objects was being handled. I could just sense that some parts of the character object were losing “something”. Whether it was the shape, some sort of setting, something was being changed. But why?

One thing I did was implement contact listeners for all bodies. Meaning any collision will call physics::Physics::OnContactAdded with the two bodies that collided. During my debugging these were never called.

From here I decided to add move assign, move constructors and destructors to the character::Character class, which houses all the Jolt physics character settings and references. While implementing these class functions I also added logging, what I saw surprised me.

This is how a user is spawned into the world:

{
    //...
	User.add(flecs::ChildOf, Map)
		.set<character::Character>({Logger, Physics->System, Position})
		.set<units::Position>({Position})
		.set<units::Rotation>({});
	   
    Logger->Info("User has been spawned into the world {}!", User.id());
}

So we have our flecs user entity and when we call AddUserToLevel, we set the user as a child of the map and create our character::Character component which has all of our Jolt physics character goodies.

And this is what I see in the logs for just that methods scope:

ControllableCharacter constructor called
Char State: InAir
Controllable Character move assign called
Controllable Character destructor called
User has been spawned into the world 593! <-- we exit method scope here
Controllable Character move assign called
Controllable Character destructor called
Controllable Character move called
Controllable Character destructor called
Controllable Character move called
Controllable Character destructor called

That’s… an insane amount of moves & destructors being called. This was all happening inside flecs, apparently as part of it’s table management, it moves various objects around, and in doing this, calls destructors as well.

Armed with this knowledge, I now wanted to prove it was some combination of this character being added to flecs and Jolt. So I decided to spawn a character as part of the flecs level module (which is a singleton) and just hang on to the reference, never adding it to flecs as a component.

Sure enough I ran my test case and physics::Physics::OnContactAdded and I see Char State: OnGround be printed out to my logs.

Finally a working example to compare against!

I can not stress how important it is to have a working case with as many variables removed. Using this I can finally see what some differences are, one stood out IMMEDIATELY after printing out the positions of the bodies per each frame tick. See if you can spot the problem. BodyID 16777216 is the ground where 16777217 is the Character.

Character managed in flecs:

Post: Body ID: 16777216 Is Invalid: false Num Bodies: 2 Num Tris: 12 Body Layer: 0, Pos: x:0 y:-1 z:0
Post: Body ID: 16777217 Is Invalid: false Num Bodies: 2 Num Tris: 0 Body Layer: 1, Pos: x:0 y:0.9891182 z:0
PhysicsUpdated
Post: Body ID: 16777216 Is Invalid: false Num Bodies: 2 Num Tris: 12 Body Layer: 0, Pos: x:0 y:-1 z:0
Post: Body ID: 16777217 Is Invalid: false Num Bodies: 2 Num Tris: 0 Body Layer: 1, Pos: x:0 y:-126.09011 z:0
PhysicsUpdated
Post: Body ID: 16777216 Is Invalid: false Num Bodies: 2 Num Tris: 12 Body Layer: 0, Pos: x:0 y:-1 z:0
Post: Body ID: 16777217 Is Invalid: false Num Bodies: 2 Num Tris: 0 Body Layer: 1, Pos: x:0 y:-814.09344 z:0
PhysicsUpdated
Post: Body ID: 16777216 Is Invalid: false Num Bodies: 2 Num Tris: 12 Body Layer: 0, Pos: x:0 y:-1 z:0
Post: Body ID: 16777217 Is Invalid: false Num Bodies: 2 Num Tris: 0 Body Layer: 1, Pos: x:0 y:-1479.9579 z:0

Character managed outside of flecs:

Post: Body ID: 16777216 Is Invalid: false Num Bodies: 2 Num Tris: 12 Body Layer: 0, Pos: x:0 y:-1 z:0
Post: Body ID: 16777217 Is Invalid: false Num Bodies: 2 Num Tris: 0 Body Layer: 1, Pos: x:0 y:0.9891182 z:0
PhysicsUpdated
Post: Body ID: 16777216 Is Invalid: false Num Bodies: 2 Num Tris: 12 Body Layer: 0, Pos: x:0 y:-1 z:0
Post: Body ID: 16777217 Is Invalid: false Num Bodies: 2 Num Tris: 0 Body Layer: 1, Pos: x:0 y:0.9670255 z:0
PhysicsUpdated
Post: Body ID: 16777216 Is Invalid: false Num Bodies: 2 Num Tris: 12 Body Layer: 0, Pos: x:0 y:-1 z:0
Post: Body ID: 16777217 Is Invalid: false Num Bodies: 2 Num Tris: 0 Body Layer: 1, Pos: x:0 y:0.93395734 z:0
PhysicsUpdated
Post: Body ID: 16777216 Is Invalid: false Num Bodies: 2 Num Tris: 12 Body Layer: 0, Pos: x:0 y:-1 z:0
Post: Body ID: 16777217 Is Invalid: false Num Bodies: 2 Num Tris: 0 Body Layer: 1, Pos: x:0 y:0.8896775 z:0

If you look at the position of the character managed in flecs you’ll notice some crazy velocity on the y axis. In two frames we go from 0.98 to -126, on the third we are far below the world at -814.

As for the character managed outside, we see a much more natural falling from the starting .989 to .967 then to .933 and so on until it collides with the ground surface (not shown in logs).

This made me think, oh the characters velocity is so fast that it doesn’t have time to do collision, it pierces right through it in a single frame!

Again, thinking there was something wrong with the memory management, I added more references to various intermediary shape objects created by the character.


Creating a Character has a lot of these, here’s the example Character being created in the Jolt Samples:

mStandingShape = RotatedTranslatedShapeSettings(Vec3(0, 0.5f * cCharacterHeightStanding + cCharacterRadiusStanding, 0), Quat::sIdentity(), new CapsuleShape(0.5f * cCharacterHeightStanding, cCharacterRadiusStanding)).Create().Get();
...
// Create 'player' character
Ref<CharacterSettings> settings = new CharacterSettings();
settings->mMaxSlopeAngle = DegreesToRadians(45.0f);
settings->mLayer = Layers::MOVING;
settings->mShape = mStandingShape;
settings->mFriction = 0.5f;
settings->mSupportingVolume = Plane(Vec3::sAxisY(), -cCharacterRadiusStanding); // Accept contacts that touch the lower sphere of the capsule
mCharacter = new Character(settings, RVec3::sZero(), Quat::sIdentity(), 0, mPhysicsSystem);
mCharacter->AddToPhysicsSystem(EActivation::Activate);

I was only storing the CharacterSettings, StandingShape and Character object references. This (should be) enough, but I also added storing the new Capsule(...) object as well.

This seemed to fix the wild velocity because the character fell more smoothly… right through the end of the world again.

Grasping at straws

My final attempt was to walk through and set breakpoints on every call to JPH::Ref (their ref counting wrapper class) Release() calls, hoping to find which destructor or loss of an object differed between my working and non-working examples. The sheer number of calls was enough for me to realize it was trying to find a needle in a hay stack.

Just containerize it

I’ve had enough, I thought, well clearly something about it being moved is causing references to be dropped prematurely, what if I just hide all the Jolt references and objects into another object that will be moved by flecs? And that’s what I did:

struct Container
{
	void HandleInput(JPH::Vec3Arg Dir) { Character->HandleInput(Dir); };

	void PostPhysicsUpdate() { Character->PostPhysicsUpdate();};

	std::unique_ptr<ControllableCharacter> Character = nullptr;
};

Now when spawning my character and putting them into the world I just make a unique pointer, and throw it over to flecs to initialize the character::Container class with it.

auto Physics = GameWorld.get_mut<physics::Physics>();
auto Char = std::make_unique<character::ControllableCharacter>(Logger, Physics->System, Position);

User.add(flecs::ChildOf, Map)
	.set<character::Container>({std::move(Char)})
	.set<units::Position>({Position})
	.set<units::Rotation>({});

I gave this a run and all of a sudden my debugger breaks on:

void Physics::OnContactAdded(const JPH::Body &inBody1, const JPH::Body &inBody2, const JPH::ContactManifold &inManifold, JPH::ContactSettings &ioSettings)
{
	Log->Info("OnContactAdded: New Contact {} contacted {}!", inBody1.GetID().GetIndexAndSequenceNumber(), inBody2.GetID().GetIndexAndSequenceNumber());
}

FINALLY, IT NO LONGER FALLS THROUGH THE WORLD!

Pasted image 20240524221305.png
OK, well I may have to adjust the capsule collision here, but I’m happy, the character moves and everything so I’m calling this a win.

Time to clean up the code and implement the character as a “Virtual” character instead of the base character class as this one doesn’t handle stairs and some other things.