Influence Maps avanzados: capas, propagación y predicción
Mapas de influencia multicapa para que los NPCs entiendan el territorio: dónde es peligroso, dónde hay oportunidad, dónde estará el enemigo en 3 segundos.
Publicado: · Por Juanjo "Banyo" López
0. Introducción
Tu enemigo te ve y te dispara. Tu enemigo te pierde y va a tu última posición conocida. Tu enemigo es un poco más listo y patrulla un cono. Tu enemigo, aun así, no entiende el territorio: ataca desde donde está, no desde donde le conviene. Cruza por el centro de la sala cuando hay cobertura a tres metros. Avanza solo cuando tiene dos aliados muertos al lado.
El parche clásico para esto se llama influence map. Lo usaron Total War, Supreme Commander y Civilization para razonar sobre territorios completos; Into the Breach lo usa para que veas, frame a frame, qué casilla va a ser peligrosa el turno siguiente. Es la misma estructura: una grid 2D donde cada celda guarda un número.
¿Qué es un influence map?
Un influence map es una grid 2D donde cada celda guarda un valor que representa la “fuerza” de algo (peligro, control, atracción) en ese punto del mundo. Cuando combinas varias capas, el NPC puede razonar tácticamente sobre el espacio.
Sin influence map, un NPC solo sabe dónde está él y dónde está su objetivo. Con influence map, sabe cómo se reparte el mundo entero según las dimensiones que te interesen.
Lo que cubre este tutorial
- Cómo una unidad propaga su influencia a las celdas vecinas.
- Cómo combinar varias capas (peligro, control, oportunidad) con pesos por rol.
- Cómo proyectar el futuro: dónde estará el enemigo en 3 segundos.
- Cómo enchufar el mapa a tu A* para que el NPC prefiera caminos seguros, no cortos.
No cubre: pathfinding base (mira A* y flow fields) ni decisión táctica de alto nivel (eso es GOAP o utility AI leyendo el mapa).
1. Demo
2. ¿Cómo funcionan los influence maps?
2.1 La capa básica: una unidad propagando influencia
Empieza con lo más simple: una sola unidad, una sola celda con valor 1.0. Eso ya es un influence map degenerado, pero el valor cae a cero a un metro de distancia y no sirve de nada — el NPC necesita saber que la casilla vecina a un enemigo también es peligrosa.
La unidad propaga su influencia hacia las celdas vecinas con un falloff (caída con la distancia). Los dos modelos típicos son:
- Falloff lineal:
valor = source * (1 - dist / radio). Simple, predecible. - Falloff exponencial:
valor = source * exp(-k * dist). La influencia nunca llega a cero del todo pero decae rápido.
Visualmente, una sola fuente con radio 4 y falloff lineal parece un degradado radial:
Radio 4, falloff lineal. La opacidad de cada celda es proporcional a su valor; el número está dibujado encima. Valores redondeados; en el grid real son floats.
2.2 ¿Cómo se propaga la influencia exactamente?
Hay tres formas comunes de propagar, en orden de costo creciente:
- Stamping: para cada fuente, recorres todas las celdas dentro del radio y aplicas el falloff con distancia euclidiana real. O(fuentes × celdas_en_radio). Bien si hay pocas fuentes y radios moderados.
- BFS desde la fuente: empiezas en la celda de la fuente y multiplicas por un
decay < 1en cada vecino. Distancia en pasos de grid. La influencia no atraviesa muros — exactamente lo que quieres para la peligrosidad de un enemigo encerrado en otra sala. - Gaussian blur: stampeas un valor puntual y aplicas un blur 2D al grid entero. Elegante y rápido en GPU, pero atraviesa paredes y suaviza demasiado las puntas.
2.3 Multi-capa: el mundo no es solo “peligro”
Una sola capa es útil pero limitada. Lo que separa un influence map de juguete de uno utilizable es mantener varias capas independientes y combinarlas según el rol del NPC. Las cuatro capas estándar:
- Danger — emitida por enemigos. Radio amplio, falloff suave. Las unidades de asalto pesado emiten más; los francotiradores emiten un radio enorme pero finito.
- Control — emitida por aliados. “Mi escuadra controla esta zona.” Sirve para que el NPC sepa si está en territorio propio o en tierra de nadie.
- Opportunity — emitida por items, cobertura, puntos de captura, flancos abiertos. Lo “deseable” del terreno sin contar enemigos.
- Scent / rastro — la última posición conocida del jugador, decayendo con el tiempo. Conecta directo con la memoria de percepción de NPCs.
Tres unidades, tres capas separadas:
Danger (2 enemigos), control (1 aliado en la esquina superior izquierda) y opportunity (1 item al este). Cada celda guarda los tres valores en paralelo.
Cada celda termina con tres valores independientes. El NPC los lee según le interese.
2.4 Combinación con pesos: la capa “táctica”
La fórmula básica es una suma ponderada:
tactical[cell] = w_control * control[cell]
- w_danger * danger[cell]
+ w_opp * opportunity[cell]
Los pesos los define el rol del NPC:
- Asalto:
w_danger = 1.0,w_opp = 1.5. Va donde haya oportunidad aunque sea peligroso. - Médico:
w_danger = 3.0,w_opp = 0.5. Evita peligro a toda costa. - Francotirador:
w_danger = 2.0,w_opp = 0.0,w_control = -0.5. Busca terreno sin control aliado (flancos solitarios) lejos del peligro. - Carne de cañón:
w_danger = -1.0. Sí, negativo. El NPC corre hacia el peligro porque su rol es absorber daño.
La capa combinada para un asalto en el ejemplo anterior queda:
Fórmula: control − 2·danger + 0.5·opportunity. Celdas rellenas = valor positivo (atractivo); celdas con sólo borde = negativo (a evitar). El máximo del grid (9) marca la celda destino: argmax.
El máximo del grid (9) le dice al asalto a qué celda moverse: cerca del item, lejos de los enemigos. Sin combinación, el NPC tendría que mirar tres capas a la vez y decidir él. Con la combinada, la decisión es un argmax.
2.5 ¿Cuándo recalcular y a qué resolución?
Esta es la pregunta que decide si tu influence map es un feature o un cuello de botella.
Resolución: cuanto más fina, más cara cada propagación. Reglas decentes:
| Escenario | Resolución típica | Por qué |
|---|---|---|
| Sala de combate cerrada | 16×16 a 32×32 | El NPC solo tiene ~20 celdas alrededor; más detalle no se nota |
| Mapa de RTS pequeño | 64×64 | Suficiente para tácticas de pelotón |
| Mapa de RTS grande | 128×128 | Empieza a doler; considera multi-resolución |
| World map de 4X | 64×64 hex | A escala de provincia, no de unidad |
Recálculo: event-driven, no per-frame. Recalculas la capa cuando una fuente se mueve más de N celdas, nace, muere, o cambia la topología (pared destruida). Entre eventos la capa se queda quieta. Si no aguantas la latencia, dos trucos: incremental (restas la contribución vieja de la fuente y la sumas en la celda nueva, solo tocas su radio), o a slices (divides el trabajo en N frames; la capa va un frame por detrás, pero el coste por frame es 1/N).
2.6 ¿Cómo predecir dónde estará el enemigo en 3 segundos?
Aquí es donde los influence maps dejan de ser “mapa estático del presente” y se vuelven interesantes. La idea es brutalmente simple: proyectas la posición futura de cada fuente y la usas como fuente extra.
predicted_pos = enemy.position + enemy.velocity * lookAhead
Stampas el danger en predicted_pos con un peso menor (porque la predicción es incierta) y obtienes una danger map del futuro. Combinándola con la del presente, el NPC puede:
- Moverse a un punto que ahora es seguro y en 3 segundos también.
- Evitar la trayectoria del enemigo, no su posición actual.
- Disparar con lead (apuntar adonde va a estar la cabeza, no donde está).
Un truco de producción: emite varias capas predichas a distintos horizontes (1s, 3s, 6s) y combínalas con pesos decrecientes. El NPC ve el futuro como un degradado de incertidumbre, no como una foto.
2.7 ¿Cómo se usa el mapa en pathfinding?
El uso más limpio es pathfinding influenciado: tu A* sigue siendo el mismo, pero el coste por celda se infla con el peligro.
cost(cell) = baseCost(cell) + dangerMap[cell] * weight
Con weight = 0, A* encuentra el camino más corto, como siempre. Con weight grande, A* prefiere caminos largos que rodeen el peligro. El NPC sale por la puerta de atrás aunque la principal sea más corta. El Dijkstra aplicado tiene el mismo truco con grids de coste heterogéneo.
Ojo con un detalle: si dangerMap[cell] * weight > baseCost, el A* puede romper su garantía de optimalidad si la heurística no se actualiza. La fix es usar Dijkstra puro sobre el coste compuesto, o asegurar que la heurística sigue siendo admisible respecto al coste inflado.
3. Pseudocódigo
function propagate(sources: List<Source>, gridSize: Vec2, decay: Float) -> Grid<Float>
grid = newGrid(gridSize, 0)
for each src in sources
spreadInfluence(src.cell, src.value, grid, decay)
return grid
function spreadInfluence(origin: Cell, value: Float, grid: Grid, decay: Float)
# BFS con propagación multiplicativa; respeta obstáculos
queue = priorityQueue()
queue.push((origin, value))
while queue.notEmpty
cell, val = queue.pop()
if val <= grid[cell]: continue # ya hay mejor influencia
grid[cell] = val
for each neighbor in walkableNeighbors(cell)
queue.push((neighbor, val * decay))
function combine(layers: Dict<String, Grid>, weights: Dict<String, Float>) -> Grid
out = newGrid(...)
for each cell
out[cell] = sum(layers[k][cell] * weights[k] for k in layers)
return out
function predictiveDanger(enemies: List<Enemy>, lookAhead: Float, decay: Float) -> Grid
sources = []
for each e in enemies
future = e.position + e.velocity * lookAhead
sources.add(Source(future, e.threatValue * 0.6)) # peso menor: incierto
return propagate(sources, gridSize, decay)
El patrón es siempre el mismo: propaga, combina, lee. La decisión del NPC se reduce a leer una celda o a hacer argmax sobre una región.
4. Implementación en Unity / C#
Snippet representativo de la clase InfluenceMap. Cubre fuente, propagación BFS, sample en world coordinates y combinación con otra capa.
using System.Collections.Generic;
using UnityEngine;
public class InfluenceMap {
public int Cols, Rows;
public float CellSize;
public Vector2 Origin; // esquina inferior izquierda en world coords
float[,] grid;
public InfluenceMap(int cols, int rows, float cellSize, Vector2 origin) {
Cols = cols; Rows = rows; CellSize = cellSize; Origin = origin;
grid = new float[cols, rows];
}
public void Clear() {
for (int x = 0; x < Cols; x++)
for (int y = 0; y < Rows; y++)
grid[x, y] = 0f;
}
public Vector2Int WorldToCell(Vector3 worldPos) {
int cx = Mathf.Clamp((int)((worldPos.x - Origin.x) / CellSize), 0, Cols - 1);
int cy = Mathf.Clamp((int)((worldPos.z - Origin.y) / CellSize), 0, Rows - 1);
return new Vector2Int(cx, cy);
}
public void AddSource(Vector2Int cell, float value, float decay, System.Func<Vector2Int, bool> walkable) {
// BFS con propagación multiplicativa; respeta walkable() para no atravesar muros
var queue = new Queue<(Vector2Int c, float v)>();
queue.Enqueue((cell, value));
var dirs = new[] { new Vector2Int(1,0), new Vector2Int(-1,0), new Vector2Int(0,1), new Vector2Int(0,-1) };
while (queue.Count > 0) {
var (c, v) = queue.Dequeue();
if (c.x < 0 || c.x >= Cols || c.y < 0 || c.y >= Rows) continue;
if (v <= grid[c.x, c.y]) continue;
grid[c.x, c.y] = Mathf.Max(grid[c.x, c.y], v);
foreach (var d in dirs) {
var n = c + d;
if (n.x < 0 || n.x >= Cols || n.y < 0 || n.y >= Rows) continue;
if (!walkable(n)) continue;
float nv = v * decay;
if (nv > 0.05f && nv > grid[n.x, n.y])
queue.Enqueue((n, nv));
}
}
}
public float Sample(Vector3 worldPos) {
var c = WorldToCell(worldPos);
return grid[c.x, c.y];
}
public void CombineInto(InfluenceMap other, float weightSelf, float weightOther, InfluenceMap output) {
for (int x = 0; x < Cols; x++)
for (int y = 0; y < Rows; y++)
output.grid[x, y] = grid[x, y] * weightSelf + other.grid[x, y] * weightOther;
}
}
Uso típico cada vez que algo importante se mueva:
danger.Clear();
foreach (var e in enemies)
danger.AddSource(danger.WorldToCell(e.position), e.threat, 0.75f, IsWalkable);
control.Clear();
foreach (var a in allies)
control.AddSource(control.WorldToCell(a.position), 1f, 0.85f, IsWalkable);
// Sample en runtime
float threatHere = danger.Sample(npc.transform.position);
5. En otros engines
- Godot: usa un
TileMappara la topología y unArray2D paralelo (PackedFloat32Arraypor fila) para los valores. La propagación BFS es idéntica. Para visualizar, dibuja unImagey muéstralo como textura encima del tile map. - Unreal: el EQS (Environment Query System) ofrece queries espaciales con pesos parecidos a una capa combinada, pero no es un influence map propagado. Para mapas reales, usa un
TArray2D<float>y propagación manual; EQS lo consume comoGeneratorcustom. - JavaScript / TypeScript: cualquier
Float32Arrayindexado pory * cols + xte basta. La lib del sitio implementa la propagación BFS directamente sobreFloat32Arraycon cola plana.
6. Quiz
Pon a prueba lo que entendiste
Responde una por una. La explicación aparece al elegir, correcta o no.
Tu danger map se siente pixelado y los NPCs zigzaguean cuando intentan rodear a un enemigo. ¿Qué subes primero?
Combinas control y danger con pesos iguales (1.0 y 1.0). El NPC nunca avanza. ¿Por qué?
¿Cuándo recalcular el mapa entero vs solo afectar celdas cercanas a la fuente que se movió?
Quieres que tu NPC esquive *donde va a estar* el enemigo en 3 segundos, no donde está ahora. ¿Cuál es el cambio mínimo?
Configuras pathfinding con cost = baseCost + danger * weight. Con weight muy alto, A* deja de encontrar rutas obvias. ¿Qué pasa?
7. Siguientes pasos
Los influence maps son la capa de “lectura del mundo” que conecta percepción con decisión. Si quieres que tu NPC además planifique sobre esa lectura, el siguiente eslabón es GOAP o utility AI consumiendo la capa combinada como otra consideration.