El secreto para mover hordas con flow field godot 4
Aprende a implementar flow field godot 4 para mover cientos de unidades en pantalla sin caídas de FPS. Tutorial completo y práctico en GDScript.
Al intentar colocar cientos de unidades navegando simultáneamente en un juego de estrategia en tiempo real o survivor de arena, la primera reacción de gran parte de los desarrolladores es instanciar múltiples nodos de navegación y calcular rutas individuales. El problema es que el algoritmo A estándar, presente en los sistemas tradicionales de pathfinding, escala de manera terriblemente ineficiente cuando decenas o cientos de agentes intentan recalcular sus caminos hacia el mismo destino en cada fotograma. Si ya has enfrentado caídas vertiginosas en la tasa de cuadros al aumentar el número de enemigos que persiguen al jugador, implementar el flow field godot 4* es la respuesta definitiva para mantener tu proyecto ejecutándose a 60 FPS estables incluso con miles de entidades en pantalla.
A diferencia de los enfoques de búsqueda de camino por agente, donde cada unidad procesa su propio grafo de nodos, un campo de flujo (flow field) invierte esta lógica de procesamiento. El mapa entero de navegación se convierte en una cuadrícula bidimensional de vectores de dirección calculados una sola vez por fotograma para un objetivo determinado. Todas las unidades en el mapa simplemente leen el vector de la celda donde están pisando y aplican una fuerza de movimiento en esa dirección. El costo computacional deja de ser proporcional al número de unidades en escena y pasa a estar vinculado únicamente al tamaño de la cuadrícula del mapa. En este artículo, vamos a construir una implementación completa desde cero en Godot 4.7.2, explorando desde la construcción de la matriz de costos hasta la optimización en tiempo de ejecución.
¿Por qué el algoritmo A* tradicional falla con cientos de unidades?

