Ir al contenido

Por Qué las Partículas de Malla Vencen al ParticleEmitter Nativo

Sobre el papel, un ParticleEmitter estándar de Roblox debería ganar todos los benchmarks. Cada «partícula» es un cartel 2D con textura renderizado directamente en la GPU. La propia documentación de Roblox los describe como «imágenes 2D personalizables» renderizadas como un «cartel quad estándar orientado hacia la cámara». Sin instancia del DataModel por partícula, sin CFrame que actualizar en el lado del motor, sin colisionador.

Las partículas de malla del plugin son diferentes. Cada una es una Part real, a veces una MeshPart, a veces una Part con un hijo SpecialMesh, a veces llevando un descendiente Beam o Trail. Instancia real, CFrame real, Size real.

Cabría esperar que eso fuera lento.

No lo es. Configura 100 partículas usando una fuente Part transformada, dispárala, observa el tiempo de fotograma en el MicroProfiler. Luego configura el mismo efecto en un ParticleEmitter nativo, dispáralo, observa. La ruta de malla se ejecuta más rápido, a veces por un orden de magnitud. Este capítulo explica por qué, con los recibos.

Cómo escala el ParticleEmitter nativo (según los propios documentos de Roblox)

Sección titulada «Cómo escala el ParticleEmitter nativo (según los propios documentos de Roblox)»

No tienes que creer en las afirmaciones del plugin por fe. La documentación oficial del particle emitter de Roblox describe el modo de fallo claramente:

«El tamaño de las partículas puede impactar el rendimiento debido al fill-rate. Cuantos más píxeles ocupen las partículas en la pantalla de un jugador, más costoso es para la GPU.»

«El recuento de partículas puede impactar el rendimiento debido al overdraw, especialmente cuando las partículas se superponen. Cuantas más capas de efectos transparentes haya en pantalla, más costoso es para la GPU.»

«Para el mejor rendimiento, mantén la tasa de partículas tan baja como sea posible…»

La misma página documenta topes concretos:

«Un único particle emitter puede crear hasta 400 partículas por segundo… 100 por segundo en móvil.»

«Las partículas tienen un tiempo de vida máximo de 20 segundos.»

Juntando todo eso: cada partícula nativa es un cartel 2D. La GPU paga por cada píxel que cubre (fill-rate). Paga otra vez cuando las partículas se superponen desde el punto de vista de la cámara — los sprites con alpha-blending no se ordenan en z entre sí, así que dos partículas superpuestas cuestan ambos fragment shaders. La propia Roblox recomienda mantener las tasas bajas.

Un hilo del Developer Forum con mediciones de la comunidad reporta aproximadamente lo que predirías: un par de cientos de partículas de ParticleEmitter superpuestas reducen la tasa de fotogramas drásticamente en hardware de gama media, más cuando la cámara está cerca (más cobertura de pantalla, más overdraw). Toma cualquier número individual de la comunidad con cautela y verifícalo con tu propio MicroProfiler. El techo arquitectónico es el que describe Roblox.

La ruta nativa también evalúa cada propiedad en cada fotograma. Roblox documenta que varias propiedades (Drag, Transparency, Size, Squash) se muestrean por fotograma independientemente de si las cambias de sus valores por defecto.

Cómo escalan las partículas de malla del plugin

Sección titulada «Cómo escalan las partículas de malla del plugin»

Cada partícula de malla emitida es una instancia real de clase Part, a veces llevando un hijo SpecialMesh para geometría personalizada. Sobre el papel son 100 instancias extra en el DataModel por emisión. En la práctica, cinco comportamientos colaboran para mantener el coste por partícula por debajo del de un sprite quad.

La primera vez que una fuente emite, el motor asigna los clones que necesita. A partir de ese momento, toda emisión posterior recicla. Los clones regresan a un pool oculto al final de su tiempo de vida, y la siguiente emisión adquiere del pool en vez de clonar de nuevo. El pool se dimensiona a tu tasa de régimen estacionario.

La asignación de instancias de Roblox no es gratis. Cada Instance.new (o :Clone()) más su posterior recolección de basura cuesta CPU. La reutilización del pool amortiza ese coste a cero a lo largo de la larga cola de emisiones. Pagas una vez durante el calentamiento, luego se acaba la factura.

