Visuals and Niagara

Mass never spawns a Niagara component per bullet. The Niagara visualizer keeps a few persistent systems alive and rewrites their User.* arrays every frame:

Mass entities (position / velocity / type)
        │  gathered on the game thread each frame (closest N within the tracer budget)
        ├─ Projectile System   (NS_MassProjectile)        one component per projectile type
        │     User.Positions / Velocities / Count + per-type look (colour, size, mesh)
        ├─ Smoke Trail System  (NS_ProjectileSmokeTrail)   one component (+1 per unique smoke material)
        │     User.TrailSmoke*   spawn-and-forget GPU puffs
        └─ Impact FX System    (NS_ProjectileImpactFX)     one component for the whole world
              User.Dust* / Spark* / Debris* / Splash*   spawn-and-forget bursts

The systems are assigned in Project Settings → Plugins → Mass Projectile Niagara (details). A projectile type has no Niagara slot: it styles these shared systems through its Visual and Surface FX traits. That's why a new ammo type costs no new Niagara asset.

Dedicated servers create none of this. With Enable Niagara Visualizer off, everything still simulates, hits, decals and plays audio.


Styling per type (no Niagara editing)

Visual trait → tracer component

Pushed once when the type's component is created:

Trait field Niagara parameter Notes
Color User.Color HDR; values above 1 bloom with an emissive material
Size User.SpriteSize cm; also scales the fallback impact puff
Trail Segments / Time Span / Length / Taper same names Analytic streak, not stored history
Bullet Mesh User.BulletMesh GPU mesh renderer (run proj.CreateNiagaraMeshRenderer once)
Mesh Scale User.MeshScale (vector)
Bullet Mesh Material User.BulletMeshMaterial Needs Used with Niagara Mesh Particles

User.GravityZ = −980 × Gravity Scale. User.HeroCount is always 0. User.MeshesEnabled follows proj.NiagaraMeshViz.

Surface FX trait → impact bursts

Effect Color and Effect Scale on each surface row tint and size that type's bursts in the Impact FX system. Decals and sounds on the row are handled by the core, not Niagara.

Recipe: an RPG round without a new Niagara asset

  1. Duplicate EC_Projectile_Shell (slow, heavy).
  2. Visual: a rocket Bullet Mesh + material, Mesh Scale, a larger Size, Emit Smoke on with thicker Smoke Start/End Size. Only set a unique Smoke Material if the puff texture must change.
  3. Surface FX: larger Effect Scale, hotter Effect Color.
  4. Make sure proj.CreateNiagaraMeshRenderer has been run and proj.NiagaraMeshViz is 1.

Rifle, Shell and Plasma already differ exactly this way: same NS_MassProjectile, different Visual and Surface FX rows.


Authoring rules for all three systems

  1. GPU sim, world space, looping. The component lives for the whole session, so the emitter must loop (not Once).
  2. Spawn Burst Instantaneous every loop. Spawn Count is an expression on a User.*Count; burst Age = Engine.Emitter.LoopedAge, so it re-fires every loop.
  3. No Spawn Rate as well. Rate plus burst piles particles at the origin.
  4. Array index per particle: (Particles.UniqueID - Engine.Emitter.TotalSpawnedParticles) / N, where N is particles per event. Spawn Count must be User.Count × N with the same N.
  5. Read arrays at spawn only, via SelectPositionFromArray, SelectVectorFromArray, SelectLinearColorFromArray, SelectFloatFromArray, SelectIntFromArray with Array Sampling Mode = Direct Set.
  6. Capture into Particles.* anything needed after the spawn frame. The User.* arrays are rewritten next frame.
  7. Materials: sprites need Used with Niagara Sprites; meshes need Used with Niagara Mesh Particles.
  8. Finite lifetimes. Particle lifetime does the cleanup.

C++ writes every array every frame, including empty ones, so a stale non-zero count can never replay.


User parameter contracts

Names as they appear in Niagara (User. prefix).

Projectile System — live tracers

One component per type; arrays hold this frame's live projectiles of that type.

Parameter Type Content
User.Positions Position array Projectile positions
User.Velocities Vector array Velocities (cm/s)
User.Count int Number of entries
User.SmokePositions / User.SmokeCount Position array / int Fallback puffs for hits without a batched impact event
User.Color, User.SpriteSize, User.TrailLength, User.TrailSegments, User.TrailTimeSpan, User.TrailTaper, User.GravityZ scalars From the type (set once)
User.BulletMesh, User.BulletMeshMaterial, User.MeshScale mesh / material / vector From the type (set once)
User.MeshesEnabled bool proj.NiagaraMeshViz
User.HeroCount int Always 0

Follow model: spawn Count particles every loop with a lifetime of about one frame. Long-lived particles here accumulate as ghosts; put trails in TrailSegments or the smoke system.

Smoke Trail System — lingering puffs

Parameter Type Content
User.TrailSmokePositions Position array Path samples this frame
User.TrailSmokeVelocities Vector array Flight direction × puff stretch (cm). Copy its length into sprite Y at spawn.
User.TrailSmokeColors Color array Visual Smoke Color
User.TrailSmokeScales float array Smoke Start Size
User.TrailSmokeStretch float array Slice length × overlap
User.TrailSmokeLifetimes float array Smoke Lifetime
User.TrailSmokeCount int Budgeted sample count
User.SmokeStartSize, SmokeEndSize, SmokeStretchLength, SmokeLifetime, SmokeColor, SmokeRiseSpeed, SmokeWindScale, SmokeTurbulence scalars Defaults / per-sample capture
User.SmokeMaterial material Type's Smoke Material override
User.Wind vector Global wind (cm/s)