Para entender la necesidad del vector de flujo, vale la pena analizar la complejidad asintótica del algoritmo A (A-Star). Cuando cien agentes buscan un camino en un mapa usando A, la CPU necesita ejecutar cien búsquedas individuales en grafos de navegación. Cada búsqueda involucra operaciones costosas de inserción y eliminación en colas de prioridad, verificación de listas de nodos abiertos y cerrados, además del cálculo iterativo de heurísticas de distancia Manhattan o Euclidiana.
Cuando dos o tres unidades entran en ruta de colisión o encuentran un obstáculo dinámico, invalidan sus rutas previas y solicitan un nuevo cálculo. En un escenario con 2000 unidades moviéndose hacia el jugador, son 2000 llamadas de búsqueda de ruta por segundo —o por fotograma, si la posición del jugador cambia constantemente—. El cuello de botella no está en la capacidad de renderizado de la GPU, sino en el sofocamiento del hilo principal de la CPU al intentar iterar sobre miles de rutas individuales de forma simultánea.
La siguiente tabla compara el comportamiento computacional entre el A* individual y el sistema de campo de flujo a medida que aumenta la escala del proyecto:
| Métrica de comparación | Pathfinding A* tradicional | Optimización con Flow Field |
|---|---|---|
| Complejidad por agentes | O(N * K) donde N es el número de agentes | O(1) por agente adicional |
| Complejidad de malla | Depende de la extensión de la ruta de cada uno | O(M) donde M es el número de celdas de la cuadrícula |
| Uso de CPU con 100 unidades | Bajo (1 a 3 ms por fotograma) | Casi imperceptible (< 0.5 ms por fotograma) |
| Uso de CPU con 2500 unidades | Crítico (bloqueos severos de fotogramas) | Estable (constante en el cálculo de la cuadrícula) |
| Adaptación a objetivos móviles | Requiere recalcular N rutas completas | Requiere actualizar la cuadrícula una sola vez |
Esta diferencia abismal en la escalabilidad es la razón por la que los juegos clásicos de estrategia masiva y los títulos modernos de hordas utilizan mapas de vectores. La unidad individual deja de ser un agente autónomo e inteligente para convertirse en una partícula pasiva guiada por el terreno.
¿Cómo implementar flow field godot 4 para simular grandes hordas?
Para poner en funcionamiento el flow field godot 4 en Godot 4.7.2, necesitamos dividir la arquitectura del sistema en tres etapas fundamentales que se ejecutan en secuencia: el mapa de costos (Cost Field), el mapa de integración (Integration Field) y el mapa de vectores (Flow Field).
La primera capa es la matriz de costos. Esta matriz representa la dificultad de paso por cada celda del mundo del juego. Las celdas navegables sin obstáculos reciben un costo base bajo (por ejemplo, 1), los terrenos difíciles como lodo o pantanos reciben costos elevados (como 5 o 10), y las paredes u obstáculos infranqueables reciben un valor de costo infinito (usualmente representado por 255 en enteros de 8 bits para optimización de memoria).
La segunda capa es la matriz de integración. Almacena la distancia acumulada de cada celda hasta el destino final, teniendo en cuenta los costos de terreno de la primera matriz. Partiendo de la posición del objetivo con distancia cero, propagamos el costo por las celdas vecinas utilizando una variación del algoritmo de Dijkstra o Flood Fill.
Por último, la tercera capa calcula el vector de dirección para cada celda. Para una celda determinada en la cuadrícula, verificamos los valores de integración de sus ocho vecinos directos (cardinales y diagonales). El vector de la celda apuntará exactamente en la dirección del vecino que posea el menor valor de integración acumulado.
A continuación se presenta la estructura base del script del administrador de flujo escrito en GDScript para Godot 4.7.2:
class_name FlowFieldManager
extends Node2D
@export var grid_width: int = 64
@export var grid_height: int = 64
@export var cell_size: float = 32.0
var cost_field: Array[Array] = []
var integration_field: Array[Array] = []
var flow_field: Array[Array] = []
const MAX_COST: int = 65535
func _ready() -> void:
_initialize_fields()
func _initialize_fields() -> void:
cost_field.clear()
integration_field.clear()
flow_field.clear()
for x in range(grid_width):
var cost_col: Array[int] = []
var integ_col: Array[int] = []
var flow_col: Array[Vector2] = []
for y in range(grid_height):
cost_col.append(1)
integ_col.append(MAX_COST)
flow_col.append(Vector2.ZERO)
cost_field.append(cost_col)
integration_field.append(integ_col)
flow_field.append(flow_col)
¿Qué es la matriz de costos y cómo calcular el mapa de integración?
El cálculo correcto del mapa de integración es el corazón del algoritmo. Si la búsqueda por inundación está mal diseñada, terminarás creando cuellos de botella similares a los de A*. La clave para mantener el cálculo ultrarrápido es utilizar una cola FIFO (First-In, First-Out) que visite únicamente los nodos necesarios a partir del origen.
A diferencia de algoritmos de ordenación complejos, el Flood Fill acoplado a una cola simple logra procesar cuadrículas de 128x128 celdas en menos de dos milisegundos. Cuando el objetivo cambia de posición —como el jugador caminando por la arena—, reiniciamos la matriz de integración, definimos la celda que contiene el objetivo con valor cero y encolamos esa celda inicial.
A continuación tenemos el fragmento que procesa la propagación de las distancias por la cuadrícula:
func generate_integration_field(target_grid_pos: Vector2i) -> void:
for x in range(grid_width):
for y in range(grid_height):
integration_field[x][y] = MAX_COST
if not _is_valid_cell(target_grid_pos.x, target_grid_pos.y):
return
integration_field[target_grid_pos.x][target_grid_pos.y] = 0
var queue: Array[Vector2i] = [target_grid_pos]
var neighbors_offsets = [
Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1),
Vector2i(1, 1), Vector2i(-1, 1), Vector2i(1, -1), Vector2i(-1, -1)
]
while queue.size() > 0:
var current = queue.pop_front()
var current_cost = integration_field[current.x][current.y]
for offset in neighbors_offsets:
var neighbor = current + offset
if not _is_valid_cell(neighbor.x, neighbor.y):
continue
var tile_cost = cost_field[neighbor.x][neighbor.y]
if tile_cost == 255:
continue
var new_cost = current_cost + tile_cost
if new_cost < integration_field[neighbor.x][neighbor.y]:
integration_field[neighbor.x][neighbor.y] = new_cost
queue.append(neighbor)
func _is_valid_cell(x: int, y: int) -> bool:
return x >= 0 and x < grid_width and y >= 0 and y < grid_height
Observa el uso de la verificación de límites y el bloqueo de costo 255 para paredes. Este mecanismo garantiza que los caminos rodeen obstáculos de forma natural, sin requerir verificaciones complejas de colisión física con RayCast2D en medio de la navegación.
¿Cómo generar el campo de vectores y aplicar la fuerza de dirección en las entidades?
Con el mapa de integración completamente lleno, generar el campo de flujo es una operación simple de comparación local. Para cada celda de la cuadrícula, inspeccionamos los vecinos e identificamos cuál de ellos posee la menor puntuación en el mapa de integración. El vector de flujo de esa celda será la dirección normalizada apuntando hacia ese vecino de menor costo.