El ParticleEmitter nativo asigna estado interno fresco por cada partícula generada. El estado no es una Instance a nivel de Roblox, pero es asignación, y se libera cada vez que la partícula muere.

2. Muestreo de gráficos cuantizado por pasos

Sección titulada «2. Muestreo de gráficos cuantizado por pasos»

Abre la sección Advanced del panel de propiedades de cualquier emisor transformado. Hay una fila etiquetada Anim. Steps (atributo de datos TotalKeyFrames). Predeterminado 100. Consulta la sección Anim. Steps para la explicación completa.

Lo que significa para el rendimiento: una partícula que vive durante 3 segundos con Anim. Steps = 100 vuelve a muestrear sus gráficos aproximadamente 33 veces por segundo, no 60 ni 120. El simulador se ejecuta en cada fotograma, pero solo avanza los valores subyacentes del gráfico cuando cambia el límite del paso discreto. Dentro de un paso, el visual interpola suavemente entre los dos CFrames de los extremos; las consultas al gráfico ocurren una vez por paso.

Baja Anim. Steps a 30 para un emisor denso y de corta duración y el coste por partícula baja con él. La pérdida visual es mínima para emisores que no tienen detalle de larga vida que preservar.

El ParticleEmitter nativo no expone esta perilla. Evalúa por fotograma.

Para el caso común (un gráfico Speed sin drag, sin VelocityVectored, aceleración simple) el simulador resuelve la trayectoria directamente. La posición en el tiempo t se calcula a partir de la integral de velocidad, no pasando por cada fotograma anterior. El bucle caliente se salta por completo la actualización de velocidad por fotograma.

Drag y VelocityVectored recurren a un bucle por pasos porque su movimiento no tiene forma cerrada. La ruta rápida de movimiento simple aún cubre la mayoría de los emisores.

Cada clon emitido se establece con la configuración completa de coste cero:

  • Anchored = true — sin participación del solver de física.
  • CanCollide = false — sin colisionador, sin comprobaciones de solapamiento.
  • CanQuery = false — invisible para raycasts (excepto donde el sistema de Eventos opta por la colisión OnHit).
  • CanTouch = false — sin disparar evento Touched.
  • Massless = true — impacto cero en assemblies padre.
  • CastShadow = false — sin proyector de sombra.

Los clones vivos también viven fuera de la rama activa del workspace, así que no aparecen en los recorridos normales de :GetChildren() de tu escena. Otros sistemas que escanean el workspace (tus propios scripts de juego, herramientas de terceros, la caja de selección de Roblox) nunca los ven y nunca gastan tiempo en ellos.

El subsistema de partículas de Roblox no hace nada de esto para el ParticleEmitter nativo. No hay nada de lo que desactivarse porque no hay colisionador en primer lugar. Los ahorros del lado de la malla son lo suficientemente grandes como para cerrar la brecha arquitectónica de todos modos.

El estado por partícula que se necesita en cada fotograma (CFrame actual, valores de gráfico muestreados actuales, contador de pasos de animación, referencias de Link) se preasigna en el momento de emisión y se reutiliza durante todo el tiempo de vida de la partícula. El bucle de actualización lee y escribe en ese estado sin crear un nuevo Vector3 o CFrame o tabla.

Esto importa porque el recolector de basura de Luau es reentrante. Cada Vector3.new dentro de un bucle caliente añade presión al GC que se convierte en picos intermitentes de tiempo de fotograma. El bucle de actualización del plugin no crea basura. El GC permanece inactivo siempre que nada más en tu juego esté asignando intensamente.

Junta los hilos para el resultado visible para el usuario.

100 partículas nativas de ParticleEmitter en la misma región de pantalla: 100 carteles. Los propios documentos de Roblox señalan tanto el fill-rate (píxeles cubiertos) como el overdraw (alpha blends superpuestos) como los costes dominantes. La CPU evalúa cada propiedad en cada fotograma incluso si solo cambias Color. Sin pool de asignación, así que cada generación arrastra estado interno del lado del motor.

