Making far-off monsters sleep to fix a slow dungeon
The Breakout Site ran at 9 frames per second once 58 monsters went in. Two causes, neither of them collision, and the fix that took it to 60 and up.
The Breakout Site was running at 14 to 15 frames per second when I played it, ever since the 58 meat monsters went in. I measured it in a window in the surgery and got 8.8. Now monsters that are standing idle with the party far away skip their physics step, and the same room runs at 60 to 83. The fight with the Meat Monster on the top floor went from 7 to 66 to 78.
Why was it slow?
I’d said I thought it was collision, because the debugger showed the time in physics. It wasn’t. When I reproduced it, every frame was hitting the cap of 8 physics steps, and each step took 13 to 19 ms. The game runs physics at 120 steps per second, so a step has 8.3 ms. Once steps cost more than that, the engine runs extra steps to catch up, and the extra steps make the next frame slower.
I switched things off to see where it went. With the monsters’ physics step off, the surgery ran at 56 frames per second. With the monsters gone entirely it was 81 to 89. So the monsters were most of it, but something else was eating about a quarter even with none.
What was a monster doing?
Each of the game’s 109 monsters (58 of them in the Breakout Site) ran its AI and its slide on every one of the 120 steps, wherever the party was. One monster’s step cost 0.106 ms. The slide, the actual collision, was 0.008 ms of that. Another 0.020 ms was the line-of-sight check, and 0.063 ms was a single function, DungeonPlan.dug_rect(), which gives the bounds of everything dug in a dungeon. It read every square of the sheet on every call, and every moving monster asked it of each of the 7 dungeons at every step.
The second cost was the 80 named places. Each one is an Area3D and they were on the common physics layer, so every one watched every wall, piece of furniture and monster inside its box. That was 960 bodies and 2.3 ms per step, which is what was left with no monsters at all. Only the party was ever told about, so none of it did anything.
How does a monster sleep?
There’s a small static class, MonsterSleep, and one question at the top of Monster._physics_process:
if MonsterSleep.sleeps(self, _party):
return
A monster sleeps only while all of these are true:
- Its mind is at rest. A chasing mind must be idle, not chasing or walking home. A leashed one must have no raised ally standing anywhere. A stationary one always qualifies.
- It doesn’t fly, because a flyer hovers and bobs.
- It’s standing on its spawn point, within 0.05 m, on the floor, not moving.
- The party is further away, level, than the range it notices at, plus 6 m. The 6 m is room for a tank monster to put its guard up on its pack before the party can strike.
Nothing is saved. The question is asked again every step from what the monster is and where the party is, so a hit from anywhere, a call from its room, or the party coming inside the range wakes it on the next step. A monster that was sent home, respawned, loaded, pushed or dropped isn’t at rest until it has stood on its spot again.
Cooldowns, the AI’s tick and the weekly respawn run off the game clock’s signal and not the physics step, so those carry on while it sleeps. As a safety net, each sleeper takes one whole step per second anyway, offset by its instance id so they don’t all land together. If something takes the floor out from under a sleeper far away, it’s put right within a second.
The other two fixes were small. dug_rect() now keeps its last answer while the sheet’s width, square size and cells are unchanged, which took a monster’s whole step from 0.106 to 0.034 ms. And Place now sets its collision layer to 0 and its mask to a layer that only the party is on, so the areas watch one body instead of 960. I left the physics rate at 120. The fix is to do less per step, not to take fewer steps.
Did it work?
I measured three runs in the surgery with another agent rendering on the same machine, using 18 to 49 percent of the processor. They gave 59.6, 73.6 and 83.0 frames per second, at 1.5 to 2.0 physics steps per frame. Two more under heavier load gave 55 and 70. The top floor in the Meat Monster fight gave 66.0, 70.6 and 77.5. With no monsters at all it’s 113 to 118. In the film’s overlay, 100 of the 109 monsters are asleep in the surgery, and 94 to 97 when I’m near a room.
I added a test for the rule with 42 checks, and a frame-time test for the site that runs alone, so it isn’t measured while other tests render. I haven’t run the game from play.bat by hand to check the figure there, and I never measured the commit just before the monsters went in, so “monsters gone” stands in for it.