Influence Maps
Pathfinding tells an entity how to reach a destination. It doesn’t tell the entity which destination is worth reaching. An influence map fills that gap: a second grid, the same dimensions as your tile map, where each cell holds a numeric value representing some property of that location — how dangerous it is, how much of the area a faction controls, how recently the player passed through.
The guard doesn’t need to pathfind to every tile to know that standing in the open near the player is a bad idea. It reads the influence map, finds the cell with the highest “danger” value, and moves away from it. Spatial reasoning without exhaustive search.
What You’ll Master:
- Building an influence map by BFS propagation from sources
- Controlling spread through a decay factor
- Visualising influence as a heat map overlay
- Using influence for guard decision-making
- Combining multiple maps for layered spatial reasoning
Prerequisites:
- BFS (influence propagation uses the same flood-fill pattern)
The red intensity of each tile shows its influence value — brightest at the player, fading outward through walls. The blue guard reads its neighbours each frame and steps toward the least-influenced tile, keeping clear of the player without pathfinding.
What an Influence Map Is
An influence map is an array of floats parallel to your tile map:
// Same dimensions as the tile map
const influenceMap = Array.from({ length: ROWS }, () => new Array(COLS).fill(0));
You populate it by propagating values outward from sources — entities, events, or locations that project influence into the world. A player projects “player presence”. A guard projects “guard territory”. An explosion projects “recent danger”. The values decay with distance so nearby tiles have higher influence than distant ones.
Building the Map: BFS with Decay
The propagation is BFS from each source. Each neighbour receives the parent’s value multiplied by a decay factor:
const DECAY = 0.75; // 0.0 = no spread, 1.0 = infinite spread, no decay
function buildInfluenceMap(sources, map, rows, cols) {
const inf = Array.from({ length: rows }, () => new Array(cols).fill(0));
const queue = [];
const visited = new Set();
// Seed the queue with all sources
for (const source of sources) {
const tx = Math.floor(source.x / TILE_SIZE);
const ty = Math.floor(source.y / TILE_SIZE);
inf[ty][tx] = source.strength;
queue.push({ x: tx, y: ty, value: source.strength });
}
let head = 0;
while (head < queue.length) {
const { x, y, value } = queue[head++];
const key = `${x},${y}`;
if (visited.has(key)) continue;
visited.add(key);
for (const dir of DIRS) {
const nx = x + dir.x, ny = y + dir.y;
if (nx < 0 || nx >= cols || ny < 0 || ny >= rows) continue;
if (map[ny][nx] === WALL) continue; // walls block influence
const newValue = value * DECAY;
// Only update if this is the strongest path to this tile
if (newValue > inf[ny][nx]) {
inf[ny][nx] = newValue;
queue.push({ x: nx, y: ny, value: newValue });
}
}
}
return inf;
}
The decay factor controls spread radius. A decay of 0.75 means influence halves every ~2.4 tiles (0.75^2.4 ≈ 0.5). A decay of 0.9 spreads much further.
Using Influence for Decisions
Once you have the map, decisions become table lookups. No pathfinding, no line-of-sight checks — just read the value at a tile and compare.
Move toward highest influence (pursue player):
function getBestNeighbour(entityTileX, entityTileY, infMap, mode = 'highest') {
let bestValue = mode === 'highest' ? -Infinity : Infinity;
let bestTile = null;
for (const dir of DIRS) {
const nx = entityTileX + dir.x;
const ny = entityTileY + dir.y;
if (!isWalkable(nx, ny)) continue;
const value = infMap[ny][nx];
const isBetter = mode === 'highest' ? value > bestValue : value < bestValue;
if (isBetter) { bestValue = value; bestTile = { x: nx, y: ny }; }
}
return bestTile;
}
// A guard that moves toward player influence
const target = getBestNeighbour(guard.tileX, guard.tileY, playerInfluence, 'highest');
// A guard that avoids player influence
const escape = getBestNeighbour(guard.tileX, guard.tileY, playerInfluence, 'lowest');
Check if a position is safe (above a threshold):
const DANGER_THRESHOLD = 0.3;
function isSafe(tileX, tileY, infMap) {
return infMap[tileY][tileX] < DANGER_THRESHOLD;
}
Combining Multiple Maps
Real games layer multiple influence sources. Player presence, guard patrol coverage, and recent combat all paint different pictures of the world:
// Build separate maps for different concerns
const playerInfluence = buildInfluenceMap([{ x: player.x, y: player.y, strength: 1.0 }], ...);
const guardInfluence = buildInfluenceMap(guards.map(g => ({ x: g.x, y: g.y, strength: 0.8 })), ...);
const dangerInfluence = buildInfluenceMap(recentExplosions.map(e => ({ x: e.x, y: e.y, strength: 1.0 })), ...);
// Combine into a composite "threat" map
const threat = computeThreat(playerInfluence, dangerInfluence);
// Subtract guard coverage to find unpatrolled areas
const unpatrolled = computeUnpatrolled(guardInfluence);
A faction AI might assign new patrol routes to tiles with low guard influence. A player might look for corridors with low player-influence to see where they haven’t explored.
Performance
Building an influence map via BFS is O(width × height) — acceptable for most games when run every few frames. It doesn’t need to run every tick. A decay update every 5–10 frames is often indistinguishable from per-frame updates and costs a fraction of the CPU:
let influenceAge = 0;
let cachedInfluence = null;
app.ticker.add(() => {
if (influenceAge++ % 6 === 0) {
cachedInfluence = buildInfluenceMap(sources, ...);
}
// Use cachedInfluence for decisions this frame
});
For very large maps, build only the portion visible to the camera, or use a coarser grid (one influence cell per 4×4 tile area) and interpolate.
Next up: Steering Behaviours — smooth, continuous movement as a layer on top of the tile grid.