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)
Loading editor…

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.