+ Publish
Pixel Strike #07

Making Bots Feel Human: Navmesh, A*, Limited Senses and Deliberate Mistakes

By @cccrayfish Part 7 of 8

Early on, the bots were wall-walkers: straight lines, into a crate, spin in place. Making them feel human takes several systems stacked on top of each other.

Pathfinding: the hard part

Bake a navigation mesh for each map, run A* over it for waypoints, and have bots follow those waypoints around obstacles instead of driving straight into them.

The build-versus-borrow call here is Part 3's prior-art research actually paying off. There are ready-made libraries like three-pathfinding. I researched it first, then judged: my maps are axis-aligned boxes, and my red line says pure static single file with no extra dependencies — pulling in a heavier library fights both. So I chose to borrow the idea and write a lightweight grid A* myself: bake the grid, run A*, smooth the path. That is not a rejection of open source — it is exactly what "research → assess fit → decide reuse or rewrite" is supposed to produce. More libraries is not better; a small implementation you fully control is often steadier.

How it works: rasterise the walkable area into cells (inflating obstacles by the bot radius), run A* for a cell path, then do a string-pull pass — if you can walk straight to a later waypoint, skip the ones in between — so the route stops looking like stairs. Keep a local feeler-based avoidance layer on top for dodging other moving bots.

Navmesh + A* visualised: green = walkable cells, yellow lines = the A* path, blue and red dots = the two teams.
Navmesh + A* visualised: green = walkable cells, yellow lines = the A* path, blue and red dots = the two teams.
// Bake: a cell is walkable unless it overlaps any box, inflated by the bot radius
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
  const x = ox + (c + 0.5) * cell, z = oz + (r + 0.5) * cell;
  cellWalkable[r * cols + c] = !boxes.some(b =>
    x + R > b.minX && x - R < b.maxX && z + R > b.minZ && z - R < b.maxZ);
}
// A* returns waypoints; each frame the bot walks toward the next one and pops it on arrival

Perception: do not make them omniscient

  • View cone — they only "see" enemies within a certain angle in front of them, not 360°.
  • Line of sight — a wall in between means they cannot see you.
  • Memory — when they lose sight of you they move to your last known position before giving up, rather than forgetting instantly.
  • Patches — an enemy very close by (brushing past) registers regardless of facing, and getting shot makes them turn around. Without those, two bots walk right past each other and neither reacts.

They have to miss — and difficulty is not one knob

  • Aim smoothing — they turn onto the target gradually, and shots fired before they are lined up go wide, the way a real player does when ambushed.
  • Reaction delay and aim error — the machinery of "capable of missing".
  • Difficulty is not a switch — split reaction speed, accuracy, pathing ability, view range and trigger discipline into separate parameters, and roll them per bot at spawn. Now every opponent has a different personality, which reads far more naturally than an "easy / hard" dropdown.
// Roll each bot its own "personality" at spawn, instead of one global difficulty knob
const rnd = (a, b) => a + Math.random() * (b - a);
b.acc   = rnd(0.42, 0.85);   // accuracy
b.turn  = rnd(3.0, 9.0);     // turn / reaction speed
b.react = rnd(0.18, 0.55);   // reaction delay
b.fov   = rnd(1.35, 2.05);   // field-of-view angle
b.view  = rnd(34, 60);       // sight distance
In one line

Difficulty is not an easy/hard switch. It is a combination of reaction, accuracy, pathing and patience.

The prompts I used

The original
Upgrade the bots from wall-walkers into opponents that path around cover:
1) Pathfinding: bake a navigation mesh per map + A* for the route (you may reference the
   three-pathfinding approach; my maps are axis-aligned boxes, so write a lightweight grid A*
   yourself rather than pulling in a heavy library), have them follow waypoints with a
   string-pull smoothing pass, and keep local feeler-based avoidance for dodging other bots;
2) Perception: view cone + line-of-sight occlusion + memory of the last known position —
   not omniscient. Patches: register enemies at very close range regardless of facing, and
   turn toward whoever shot them;
3) Capable of missing: aim smoothing (fire only once roughly lined up) + reaction delay +
   aim error;
4) Multi-dimensional difficulty: split reaction / accuracy / pathing / view range / trigger
   discipline into parameters and roll them per bot at spawn.
Give me the implementation and the tunable parameters for each.
Reusable
Make the NPCs / opponents feel more human:
1) Replace straight-line movement with pathfinding (navigation mesh + A*); write a light
   implementation yourself rather than pulling in a heavy library where you can;
2) Replace omniscience with limited perception (view angle + line-of-sight occlusion +
   memory), plus close-range and being-hit awareness;
3) Use aim smoothing / reaction delay / aim error to make them capable of missing;
4) Split difficulty into several tunable dimensions and randomise them so each individual
   has its own personality.
Give the implementation and parameters for each item.

The game I built

This is what came out the other end of all of it. It opens in the browser, nothing to download — go and break it.

Pixel Strike — free online shooting game on Pogglo

Pixel Strike

@cccrayfish · ♥ 6