EchoQuest v0.19.7: Server Game Loop Optimization
Last update we added profiling instrumentation across the server. Now we’re putting that data to work. This release is Phase 3 of the performance plan — targeting the AI tick loop, the single biggest CPU bottleneck on the server. The result: dramatically less work per tick, with zero impact on gameplay.
The Problem
EchoQuest’s enemy AI runs at 10 ticks per second. Every tick, the server iterates every active map, every enemy on that map, and runs aggro checks, pathfinding, attacks, and position broadcasts. On a busy server with 20 maps and hundreds of enemies, most of that work is wasted — idle enemies far from any player don’t need to think every 100ms.
Tiered Tick Rates
Not all enemies need the same update frequency. Enemies now tick at different rates depending on their state and distance to the nearest player:
- Combat enemies (chasing or attacking) — every tick, no change
- Nearby idle (within 400px of a player) — every 4th tick
- Distant idle (beyond 400px) — every 10th tick
- Returning to spawn — every 2nd tick
Each enemy gets a random “tick bucket” (0–9) on spawn so the load is distributed evenly across frames rather than spiking on a single tick. The distance check uses a new nearest() method on the spatial grid, which expands outward from the enemy’s cell ring-by-ring for an efficient O(nearby) lookup.
Empty Map Skipping
Previously, the tick loop iterated every map that had ever spawned enemies — even maps with zero players. Now the loop checks the player presence map first and skips empty maps entirely. No array allocation, no function calls, just a .has() check and a continue.
Zero-Copy Player Data
The AI loop calls getPlayersOnMap() every tick to get player positions for aggro checks. The old implementation created spread copies ({...data}) of every player object on every call — safe, but wasteful since the AI loop never mutates this data. A new getPlayersOnMapDirect() returns direct references, eliminating thousands of object copies per second.
Boss AI Spatial Grid
Normal enemies already used the spatial grid for aggro range queries, but boss AI was still doing an O(n) scan of all players on the map. Bosses now use the same playerGrid.query() path, bringing them in line with the rest of the AI and removing the last linear scan from the hot path.
Map Dormancy
Maps that have been empty for 60 seconds are now put into a dormant state. Enemy data is freed from memory, and the map is flagged as sleeping. When the first player enters a dormant map, it wakes up instantly and re-initializes enemies from the database — same as if the map were loading for the first time. Dungeon instances are excluded since they have their own lifecycle.
Graceful Degradation
The server now monitors its own tick performance and automatically sheds non-critical work when overloaded. A rolling average of the last 10 tick durations determines the degradation level:
- Level 0 (<80ms) — Normal operation
- Level 1 (>80ms) — All idle enemies tick every 10th frame
- Level 2 (>150ms) — Position broadcasts slow to every 4 ticks; minion AI paused
- Level 3 (>250ms) — Only combat enemies tick; broadcasts every 6 ticks
Escalation is immediate; de-escalation requires 10 consecutive ticks below the threshold to prevent flapping. The current level is exported as a Prometheus gauge (eq_degradation_level) for monitoring.
Results
With no players online, tick times are sub-1ms (previously ~1ms). Under load, the combined effect of tick budgeting, empty map skipping, and zero-copy data should reduce the P95 tick time significantly. The degradation ladder ensures that even under extreme load, combat remains responsive while lower-priority work is temporarily shed.
Try the EchoQuest Demo — free in your browser. Full game coming to Steam.