Kirino 的游戏宝藏
← Volver al número 022
Observación de juegos

Pagar por tipo, cómo Naio Games sostiene cientos de enemigos y balas en Godot 4 con GDScript puro

Si el oficio por defecto de los survivors-like, los bullet heavens y las tower defenses en los últimos años ha sido colgar un script, una luz y un collider a cada enemigo, cada…

Mis guardados ↗

Si el oficio por defecto de los survivors-like, los bullet heavens y las tower defenses en los últimos años ha sido colgar un script, una luz y un collider a cada enemigo, cada bala y cada recogible, entonces la guía de unos dieciocho minutos que Naio Games publicó el 15 de octubre de 2026 abre esa factura ante los pares. Escriben desde medidas reales en su roguelite de spell-crafting Hexwright, en Godot 4. Las escenas objetivo pueden llegar a unos 1000 enemigos, 778 proyectiles y casi 10 000 nodos. Pasados unos 120 enemigos, el frame rate cayó de 48 a 32. La razón es llana. A partir de ese punto cada fotograma ejecutaba dos physics ticks, así que todo el trabajo por enemigo se duplicaba.

Leí la guía pública de punta a punta. Los hechos que se pueden clavar son simples. Todo el utillaje está en GDScript ordinario más el lenguaje de shaders de Godot. Sin C#, sin módulo C++, sin GDExtension. Una restricción era sacar un build web; la exportación web de C# no les servía entonces. En sus logs lo lento rara vez era el GDScript en sí. Era pedir al motor una pequeña tarea por objeto, cientos de veces por fotograma. Lo que movió los números fue cambiar la forma de ese trabajo.

La imagen que atraviesa este texto es pagar por tipo. Suena a contabilidad. En un presupuesto de fotograma es un conjunto de cambios de libro verificables.

Imagen OG de la guía de rendimiento de Naio Games

1. Por defecto de la industria, cada objeto paga su propia cuenta

Los survivors-like y los bullet heavens comparten una forma. Pocos tipos de cosas, cada uno repetido cientos de veces. Cualquier milisegundo extra de luz, un callback, un cambio de textura en cada objeto se multiplica por la horda hasta un desastre de fotograma entero. El estilo de escritura por defecto también está fijo. Las balas llevan `_physics_process`. Los enemigos llevan `Area2D`. Los recogibles llevan `PointLight2D`. Las estelas usan un `Line2D`. El motor los enchufa a física, render y señales. Es cómodo de escribir. La factura sigue siendo por unidad.

Naio enumera costes locales punzantes. Ochocientas ochenta luces de recogibles llegaron a costar solas unos 5 ms por fotograma. Las comprobaciones de colisión enemigo-jugador eran cerca del 54% de todo el coste de enemigos. Trescientas balas atravesando 240 enemigos costaban 7,9 ms con impactos activos frente a 4,9 ms con impactos apagados. Trescientas estelas de bala bajaron de 13,7 ms a 5,0 ms solo después de que los draw calls cayeran de 316 a 28. Esos números salen de sus propios logs de playtest y escenas de referencia. Las máquinas cambian; la dirección no. El coste suele caer primero en pagar por objeto, no en un fragmento de IA ingenioso.

La bifurcación respecto al defecto está aquí. El defecto pregunta cómo terminar el comportamiento de cada objeto. Ellos preguntan si un tipo de cosa puede pagar una sola cuota de gestión, mientras las instancias pagan solo por sus datos.

Póster de combate de la guía

2. Reescribir el libro, pools, un dueño por tipo, luces falsas, matemáticas de rejilla

El primer corte es crear y destruir. A mitad de pelea, `instantiate()` arma el árbol de nodos, ejecuta `_ready()` y registra física y render. `queue_free()` lo desmonta otra vez. Cincuenta de esas en un fotograma, un hechizo de escopeta o una oleada cayendo junta, es un tirón visible. La solución es precalentar pools al cargar y en combate solo acquire y release. Autorizar las escenas inertes, ocultas, colisión apagada, y `activate()` solo tras colgarlas del árbol. En cada reuso escribir todo ajuste visual, incluidos los valores por defecto, o una bola de fuego reciclada conserva el brillo anterior. La interpolación física en un nodo oculto se ignora; hay que resetearla antes de mostrar, o la bala se desliza desde donde murió la vez anterior. Registrar un miss del pool. No recortar el hechizo en silencio.

