Cover system: detección y uso táctico de cobertura
Cómo Gears of War y The Last of Us hacen que sus enemigos sepan dónde esconderse: detección de cover, scoring por calidad y selección dinámica.
Publicado: · Por Juanjo "Banyo" López
0. Introducción
Un enemigo parado en mitad del pasillo, disparando hacia el jugador, sin moverse: pato de feria. Si encima tiene 200 de vida, no es difícil — es tedioso. El enemigo que se siente competente no apunta mejor: usa el entorno. Se mete tras una columna, asoma medio segundo, dispara, vuelve a cubrirse.
Esa es la promesa del cover system. Y la trampa también: hacerlo mal queda peor que no hacerlo. Un NPC que se “esconde” detrás de una mesa de 30 cm con la cabeza sobresaliendo medio metro rompe la inmersión más rápido que un T-pose.
Los juegos que lo hicieron bien definieron el género: Gears of War (cover-centric, todo el combate gira alrededor de la cobertura), The Last of Us y Uncharted (cover como herramienta, no como mecánica obligatoria), Halo (cover situacional, los Elites se cubren cuando pierden el escudo). En todos hay tres piezas detrás del telón: detección de puntos de cobertura, scoring de cuán bueno es cada uno, y lógica para entrar, asomarse y cambiar.
En este tutorial vas a ver:
- Qué cuenta como cover y qué no, geométricamente hablando.
- Precomputed vs runtime: cuándo conviene cada uno.
- Cómo puntuar covers para que el NPC elija el bueno, no el más cercano.
- Cuándo cambiar de cover y cómo asomarse sin morir en el intento.
1. Demo
El enemigo arriba. Tres candidatos: full (muro grueso), half (muro fino) y uno expuesto. El NPC elige el full que bloquea LOS.
2. ¿Cómo funciona un cover system?
Un cover system es la combinación de tres cosas: (1) detección o pre-cómputo de puntos de cobertura en el nivel, (2) scoring de cuán bueno es cada cover frente al enemigo actual, y (3) lógica para decidir cuándo entrar, asomarse o cambiar de cover. Sin las tres, el sistema se siente roto.
El resto de esta sección desmenuza cada pieza, empezando por la geometría y subiendo hasta el squad behavior.
2.1 ¿Qué cuenta como cover?
Un punto es cover válido si, parado (o agachado) en él, la geometría del nivel bloquea la línea de visión entre el NPC y el enemigo actual. Esa es la definición operacional. Todo lo demás son detalles.
De ahí salen dos tipos clásicos:
- Full cover: el obstáculo es más alto que el NPC parado. Le bloquea cuerpo entero. Para disparar tiene que asomarse por un lado.
- Half cover (o low cover): el obstáculo es más bajo que el NPC parado pero más alto que el NPC agachado. Para protegerse, se agacha; para disparar, se asoma por arriba.
Vista lateral. A la izquierda, muro alto tapa al NPC parado. A la derecha, muro bajo solo cubre al NPC agachado; parado se expone.
Half cover es direccional: te cubre del enemigo de un lado, pero si otro enemigo te flanquea por arriba (un piso superior, una escalera), no sirve. Esto importa cuando hay más de un enemigo activo.
2.2 Detección precomputada vs runtime
Hay dos formas de saber dónde puede cubrirse el NPC.
Precomputed: marcas los cover points en el nivel a mano (o con una tool al guardar la escena). Cada cover point es un Transform con una orientación: hacia dónde “mira” el cover (el lado expuesto). Pro: barato en runtime, predecible. Contra: requiere placement manual, no aguanta geometría destructible.
Runtime: en cada combate, lanzas raycasts contra la geometría dinámica para encontrar puntos válidos. Típicamente desde una grilla alrededor del NPC, o desde puntos de un nav mesh. Pro: aguanta cualquier escena, incluyendo paredes que se rompen. Contra: caro, hay que cachear, y los resultados pueden saltar entre frames.
Híbrido (lo que usa la mayoría de juegos AAA): puntos precomputados con validación runtime del LOS contra el enemigo actual. Los puntos están donde el diseñador los puso, pero cada vez que el NPC quiere uno, se confirma con un raycast que efectivamente bloquea la línea desde este enemigo. Si el muro se rompió, el cover deja de ser válido.
2.3 Cover scoring: elegir el bueno, no el cercano
Si tu NPC corre al cover más cercano, va a hacer cosas estúpidas: meterse en un cover que no bloquea al enemigo actual, pelearse por el mismo cover con un aliado, o exponerse en el camino. La solución es asignarle un score a cada candidato y elegir el de mayor puntaje.
Los factores típicos:
- Block angle: ¿qué tanto del ángulo desde el enemigo bloquea este cover? Un muro grande tapa más que una columna delgada. Más bloqueo, más score.
- Distance to enemy: ni muy cerca (te ven inevitable) ni muy lejos (no puedes disparar de vuelta). Una banda ideal, típicamente 8-20 m para shooters.
- Distance to NPC current position: si dos covers son equivalentes, prefiere el cercano. Menos tiempo expuesto en el camino.
- Existing occupancy: si otro NPC ya está en ese cover, penalízalo fuerte. Que no se amontonen.
- Approach safety: ¿hay LOS abierto en el camino desde la posición actual del NPC hasta el cover? Si para llegar al cover tiene que cruzar el pasillo entero, ese cover no es bueno aunque sea perfecto al llegar.
La fórmula final es una suma ponderada. Los pesos los tunea el diseñador hasta que se sienta bien. No hay valores mágicos: dependen del juego, de la escala del nivel, del estilo de combate.
2.4 ¿Cuándo cambiar de cover?
Quedarse en el mismo cover toda la pelea es tan robótico como no usarlo. Los eventos que disparan un cambio:
- Flanqueo: el enemigo se movió a una posición donde tu cover ya no bloquea. El score del cover actual cae por debajo de un umbral, hay que buscar otro.
- Cover destruido: explosivos, paredes rompibles. El cover deja de existir y el NPC se queda al descubierto.
- Oportunidad de avanzar: el enemigo está recargando, o un aliado está suprimiendo. Aprovechar para reducir distancia.
- Suppression del jugador: el jugador está disparando a tu cover. Aguantar quema munición; cambiar lateral te da una ventana de tiro.
La regla práctica: reevaluar el score del cover actual cada N segundos (típicamente 2-4). Si bajó significativamente, o si hay un cover candidato con score mucho mejor, cambiar.
2.5 Peek and shoot: ¿por dónde asomarse?
Estar cubierto es la mitad del cover system. La otra mitad es disparar de vuelta sin morir. El asomarse depende del tipo de cover y de su orientación:
- Side peek (asomarse por el lado): para covers con esquina o borde lateral. El NPC se desplaza unos centímetros hacia el lado, dispara, vuelve.
- Top peek (asomarse por arriba): para half covers. El NPC se levanta, dispara, se vuelve a agachar.
- Blind fire: el NPC dispara sin asomar la cabeza, con la pistola por encima del cover. Imprecisión alta, pero útil para suppression.
Qué peek está disponible lo define el cover point. Un cover de esquina tiene canPeekLeft o canPeekRight según la orientación. Un half cover tiene canPeekTop. Esto lo marca el diseñador (o se computa con raycasts en la dirección del enemigo) al construir el cover point.
Cover de esquina: el NPC se desplaza al borde, asoma hacia el enemigo y dispara. La bala viaja del NPC al enemigo.
2.6 ¿Existe un cover graph?
Para covers conectados que permiten moverse “cubierto entre ellos”, se construye un cover graph: cada nodo es un cover point, cada arista marca “puedo ir de A a B sin perder cobertura significativa frente al enemigo X”.
Esto habilita el truco visual de los enemigos de The Last of Us y Gears: avanzan de cover en cover, no se quedan estáticos. El path se calcula con A* sobre el cover graph, no sobre el navmesh raso. La heurística incluye exposure: cuánto LOS abierto tiene el path frente al enemigo.
El cover graph se precomputa al cargar el nivel (o al editar, si tu engine lo soporta) y se invalida parcialmente si la geometría cambia.
2.7 Cover stealing y conflictos de squad
Dos NPCs queriendo el mismo cover, o el jugador metiéndose en el cover del NPC, son situaciones inevitables. La regla es simple: el cover tiene un owner o está libre. Si el jugador entra a un cover ocupado por un NPC, ese NPC busca el siguiente mejor (cover stealing del lado del jugador).
A nivel de squad, el patrón canónico es cover claim: cuando un NPC decide ir a un cover, lo marca como claimed. Otros NPCs evaluando candidatos lo ven ocupado y lo descartan. Cuando el NPC llega o falla en llegar, el claim se libera.
Sin este sistema, dos NPCs corren al mismo cover y uno termina parado en medio del pasillo mientras el otro se acomoda. Lo viste mil veces en juegos mal pulidos.
3. Pseudocódigo
function selectBestCover(npc: Agent, enemy: Agent, candidates: List<CoverPoint>) -> CoverPoint
best = null
bestScore = -infinity
for each c in candidates
if not blocksLOS(c, npc, enemy): continue
if c.claimedBy != null and c.claimedBy != npc: continue
score = scoreCover(c, npc, enemy)
if score > bestScore
bestScore = score
best = c
return best
function scoreCover(cover: CoverPoint, npc: Agent, enemy: Agent) -> Float
# ángulo cubierto desde la posición del enemigo (0..180°)
blockAngle = computeBlockedAngle(cover, enemy.position)
# banda ideal de distancia al enemigo: ni pegado ni lejos
distToEnemy = distance(cover.position, enemy.position)
distScore = bandScore(distToEnemy, idealMin=8, idealMax=20)
# prefer covers cercanos al NPC (menos exposición al moverse)
distToNpc = distance(cover.position, npc.position)
# penalizar covers con LOS abierto en el camino
approachExposure = exposureAlongPath(npc.position, cover.position, enemy)
return (
blockAngle * 2.0 +
distScore * 1.5 -
distToNpc * 0.3 -
approachExposure * 2.5
)
function blocksLOS(cover: CoverPoint, npc: Agent, enemy: Agent) -> Bool
# raycast desde la posición protegida del cover al enemigo
hit = raycast(cover.protectedPoint, enemy.eyePosition)
return hit != null and hit.collider != enemy
function shouldChangeCover(npc: Agent, current: CoverPoint, enemy: Agent) -> Bool
if current == null: return true
if not blocksLOS(current, npc, enemy): return true # me flanquearon
if current.destroyed: return true # se rompió
currentScore = scoreCover(current, npc, enemy)
if currentScore < changeThreshold: return true
return false
El patrón completo es: cada cierto tiempo, evaluar si conviene cambiar. Si sí, buscar el mejor cover disponible y reservar el claim. Path-to-cover usando el cover graph cuando sea posible.
4. Implementación en Unity / C#
using UnityEngine;
using System.Collections.Generic;
public class CoverPoint : MonoBehaviour {
public Vector3 Facing => transform.forward; // lado expuesto
public bool isFull = true; // full vs half
public bool canPeekLeft = true;
public bool canPeekRight = true;
public bool canPeekTop = false;
public Agent claimedBy = null;
public bool destroyed = false;
public Vector3 ProtectedPoint =>
transform.position - Facing * 0.4f; // detrás del cover
}
public class CoverManager : MonoBehaviour {
public List<CoverPoint> all = new();
public LayerMask occluders;
public float idealMin = 8f, idealMax = 20f;
public CoverPoint FindBest(Agent npc, Agent enemy) {
CoverPoint best = null;
float bestScore = float.NegativeInfinity;
foreach (var c in all) {
if (c.destroyed) continue;
if (c.claimedBy != null && c.claimedBy != npc) continue;
if (!BlocksLOS(c, enemy)) continue;
float s = Score(c, npc, enemy);
if (s > bestScore) { bestScore = s; best = c; }
}
return best;
}
bool BlocksLOS(CoverPoint c, Agent enemy) {
var dir = enemy.EyePosition - c.ProtectedPoint;
return Physics.Raycast(c.ProtectedPoint, dir.normalized,
dir.magnitude, occluders);
}
float Score(CoverPoint c, Agent npc, Agent enemy) {
float blockAngle = ComputeBlockedAngle(c, enemy); // 0..180
float dEnemy = Vector3.Distance(c.transform.position, enemy.Position);
float distScore = BandScore(dEnemy, idealMin, idealMax); // 0..1
float dNpc = Vector3.Distance(c.transform.position, npc.Position);
float exposure = ExposureAlongPath(npc.Position,
c.transform.position, enemy);
return blockAngle * 2f + distScore * 60f
- dNpc * 0.3f - exposure * 2.5f;
}
float BandScore(float d, float lo, float hi) {
if (d >= lo && d <= hi) return 1f;
float over = d < lo ? lo - d : d - hi;
return Mathf.Max(0f, 1f - over / 10f);
}
}
El snippet omite ComputeBlockedAngle y ExposureAlongPath por brevedad: el primero hace raycasts en abanico desde el enemigo hacia el cover y cuenta cuántos pegan; el segundo samplea el segmento entre NPC y cover, contando cuántos puntos tienen LOS abierto al enemigo.
5. En otros engines
- Godot:
Area3Dpara los cover points con un script custom que exponefacingeisFull. La validación LOS usaPhysicsDirectSpaceState3D.intersect_ray(). Para el cover graph, unResourcecon array de nodos y aristas, serializado por escena. - Unreal: hay varios plugins de cover en el Marketplace; la solución nativa es EQS (Environment Query System), hecho justo para esto. Defines un query “cover válido frente al enemigo X” con generadores (puntos cerca del NPC) y tests (LOS, distancia, ocupación). EQS te da los candidatos puntuados de fábrica.
- JavaScript / TypeScript: matemática pura. Los raycasts dependen del motor (Three.js, Babylon.js, o tu propio engine 2D). El scoring es idéntico — todo es vectores y distancias.
6. Quiz
Pon a prueba lo que entendiste
Responde una por una. La explicación aparece al elegir, correcta o no.
Tu NPC corre directo al cover más cercano y muere por el camino atravesando el pasillo expuesto. ¿Qué falta en el scoring?
Dos NPCs eligen el mismo cover y uno termina parado en medio del cuarto. ¿Cuál es la solución más simple?
Tu enemigo siempre asoma por el mismo lado del cover y el jugador lo headshootea sin esfuerzo. ¿Qué corriges?
Marcas todos los cover points a mano en el editor. En testing, un muro destructible se rompe y el NPC sigue 'cubriéndose' detrás del aire. ¿Qué te falta?