Second Chance DevlogThe Gazette

The pub filled up and nobody ever walked in

unreal-enginenavmeshrecastai-navigationnpcsilent-failure

The Slag & Slurry is the pub in the middle of the game's town, and by yesterday morning it had almost everything it needed: a barmaid who pours, a singer who starts her set at seven, a bingo caller, tables, stools, beer mats. What it did not have was customers who arrive. The test is simple to state — at opening time, a townsperson should walk down the street, push the door open, cross the floor and stand at the bar — and it took two dozen numbered agent sessions in one day to pass it.

Most of that day went on the navigation mesh. This entry is the part worth keeping: four distinct ways a navmesh can be wrong while every instrument you have says it is fine, and one much more expensive lesson about what a navmesh is actually for.

The lounge bar of the Slag & Slurry, patterned wallpaper and pub carpet, wooden chairs and stools around the tables, one figure sitting alone at the bar.
The lounge bar the punters were failing to reach. Almost everything visible here — the tables, the beer mats, the menus on the counter — turned out to be silently carving holes in the floor the NPCs were trying to walk across.

Symptom one: navmesh on every flat surface in the building, and none on any stair tread

The pub is on three floors. The ground floor meshed. The upstairs rooms meshed. The stairs between them did not, so the building was a set of disconnected islands and nothing could ever climb.

Everything I checked ruled itself out, which should have been the first clue. The stairwell is 90 cm clear, against an agent radius of 35. The risers are 21.4 cm against a max step height of 60. Headroom is over two metres against a 144 cm agent. The treads are flat, so slope is not it. Minimum region area was zero, so nothing was being culled for being small. The proxy geometry the engine builds for distant scenery was already excluded from navigation over the pub. Six suspects, six acquittals, and no navmesh.

The cause is that Recast does its arithmetic in whole voxels, not in centimetres. Before it generates a walkable surface, it erodes each region inward by the agent's radius — and it does that by rounding the radius up to a whole number of cells and removing that many. With a cell size of 19 and an agent radius of 35, that is two whole cells, so 38 cm comes off each side. A 90 cm stairwell loses 76 cm and keeps 14 — less than one cell wide, which rounds away to nothing.

Rooms four metres across survive the same erosion without anyone noticing, and that is exactly why the symptom reads as "stairs are special". They are not. They are just the only thing in the building narrow enough for the rounding to eat.

"90 is greater than 70, so it fits" is the trap. The real inequality is in cells: the eroded width has to survive as at least one whole cell, so a narrow feature wants a smaller cell size. The fix was to define a high-resolution navmesh tier with a cell size of 7 and put resolution-modifier volumes over the stairwells and the interior. No global rebuild was needed, because nothing else in the level was using the high tier — it had been left identical to the default, which is worth checking before you assume changing it is expensive.

Three things about those modifier volumes cost time:

  • The volume's area class defaults to the null area. Drop one in to change resolution and

leave that field alone, and you have punched a hole in the navmesh instead of sharpening it.

  • Resolution applies per tile, not per volume. With roughly ten-metre tiles, a

three-metre volume promotes a ten-metre tile. Budget for that.

  • The tool that spawns them snaps to a 10 cm grid, and a volume of this kind could not be

created from script with a usable brush at all — the working route was to duplicate an existing one and move it.

Symptom two: half the building is outside every navigation bounds volume in the level

Before any of that, there was a simpler fault that had been invisible for weeks. The town's navigation bounds volume stops at a certain eastern coordinate. The pub straddles it. The whole east side of the building — including the entire second flight of stairs — was outside every bounds volume in the level, so no amount of tuning was ever going to generate anything there.

Adding a bounds volume around the pub took ground-floor coverage from 11 sample points out of 56 to 30 out of 56. Nothing warned about this. A navmesh does not report the geometry it was never asked to consider.

Here is the one I got confidently, expensively wrong.

Off-mesh links are how an agent crosses a gap the mesh does not cover — a doorway, a step down, a jump. After the stairs meshed, I tested a link and it appeared inert. Then another. Then the long-standing links on the pub's own doors. I wrote it down as a town-wide fault — the project leans on more than two thousand of these in terraced-house doorways — and set a whole session aside for it.

It is false, and the next session refuted it three separate ways.

The test I was using cannot detect a working link. I was casting a navigation ray from A to B and treating a short stop as proof the link was dead. But a navigation raycast walks the mesh surface; by construction it cannot cross an off-mesh link. A raycast that stops at the gap while a full pathfind completes through it is not evidence of a broken link — it is proof the link is carrying traffic.

The general form: before concluding a mechanism is broken, check whether your instrument is capable of observing that mechanism working. Mine was structurally incapable, and it nearly cost a day.

A second claim went the same way. I had recorded that the pub's customers only ever got indoors because their movement was a glide that ignored walls. Also false: they run on real navmesh path-following. The gliding movement belonged to two other characters entirely — the singer and the regular in the corner — and I had generalised from them.

The headline symptom: the route was perfect and nobody ever used it

So by mid-afternoon the pub had bounds, the stairs meshed, the doors linked, and a proven path existed from every customer's front door to a marker on the pub floor. I jumped the clock to opening time and watched.

At the exact moment the pub opened, six people appeared standing inside it.

Not walked in. Appeared. The navmesh work was correct, complete, and had never once been used.

The mechanism is in the crowd director, not in the navigation. The director keeps a small pool of bodies and only creates one when its spawn point is within about 70 metres of the player. And for the first game-hour of a journey it deliberately spawns a traveller at their previous destination — the idea being that they set out from where they were. Every pub-goer's previous destination is their workplace, which is between 260 and 800 metres from the pub. So for the whole travel hour the spawn point sits far outside the radius and no body exists anywhere in the world. The instant the travel timer elapses, the spawn point flips to the arrival point, about a metre from a player standing at the bar, and the person is created there.