El segundo corte es la forma del callback. Trescientas balas con `_physics_process` cada una son trescientos callbacks del motor. Las balas solo tienen `step(delta)`. Un `bullet_manager` recorre la lista viva hacia atrás. La liberación usa swap-remove en tiempo constante. Si la pausa usa `time_scale = 0`, los managers deben volver al instante. Su propio tropiezo fue que balas y enemigos se detenían mientras hechizos de área y haces seguían barriendo a velocidad cero, gastando 6–14 ms en la pantalla de subida de nivel. La misma idea vale para el dibujo. Doscientos iconos de estado pueden ser un solo dibujante en un `_draw()`.

El tercer corte son las luces. Un `PointLight2D` real añade trabajo de dibujo a cada sprite que toca, y cada sprite limita cuántas luces puede recibir. Cincuenta balas luminosas por una muchedumbre multiplican el coste de la muchedumbre. Conservaron luces reales para unas pocas fuentes que importan, el jugador, un jefe, una hoguera. Balas y recogibles usan un sprite circular suave con mezcla aditiva para el brillo del suelo, misma textura y material para que se agrupen. Cuando el `CanvasModulate` nocturno oscurece, un parámetro global de shader multiplica el color del sprite hacia arriba. Media oscuridad se convierte en ×2 de elevación para seguir legibles. Ochocientas ochenta luces de recogibles a unos 5 ms solas son el recibo más claro de este cambio.

El cuarto corte es la colisión. Si solo necesitas quién está en un círculo o qué barre un segmento, una rejilla espacial más pruebas de distancia suele ser más barata que el emparejamiento físico. Una vez por tick, meter enemigos en celdas de 64 px. Las consultas visitan solo las celdas cubiertas y luego miden distancia exacta a los pocos candidatos. Los enemigos pueden salir del motor físico; la lógica de impacto sigue llamando al `on_hit` antiguo. La identidad estable debe usar instance id, nunca el índice de lista, porque swap-remove reutiliza índices. El radio del jefe es una trampa dedicada. Las consultas deben crecer con el radio del monstruo más grande para no fallar. Un jefe de unos 45 px de radio hace que cada consulta de bala barre celdas de más. Guardan monstruos mayores de unos 32 px en una lista aparte y los comprueban después de la rejilla. Con 600 balas, 60 enemigos y un jefe, la física bajó de 17,4–18,3 ms a 15,5–15,7 ms, unos 2 ms. La guía anota que eso es solo parte del coste del combate; el procesamiento de impactos sigue llevándose el resto.

Estos cuatro cortes comparten un principio. Los pools quitan el impuesto de creación a mitad de pelea. Un dueño por tipo quita el impuesto de callback por objeto. Las luces falsas quitan el impuesto de iluminación por objeto. Las matemáticas de rejilla quitan el impuesto de emparejamiento físico por objeto. Las reglas de impacto del lado del juego se quedan. Lo que cambia es quién paga.

Captura nocturna de flechas de Hexwright

3. Los números siguen bajando, atlas, MultiMesh, redibujar solo el cambio, vuelo recto en GPU

El agrupado de texturas suele ser el corte 2D más barato y apenas toca el código de juego. Godot fusiona sprites vecinos solo mientras textura y material coinciden. Monstruos caminantes con ocho direcciones por ocho pasos en imágenes sueltas pueden dar 241 draw calls para 240 monstruos. Empaquetados en un atlas con margen, la misma escena cae a 2 draw calls. Midieron tiempo de fotograma aún en torno a 2,2 ms; en una GPU rápida es más margen que un salto visible de fps. Dejar margen para que los shaders de contorno no muestreen vecinos. Los materiales también parten lotes. Tres materiales especiales entre 200 enemigos cuestan tres lotes extra. Un material único por enemigo cuesta 200.

Más abajo, cientos de cositas idénticas, recogibles, chispas, segmentos de estela, que no necesitan ordenarse delante o detrás de otros objetos, no necesitan un nodo cada una. Cuelgan un MultiMesh en el RenderingServer y escriben datos de instancia solo cuando algo aterriza o se recoge. El balanceo idle corre en la GPU; los recogibles en reposo cuestan cero script. Las estelas igual. Un `Line2D` por bala es un draw call por bala. Un anillo de ranuras de segmento en un MultiMesh deja que cada tick selle un segmento con tiempo; el shader afila y desvanece por edad. En su banco de 300 flechas, el fotograma entero bajó 13,7 → 5,0 ms, y los draw calls 316 → 28.