El siguiente script demuestra cómo derivar los vectores y cómo las entidades individuales consultan la cuadrícula para calcular su movimiento de manera fluida:
func generate_flow_field() -> void:
var neighbors_offsets = [
Vector2i(0, -1), Vector2i(1, 0), Vector2i(0, 1), Vector2i(-1, 0),
Vector2i(1, -1), Vector2i(1, 1), Vector2i(-1, 1), Vector2i(-1, -1)
]
for x in range(grid_width):
for y in range(grid_height):
if cost_field[x][y] == 255:
flow_field[x][y] = Vector2.ZERO
continue
var best_cost = integration_field[x][y]
var best_direction = Vector2.ZERO
for offset in neighbors_offsets:
var neighbor = Vector2i(x, y) + offset
if _is_valid_cell(neighbor.x, neighbor.y):
var neighbor_cost = integration_field[neighbor.x][neighbor.y]
if neighbor_cost < best_cost:
best_cost = neighbor_cost
best_direction = Vector2(offset).normalized()
flow_field[x][y] = best_direction
func get_flow_vector(world_position: Vector2) -> Vector2:
var grid_x = int(world_position.x / cell_size)
var grid_y = int(world_position.y / cell_size)
if _is_valid_cell(grid_x, grid_y):
return flow_field[grid_x][grid_y]
return Vector2.ZERO
En las entidades (los enemigos de la horda), no necesitas nodos pesados de física CharacterBody2D si hay miles de ellos en escena. En su lugar, combina el vector obtenido de get_flow_vector() con un comportamiento simple de separación local (steering behaviors) para evitar que las unidades se superpongan por completo durante el trayecto.
¿Qué técnicas de optimización garantizan 60 FPS continuos en Godot 4.7.2?
Aunque el algoritmo de campo de flujo es inmensamente más eficiente que el A* individual, mover 5000 instancias de nodos individuales en el árbol de escenas de Godot aún puede causar sobrecarga de CPU en la gestión de nodos y en la latencia del recolector de basura de GDScript.
Para extraer hasta la última gota de rendimiento del motor, aplica estas cuatro optimizaciones técnicas cruciales:
- Actualización asíncrona o espaciada: No recalcules la matriz de integración en cada fotograma de renderizado. Si el jugador se mueve despacio, actualizar el campo de flujo de 10 a 20 veces por segundo (usando un temporizador o acumulador de tiempo delta) es más que suficiente para mantener a la horda respondiendo de forma fluida.
- Sustitución de CharacterBody2D por Server API: El uso de la API nativa
PhysicsServer2DoRenderingServerelimina por completo la sobrecarga de nodos del árbol de escenas (SceneTree). En lugar de 2000 nodos en el proyecto, mantienes un único nodo administrador que actualiza las posiciones de los objetos directamente en los servidores del motor. - Arreglo plano de una dimensión (Flat Array): En lugar de matrices bidimensionales anidadas (
Array[Array]), utiliza vectores unidimensionales de tamaño fijogrid_width * grid_heightcon indexación calculada medianteindex = x + y * grid_width. En GDScript, la búsqueda en arreglos planos contiguos aprovecha mejor la caché L1/L2 del procesador. - Interpolación bilineal de vectores (Bilinear Interpolation): Para evitar que los agentes den giros bruscos al cruzar la frontera de una celda a otra, interpola el vector del agente muestreando los vectores de las cuatro celdas más cercanas. Esto genera un movimiento fluido y orgánico sin ningún costo relevante de CPU.
A continuación se muestra un ejemplo práctico de indexación en Flat Array e interpolación de vectores para alto rendimiento:
class_name OptimizedFlowGrid
extends RefCounted
var width: int
var height: int
var cell_size: float
var flat_flow: PackedVector2Array
func _init(w: int, h: int, c_size: float) -> void:
width = w
height = h
cell_size = c_size
flat_flow.resize(w * h)
@inline func get_vector_at(x: int, y: int) -> Vector2:
return flat_flow[x + y * width]
func sample_smooth_vector(world_pos: Vector2) -> Vector2:
var gx = world_pos.x / cell_size - 0.5
var gy = world_pos.y / cell_size - 0.5
var x0 = clampi(int(floor(gx)), 0, width - 1)
var y0 = clampi(int(floor(gy)), 0, height - 1)
var x1 = clampi(x0 + 1, 0, width - 1)
var y1 = clampi(y0 + 1, 0, height - 1)
var fx = gx - floor(gx)
var fy = gy - floor(gy)
var v00 = get_vector_at(x0, y0)
var v10 = get_vector_at(x1, y0)
var v01 = get_vector_at(x0, y1)
var v11 = get_vector_at(x1, y1)
var top = v00.lerp(v10, fx)
var bottom = v01.lerp(v11, fx)
return top.lerp(bottom, fy).normalized()
Conclusión
Implementar el flow field godot 4 transforma drásticamente la capacidad de tu juego para manejar simulaciones masivas sin comprometer la estabilidad de la tasa de fotogramas. Al desacoplar el procesamiento de la ruta del número total de agentes y centralizar la lógica en matrices de integración eficientes, eliminas el principal cuello de botella de la CPU en juegos de hordas, RTS y arenas de supervivencia. Combina las estructuras de datos adecuadas con el uso de PackedVector2Array y la gestión directa a través de los servidores de Godot 4.7.2 para lograr que miles de enemigos persigan al jugador de forma fluida, inteligente y extremadamente optimizada.