100 partículas de malla del plugin (fuente Part): 100 instancias extraídas de un pool dimensionado a la tasa. Cero asignación tras el calentamiento. El coste de CPU se ejecuta solo en las propiedades que realmente cambias. El movimiento se resuelve en forma cerrada en el caso común. Sin colisionador, sin superficie de raycast, sin Touched, sin participación del solver de física. El coste visible es live_count × coste de gráfico por paso / fotogramas por vida de Anim. Steps, una fracción del coste por propiedad por fotograma de la ruta nativa.

El compromiso es real pero en la dirección opuesta. Las partículas del plugin cuestan más por partícula en memoria (instancia real, CFrame real). Cuestan mucho menos por partícula por fotograma. Con 100 partículas, eso es un empate en el lado de la memoria y una victoria en el lado del tiempo de fotograma.

Verificándolo tú mismo con el MicroProfiler

Sección titulada «Verificándolo tú mismo con el MicroProfiler»

Lo de arriba es un argumento arquitectónico. Roblox distribuye un profiler que lo convierte en una medición.

  1. En Studio con tu place de prueba abierto, presiona Ctrl+F6 para abrir el overlay del MicroProfiler.
  2. Haz clic en el botón emit de tu fuente de partículas de malla transformada. Observa las barras de tiempo de fotograma durante la ráfaga.
  3. Intercambia por un ParticleEmitter nativo configurado para el mismo visual (mismo Rate, mismo Lifetime, textura y tamaño similares). Haz clic en emit.
  4. Compara las barras.

El MicroProfiler te muestra exactamente qué subsistema está gastando tiempo. El trabajo por partícula del plugin aparece bajo sus propios ticks; el subsistema de partículas de Roblox aparece bajo los cubos «RenderJob» y «ParticleEmitter». Verás la diferencia de coste por fotograma directamente, en el hardware que tus jugadores realmente tienen.

Si el resultado en tu hardware contradice la afirmación del plugin, reporta un bug. El motor evoluciona; lo que es cierto hoy puede cambiar.

Las partículas de malla no tienen sentido para todos los efectos.

  • Efectos que llenan la pantalla con miles de partículas — una ventisca pesada, una tormenta de humo cubriendo la cámara, un campo de estrellas. El coste por partícula importa menos que el coste de draw call, y la ruta del sprite quad agrupa por GPU los sprites en un pequeño número de llamadas. Las partículas de malla del plugin obtienen cada una su propio CFrame y dibujo, que escala linealmente. El PE nativo gana en el límite superior si ya has aceptado el coste del overdraw y tu presupuesto de fill-rate lo permite.
  • Sprites de cartel 2D puros sin complejidad espacial — un chorro de llama desde una torreta fija donde cada partícula tiene la misma orientación, sin rotación, sin tamaño dirigido por gráfico. La ruta del plugin añade sobrecarga que el efecto no usa.
  • Efectos que construirías una vez y nunca querrías configurar a través del plugin — una rápida actualización de un place heredado donde añadir el plugin es más coste del que el beneficio de rendimiento devuelve.

Para todo lo intermedio (los efectos VFX de docenas a cientos que componen la gran mayoría de los efectos) la ruta del plugin es la mejor opción por defecto.

Unas pocas perillas influyen significativamente en el coste por partícula.

  • Anim. Steps (referencia). Bájalo para partículas densas y de corta duración. Súbelo solo cuando puedas ver el cambio por pasos en el movimiento en partículas de larga vida.
  • Los gráficos vacíos son gratis. Un gráfico Color en blanco constante no se muestrea por paso. El motor se salta las propiedades que no se han cambiado desde sus valores por defecto. No te preocupes por dejar los gráficos en paz.
  • Optimization Indicator en el Toolbench. Resalta los emisores cuyo Rate × Lifetime × Anim. Steps aterriza en territorio costoso. Úsalo como señal de presupuesto de rendimiento.
  • Lifetime y Rate. El recuento de partículas vivas en régimen estacionario es Rate × Lifetime. Recortar cualquiera de los dos recorta la carga por fotograma proporcionalmente.

Edición Nativa cubre el uso del panel del plugin y del Editor de Gráficos para instancias estándar de Roblox ParticleEmitter, Trail y Beam. Incluso cuando el PE nativo es la elección correcta para un efecto específico, el plugin es un mejor entorno de edición para él que el panel estándar de Studio.