El árbol de habilidades es otra pantalla cara. Una red de hexágonos, hilos, muescas, iconos, precios y nombres está casi quieta; solo un poco de líquido se anima. Llamar `queue_redraw()` a toda la red cada fotograma costaba 49,9 ms a máxima mejora, unos 36 ms de GDScript reemitiendo las mismas órdenes, y 116 ms con 4× nodos. El arreglo corta la parte quieta en tiles, cada uno una lista retenida de órdenes de canvas item, regrabada solo cuando está sucia. Pan y zoom solo mueven o escalan tiles. Una capa live delgada redibuja lo que de verdad anima. Los 87 nodos de hoy bajaron 49,9 → 10,7 ms. A 4× nodos, 116 → 15,2 ms. El paneo pasó de un redibujado completo cada fotograma a cero redibujados. Un nivel más arriba, la resolución de render. El modo `canvas_items` dibuja a píxeles de pantalla; un monitor 4K es unas 9× los píxeles de un diseño 1280×720, y cada glow y shader a pantalla completa paga. Lo hicieron opción. El modo `viewport` renderiza al tamaño de diseño y escala, dando a los jugadores Rendimiento 720p y Nítido nativo.

El vuelo en GPU es el corte más fácil de pasarse. Las balas rectas solo necesitan inicio, velocidad y tiempo de salida; el shader las coloca cada fotograma sin que el script cambie transformadas. Unas 800 balas luminosas, cuerpo más halo más brillo de suelo, bajaron draw calls de unos 1150 a unos 480. El tiempo de fotograma quedó dentro del ruido del banco; con 300 flechas simples casi no cambió, porque sprites idénticos con un material ya se agrupan. El juego real mordió. La mayoría de sus balas buscan, orbitan o vuelven, así que se doblan cada tick y reescriben anclas cada tick, sumando 4–6 ms, peor que nodos normales. Solo las balas en línea recta mantienen la vía GPU. La nota final de la guía es clara. El dibujo GPU de balas rectas, el MultiMesh de recogibles y el pensamiento escalonado a 30 Hz aún no habían tenido playtest completo. El resto se midió en su máquina; tus números diferirán.

Captura de tormenta de flechas de fuego de Hexwright

4. Precalentar, pensar a medias, y el límite duro de no cambiar el juego

Mientras la cortina de carga aún tapa el mundo, colocan instancias de efectos a la vista unos fotogramas para que los pipelines de shader compilen. Lo fuera de pantalla se elimina y nunca compila. Los pools se precalientan según picos de diseño, proyectiles por lanzamiento por lanzamientos por segundo por vida. El tirón del primer fireball suele ser la primera compilación al dibujar. Su build de Windows usa Direct3D 12 por defecto; paradas raras de 100–600 ms donde ni el código ni el render están ocupados suelen ser compilación de pipeline GPU. El arranque puede pasar a Vulkan; las opciones de Steam pueden ofrecer un segundo Play.

Pensar puede ser más raro que mover. El movimiento debe ir cada tick o la horda tiembla. Decidir adónde ir no. Dos mitades de la horda piensan en ticks alternos, unos 30 Hz, cada una con el tiempo desde el último pensamiento para que los cooldowns guarden tiempo real. Jefes y monstruos en carga siguen pensando cada tick. Su nota de escala es que un pensamiento era 1,85 de los 8,7 µs de un monstruo, unos 0,9 ms ahorrados por tick a 1000. El escalonado debe sobrevivir a las muertes. Una separación que escaneaba media horda por tick pero forzaba un pase completo ante cualquier muerte perdía la ganancia, porque algo muere cada tick. Mover los resultados guardados con cada monstruo bajó la separación de 5,4 → 2,9 ms. Los navegadores van unas 1,4–1,7× más lentos; un override `.web` da al build web un tope de horda menor.

La guía vuelve una y otra vez a un límite duro. La optimización puede cambiar la implementación. No debe cambiar el tacto en silencio. Los impactos de rejilla siguen disparando una vez al entrar. Los recogibles se congelan en pausa. Las reconstrucciones visuales deben seguir llamando a los manejadores de impacto antiguos. La entrada del jefe necesita su propia prueba. Nunca añadir o quitar objetos físicos dentro de un callback de física. Limitan los pasos de puesta al día a 2 para que un fotograma lento no haga bola de nieve. El depurador F5 del editor hacía el GDScript 2–3× más lento, 31–35 ms frente a 11–19 ms. Los números de envío salen del export release que reciben los jugadores, con un `-- --perflog` opcional. Las máquinas ocupadas mienten; el mismo banco osciló 9,4 frente a 3,0 ms, un factor 3, así que correr A, luego B, luego A otra vez. Headless no mide draw calls, luces ni lotes. Los promedios esconden tirones; siempre registrar el peor fotograma.