Both halves are individually sensible. Together they guarantee a body can never exist anywhere on the route between the two.

The fix is small once it is stated: instead of snapping the spawn point between "start" and "end", interpolate along that line by how far through the journey they are, then project the result onto the navmesh. Someone 90% of the way to the pub now spawns 90% of the way to the pub — inside the radius, on the mesh, walking. With that in, a townsperson spawned about 50 metres out on the street and walked in through the front door under their own steam.

This is the lesson I most want to keep from the day: before debugging why an NPC will not walk somewhere, check whether a body can exist on that route at all. Four sessions of careful, correct navmesh work were spent on a route no character was ever placed on. The navmesh was never the bug, and every measurement I took agreed with itself.

One implementation note, because it is a trap in this project's tooling. The function that needed changing reads back as a compact conditional expression, and the writer for that format cannot express a two-statement branch. Rewriting the function wholesale would have silently changed its logic while looking like a clean round-trip. The safe move was to add a new helper function and swap a single node to call it.

Symptom four: they reach the front door, queue at the back door, and jam in a wall

With people finally walking, they walked to the wrong door. Every one of them approached the front, veered around the building and funnelled to a back entrance, where they wedged themselves 15 cm into the wall.

The front door was standing open. That is the fault.

A swinging door leaf is a static actor, so the navmesh is baked against wherever the leaf sits in the editor, not against where it swings at runtime. The leaf parked open reached into its own doorway and narrowed the walkable gap below the agent radius, so Recast closed the doorway and pathfinding correctly routed around it. The swing direction, which is where I started looking, was never involved. The fix is to clear the "affects navigation" flag on the door leaf — but on the instance, not the class default, because most of the doors in this level are locked and must stay genuinely impassable.

That same flag turned out to be the answer to a separate complaint the developer had been living with for weeks: dragging a prop onto a table moved the navmesh. Of course it did. 178 pub props were set to affect navigation — beer mats, pint glasses, the drinks menus on the counter. Clearing them all shrank the navmesh by about 2.6 KB, a small number that represents a lot of holes.

Anything that sits on a table, a bar, a shelf or a hinge should not be affecting navigation. It is not the default and nothing prompts you.

The inverse of the same problem produced the day's last two blockers. Furniture that does affect navigation grows a second walkable layer on top of itself if it is broad and flat enough. The bar counter had a full navmesh surface 110 cm up, and the low tables had one 9 cm above their tops. Customers were climbing onto them and stalling — the bug report was "a punter is standing on an invisible ledge", and the ledge was a table. Null-area volumes over the counter and the four tables fixed it.

Two probe techniques from chasing those, both learned by getting a wrong answer first:

  • A layer probe needs a floor height per column, not one constant for the room. A single

floor value reported the sloping ground outside the pub walls as nineteen phantom stacked layers, every one of them fictional.

  • **A blanket null-area band across a room at a given height is wrong wherever stairs climb

through that band.** It is easy to fix one bug and sever a staircase in the same edit.

The coverage figure that read 100% because the question was wrong

Late in the day I recorded that the pub's ground floor was fully navmesh-covered, and had to retract it in the same session.

The check asks the engine to project a sample point onto the nearest navmesh, and it takes a search extent. With an 80 cm extent, a point standing in the middle of a hole finds mesh 80 cm away and reports success. Tightened to 10 cm, the same floor reported 66.8%.

A coverage claim made with a generous search extent is worthless, and it fails in the flattering direction — the worst possible bias for a number you are using to decide whether to stop working.

What is still open, plainly

  • The bar is verified working in a live play session. The **fix for customers who leave the

pub still holding a queue slot is written, saved, and has not been played.** That cause was worth the entry on its own: the cleanup that frees a slot only ran for characters that were destroyed, so anyone who walked out alive kept one of only four queue positions and their seat, for ever. It looked exactly like a pathing bug and was not one.

  • The null-area volumes over the tables are built and untested in play. The probe diff is

clean; nobody has watched a customer walk past them.

  • A rule that the barmaid should stop serving while she is calling the bingo is half built.
  • Upstairs, the toilets are reachable by the mesh but only one character actually uses them;

the other plays a stair animation standing still behind a visibility toggle. Known, deliberate, still ugly.

What to take from it

  • Recast erodes in whole cells. For any corridor, doorway or stairwell, do the arithmetic

in cells rather than centimetres — and remember that wide rooms hide the problem.

  • Check the bounds volume before tuning anything. Geometry outside it is not failing to

mesh; it was never considered.

  • Before concluding a mechanism is broken, ask whether your test could observe it working.

A navigation raycast cannot cross an off-mesh link, so it can never confirm one.

  • Check that a body can exist on the route before debugging the route. Spawn rules can

make a perfect path unreachable, and nothing about that looks like a navigation problem.

  • Props affect navigation by default, and static means static. A door parked open, a glass

on a table and a pint on a counter all carve or grow the floor your NPCs walk on.

  • Any measurement with a tolerance will flatter you if the tolerance is loose. Tighten it

until the number gets worse, then believe it.

  • Write the retractions down. Two confident conclusions from yesterday were wrong and were

refuted within hours. The record of why they were wrong is worth more than the original claim would have been if it had been right.

← All devlog entries

Watch it get built. All of this goes up on YouTube as it happens — broken animations, buildings hovering a foot off the ground, the lot.

Subscribe on YouTube