Legacy aliases SmokePositions/SmokeCount and DustPositions/DustColors/DustScales/DustCount receive the same samples for older emitters.

After changing smoke authoring, run proj.CreateSmokeTrailSystem, then proj.ReloadAssets, and save NS_ProjectileSmokeTrail.

Impact FX System — batched bursts

Solid hits fill Dust + Sparks + Debris (index-aligned). Water (SurfaceType6) fills Splash only.

Parameter Type Content
User.DustCount, User.SparkCount, User.DebrisCount int Solid impacts this frame
User.SplashCount int Water impacts
User.DustPositions / SparkPositions / DebrisPositions / SplashPositions Position arrays Hit point pushed out along the normal (6 / 2 / 3 / 2 cm)
User.DustColors / DebrisColors / SplashColors Color arrays Row Effect Color
User.SparkColors Color array Effect Color × 1.4
User.DustScales / SparkScales / DebrisScales / SplashScales float arrays 30 / 7 / 0.6 / 26 × Effect Scale
User.SparkDirs / User.DebrisDirs Vector arrays Reflected velocity
User.DebrisMeshIndices int array Wood 3–4, others 0–2
User.ImpactEventTypes int array 0 Stop, 1 Ricochet, 2 Penetration Enter, 3 Penetration Exit

Caps: Max Impact Events Per Frame (512), Impact Effect Cull Distance (120 m). Toggle: proj.ImpactEffects. Add extra emitters inside this system; never spawn a system per hit.


Converting a marketplace Niagara system

proj.MakeNiagaraCompatible (editor console) duplicates a Niagara system and wires it to the contract above. The source asset is never modified unless you pass inplace.

proj.MakeNiagaraCompatible                                     (uses the Content Browser selection)
proj.MakeNiagaraCompatible /Game/VFX/NS_RocketTrail role=tracer dest=/Game/FX/NS_RPG_Mass
proj.MakeNiagaraCompatible role=impact assign=impact force
proj.MakeNiagaraCompatible info
Argument Meaning
role=tracer (default) Live follow: Positions / Velocities / Count, short lifetime
role=impact Spawn-and-forget hits: wires Dust / Spark / Debris / Splash by emitter name, otherwise Dust
role=smoke Spawn-and-forget path puffs: TrailSmoke*
dest=/Game/Folder/Asset Output asset. Default <source>_MassCompat next to the source
inplace Patch the selected asset instead of duplicating
force Replace an existing destination
gpu Switch emitters to GPU simulation (may break CPU-only modules)
keepparticles Do not shorten tracer lifetime
emitter=Name Only convert emitters whose name contains this text
assign=projectile / impact / smoke Also write the result into Project Settings
info Print emitters, sim targets, renderers and inputs; writes nothing

The command adds the role's User.* parameters, forces world space, zeroes Spawn Rate, drives Spawn Burst from the role's count, prefers looping, wires Position (and Velocity, Color, Size when present) through Select*FromArray, binds mesh renderers for tracers, then compiles and saves, logging anything it could not wire.

Then: assign the result in Project Settings (or use assign=) and run proj.ReloadAssets.

Heads-up: the Projectile System is shared, so assigning a converted rocket there makes every type draw with it. Types still differ by colour, size and mesh.

What does not convert cleanly

Source feature Result / fix
One-shot "play at location" systems Ghosts at the origin unless Position is array-wired and the emitter loops
CPU ribbons attached to sockets No socket exists; drive ribbons from User.Velocities
CPU collision, events, skeletal mesh DI GPU compile fails. Keep CPU or remove those modules
Spawn Rate as the only spawner Add Spawn Burst Instantaneous and re-run

Manual conversion checklist

  1. Duplicate the system. Set emitters to looping, world space, GPU if possible.
  2. Add User.Count (int) and User.Positions (Position array).
  3. Spawn Burst count = User.Count; Spawn Rate = 0.
  4. Initialize Particle → Position = SelectPositionFromArray(User.Positions) with the index formula above; same for Velocity.
  5. Tracers: lifetime ≈ Engine.DeltaTime × 2.
  6. Assign in Project Settings and run proj.ReloadAssets.

Replacing Niagara entirely (C++)

The core module has no Niagara dependency. Implement IProjectileVisualizer (ProjectileVisualizer.h) and register it from a world subsystem's Initialize with UProjectileSubsystem::SetVisualizer. Disable Enable Niagara Visualizer so both do not run. See API Reference.

Debugging visuals

Tool Use
proj.MakeNiagaraCompatible info Dump the selected system's stack
proj.CreateNiagaraMeshRenderer info Renderers and sim target on the Projectile System
fx.Niagara.Debug.Hud 1 Live Niagara instance and particle counts
proj.ReloadAssets Recreate the runtime components after editing assets
proj.ImpactEffects 0 Fall back to the cheap per-type puff
Log category LogMassProjectiles Missing assets, inactive components, type registration

Particles at the world origin: Position is not array-wired. No particles: Spawn Count not linked, emitter set to Once, or GPU compile failed.

Need help? Contact support.