Así, pagar por tipo, para un equipo indie, es concreto. Nombrar la cosa de la que tienes cientos. Luces, estelas, callbacks, emparejamiento físico, cambios de textura, redibujado a pantalla completa. Lo que lidere el log cambia primero su forma de cobro. Montar una escena de referencia con interruptores que apaguen impactos, estelas o visuales de uno en uno y respondan una sola pregunta. No empezar soñando que un módulo C++ te salvará. Sus logs dicen que lo lento a menudo era un pequeño paso por objeto multiplicado cientos de veces.

Cierre, la independencia se revalúa dentro del libro

Poner esta guía de vuelta en el suelo de producción Godot del otoño de 2026 es golpear primero un hábito de oficio silencioso. La gente asume por defecto que optimizar es un lenguaje más rápido. Asume que el dolor de rendimiento desaparecerá cuando llegue un plugin nativo. Asume que el tirón de survivors-like es un impuesto de género que solo se paga cortando cantidad. Naio abre otro camino. El lenguaje puede seguir siendo GDScript. Lo caro es la forma del trabajo. Cambiar el cobro por objeto por cobro por tipo, y el presupuesto de fotograma se mueve con ello.

Su propia lista de comprobación es casi una orden de trabajo. Una luz en cada bala o recogible, probar glow aditivo más un uniforme de elevación nocturna. Una estela `Line2D` por bala, probar segmentos MultiMesh. `instantiate` y `queue_free` a mitad de pelea, probar un pool precalentado. `_process` en cada objeto del pool, probar un manager por tipo. `Area2D` en cada enemigo y bala, probar rejilla más matemáticas de círculo o cápsula. Una imagen por fotograma de animación, probar un atlas con margen. Cientos de sprites pequeños idénticos, probar un MultiMesh en el RenderingServer. Script moviendo cada bala cada fotograma, probar un ancla en datos de instancia y reescribir al cambiar. Tirón la primera vez que un efecto se juega, probar warm-up de shaders al cargar. `queue_redraw` cada fotograma en una pantalla grande y casi quieta, probar tiles retenidos más una capa live delgada. Una pantalla 4K dibujando arte a 720p, probar una opción de calidad de render. Cada monstruo decidiendo cada tick, probar mitades alternas a 30 Hz mientras el movimiento sigue cada tick. Un build web con el tope de horda de escritorio, probar un override `.web`. Empezar solo porque se siente lento, sacar un log de rendimiento de juego real y arreglar el número más grande.

Las consignas ejecutables son cortas.

  • Escribe tu lista de lo que se multiplica por cantidad. Luces, callbacks, física, draw calls, redibujado a pantalla completa. Cuáles tres mueren al multiplicarse por cientos. Esas cambian de forma primero.
  • Monta una pelea de referencia reproducible con interruptores. Apaga impactos, estelas y visuales por separado. Responde una sola pregunta. En la misma máquina corre A, B, luego A otra vez.
  • Al cambiar colisión o dibujo, regresa impacto una vez al entrar, congelación en pausa y radio de jefe. Visuales y lógica de impacto salen en el mismo commit.
  • Las balas rectas pueden ir a GPU. Homing y bumeranes se quedan primero en CPU. Precalienta pools y shaders, pero no escondas misses cortando hechizos. Escribe las reglas de juego como tests.

El texto largo del 15 de octubre no hará fáciles los bullet heavens. Solo revalúa por qué tipo de repetición estás pagando. Los equipos dispuestos a reescribir el libro tienen ahora un deber público con números de antes y después, y un recordatorio final ya escrito en el texto. Mide primero el número más grande. Luego deja de pagar por objeto.

小光

Fuentes y lectura adicional

Naio Games (2026-10-15) Godot 4 Performance in Pure GDScript, Many Things on Screen, No Lag

Página de Hexwright (spell-crafting roguelite, en desarrollo) https://naiogames.com/hexwright

Estudio Naio Games https://naiogames.com/

Accede para guardar tus lecturas.

Consulta tus Me gusta y artículos guardados en cualquier dispositivo.

Acceder