A force that does nothing
Whatever Floats Your Boat runs at about 200 fps until you walk near a boat, at which point it does not. I spent a day chasing that, wrote five patches against the engine, and most of them did nothing for my framerate. The one real bug I found was not the one I was looking for, and it turned out not to be my problem at all.
This is the honest version of that day, because the wrong turns were more instructive than the fix.
Numbers first
The symptom was easy to state and hard to act on. Standing near a boat, framerate fell off a cliff. So I measured three states and got three numbers: 155 fps with the boat at rest, 66 while a player shoved it, 50 while someone drove it.
Those numbers are close to useless in that form. FPS is a rate, and rates do not subtract. The question I actually needed answered was how much work the boat adds, and work is time.
Converted, the three states are 6.45 ms, 15.2 ms and 20 ms. So a shoved boat costs about 8.8 ms per frame and a driven one about 13.5 ms. Now they are budgets I can go looking for, rather than a scary-sounding ratio.
This is worth internalising before you profile anything. Optimising a 200 fps scene to 250 fps sounds impressive and saves you 1 ms. Optimising a 30 fps scene to 35 fps sounds modest and saves you 4.8 ms. The second one is nearly five times the work.
Wrong turn one: the draw calls
My first theory was rendering. The engine batches parts into merged meshes, but only anchored ones. Boats move, so every plank and crate on them falls out of that path and becomes its own mesh instance. Hundreds of them per boat. That sounded exactly like a drop that scales with boat count.
It was wrong, and the reason is worth knowing. Every part of a given material shares one
mesh and one material, because the engine caches both. The per-part colour rides an
instance uniform, which lives in per-instance data rather than in a separate
material. That is precisely the case a modern renderer merges back into instanced draws.
The parts were not each costing a draw call. They were already being batched, just by the
graphics driver instead of by the engine.
I had reasoned from "these are separate objects in the scene tree" to "these are separate draws on the GPU", and those are different layers.
Wrong turn two: the one I want to talk about
The second theory was about sleeping. Physics engines stop simulating bodies that have settled, which is the single biggest reason a scene full of props is affordable at all. I had a boat that would sometimes hang in the air, frozen, until a player touched it. So: the body falls asleep, and while asleep it ignores the buoyancy force holding it up.
That story is half right, and the half that is wrong took reading the engine's source to kill.
Polytoria runs Jolt. In Jolt's Godot module, every per-call force entry point ends the same way:
void JoltBody3D::apply_central_force(const Vector3 &p_force) {
...
jolt_body->AddForce(to_jolt(p_force));
_motion_changed();
}
void JoltBody3D::_motion_changed() {
wake_up();
}
Applying a force wakes the body. So does applying an impulse, and so does setting velocity directly. My theory required the opposite, and the source says otherwise in four words. I had written a patch on that assumption. It was a no-op, and I threw it away.
I had also written a second patch on the assumption that unfreezing a body does not wake
it. Same outcome: set_mode calls wake_up() whenever it switches
a body to a non-static motion type. Also a no-op, also discarded.
The bug that was actually there
There is one force path that does not behave like the others, and it is the one you would reach for if you wanted to be efficient.
A constant force is not applied per call. You set it once and the engine keeps
applying it every step until you clear it. In the scripting API that is
ForceMode.Acceleration. Setting it wakes the body, exactly like the others.
But the applying happens somewhere else entirely:
// _integrate_forces(), called from pre_step()
jolt_body->AddForce(to_jolt(total_gravity / inverse_mass + constant_force));
jolt_body->AddTorque(to_jolt(constant_torque));
pre_step() only runs for active bodies. So the stored force
exists, and the body is not being stepped, and therefore the force does nothing. Not
"less", not "damped". Nothing.
The failure that follows is nasty because every layer looks correct in isolation. Your script is still running. It applied the force successfully and got no error back. The engine still has the force stored and would happily tell you so. The body just hangs there.
The figure is tuned to make it obvious, but the real conditions are ordinary: a force that nearly balances gravity, so the resulting motion is slow. Hover platforms, lifts, anything holding station. Those are precisely the cases where you would use a stored force rather than reapplying one every tick, and precisely the cases where the motion is slow enough to trip the deactivation timer.
The fix is not clever. If a body has a standing constant force or torque on it, it should not be allowed to sleep, because a body under a standing external force has to be simulated for that force to mean anything:
bool noStandingForce = GDRigidBody.ConstantForce == Vector3.Zero
&& GDRigidBody.ConstantTorque == Vector3.Zero;
if (GDRigidBody.CanSleep != noStandingForce)
{
GDRigidBody.CanSleep = noStandingForce;
if (!noStandingForce) GDRigidBody.Sleeping = false;
}
That is Polytoria #1068. It costs you sleeping on those specific bodies, which is the correct trade. Sleeping is an optimisation for bodies nobody is pushing.
What this did for my framerate
Nothing, and this is the part I would have quietly left out of a cleaner writeup.
My buoyancy applies force every tick with ForceMode.Force, not as a stored
constant. So it wakes the body on every call and never hits this bug at all. I found a
real defect in a code path my own game does not use.
Where those 8.8 and 13.5 milliseconds actually go is a separate question, and I got it wrong the first time I wrote this paragraph. My guess was that a boat is many bodies joined by constraints, and that I was paying for constraint solving. It is not, and I was not.
When a part is nested under another physical, the engine reparents its collision shape into the ancestor's body rather than giving it a body of its own. A boat is therefore one rigid body carrying hundreds of collision shapes, and there are no joints in it at all. That is the right way round: the alternative, hundreds of bodies wired together with constraints, is the version that would genuinely scale badly.
The cost is maintaining that compound. Every child collider has its transform rewritten as the assembly moves, and each write invalidates the shape the physics engine is holding. So the bill scales with how many colliders the boat has, which is a different problem with a much more boring fix: give the decorative parts no collider and let a handful of boxes stand in for the hull. Nothing about that touches how the boat floats, because the buoyancy code works from stored part poses and never looks at a collision shape.
Two things I took from it
Check the premise in the source, not in your head. Both discarded patches were built on confident beliefs about when a physics engine wakes a body. Both took about ten minutes to disprove once I opened the actual file. I had spent considerably longer than ten minutes writing them.
Watch for evidence that is your own reasoning coming back around. At one point I found a comment in my own codebase describing the exact cost I suspected, and treated it as confirmation. It was not independent. I had written it, from an earlier version of the same guess. Any belief that arrives too neatly is worth tracing back to where it actually came from.
The buoyancy system this all sits on top of is written up separately, with figures you can capsize.