ue-niagara-effects

SKILL.md

UE Niagara Effects

You are an expert in controlling Unreal Engine's Niagara VFX system from C++.

Context Check

Read .agents/ue-project-context.md before proceeding. Confirm:

  • The Niagara plugin is listed under enabled plugins (Plugins/FX/Niagara).
  • The target module's Build.cs has "Niagara" (and optionally "NiagaraCore") in PublicDependencyModuleNames.
  • Platform targets: note whether mobile or dedicated-server builds are in scope, because Niagara is typically suppressed on dedicated servers and may need LOD simplification on mobile.

Information Gathering

Before writing Niagara C++ code, clarify:

  1. Effect lifecycle — one-shot (fire and forget) or persistent / looping?
  2. Parameter needs — which Niagara User Parameters must be set from gameplay (positions, colors, scalars)?
  3. Data interfaces required — SkeletalMesh, StaticMesh, Curve, Array, or custom?
  4. Simulation target — CPU or GPU sim? (affects which DI features are available)
  5. Performance budget — pooling required? Mobile scalability tier?
  6. Completion handling — does gameplay need a callback when the effect finishes?

System Structure (UE Concept Map)

UNiagaraSystem  (asset: UNiagaraSystem)
  └── UNiagaraEmitter[]         (per-emitter asset, referenced via FNiagaraEmitterHandle)
        └── UNiagaraScript[]   (Spawn / Update / Event scripts; authored in Niagara editor)
              └── Modules       (stack of NiagaraScript nodes; not C++ classes)

Runtime instances:
  UNiagaraComponent             (scene component that drives one UNiagaraSystem instance)
    └── FNiagaraSystemInstance  (internal runtime state; access via GetSystemInstanceController())

Key rule: authors expose parameters to C++ by setting their namespace to User. in the Niagara editor. Only User.* parameters can be overridden at runtime from C++.


Spawning Niagara Systems

Fire-and-Forget (One-Shot) at World Location

#include "NiagaraFunctionLibrary.h"
#include "NiagaraComponent.h"

// Minimal one-shot spawn — component auto-destroys when the system completes.
UNiagaraComponent* NiagaraComp = UNiagaraFunctionLibrary::SpawnSystemAtLocation(
    this,                           // WorldContextObject
    ImpactVFXSystem,                // UPROPERTY(EditAnywhere) UNiagaraSystem*
    HitLocation,                    // FVector Location
    FRotator::ZeroRotator,          // FRotator Rotation
    FVector(1.f),                   // FVector Scale
    /*bAutoDestroy=*/ true,
    /*bAutoActivate=*/ true,
    /*PoolingMethod=*/ ENCPoolMethod::AutoRelease,   // use pool when available
    /*bPreCullCheck=*/ true
);

// Set parameters before the first tick if needed.
if (NiagaraComp)
{
    NiagaraComp->SetVariableVec3(FName("User.HitNormal"), HitNormal);
    NiagaraComp->SetVariableLinearColor(FName("User.HitColor"), DamageColor);
}

Attached to a Component (Persistent / Looping)

// Attaches to a socket and stays active until manually deactivated.
UNiagaraComponent* TrailComp = UNiagaraFunctionLibrary::SpawnSystemAttached(
    TrailVFXSystem,
    WeaponMesh,                             // USceneComponent* AttachToComponent
    FName("MuzzleSocket"),                  // FName AttachPointName
    FVector::ZeroVector,
    FRotator::ZeroRotator,
    EAttachLocation::SnapToTarget,
    /*bAutoDestroy=*/ false,
    /*bAutoActivate=*/ true,
    ENCPoolMethod::ManualRelease,
    /*bPreCullCheck=*/ true
);

Persistent Component on an Actor (Preferred for Repeated Use)

// In header:
UPROPERTY(VisibleAnywhere)
TObjectPtr<UNiagaraComponent> EngineTrailVFX;

// In constructor:
EngineTrailVFX = CreateDefaultSubobject<UNiagaraComponent>(TEXT("EngineTrailVFX"));
EngineTrailVFX->SetupAttachment(GetRootComponent());
EngineTrailVFX->SetAutoActivate(false);   // start inactive; activate via gameplay

// In gameplay code:
EngineTrailVFX->SetAsset(EngineTrailSystem);    // swap asset without destroying component
EngineTrailVFX->Activate(/*bReset=*/ true);

Lifecycle Control

NiagaraComp->Activate(/*bReset=*/ false);       // activate; resume if paused
NiagaraComp->Activate(/*bReset=*/ true);        // activate with full reset
NiagaraComp->Deactivate();                      // stop spawning, let particles drain
NiagaraComp->DeactivateImmediate();             // kill all particles immediately
NiagaraComp->ResetSystem();                     // restart from time 0
NiagaraComp->ReinitializeSystem();              // full re-init + restart (expensive; prefer ResetSystem)
NiagaraComp->SetPaused(true);                   // pause simulation
NiagaraComp->SetAutoDestroy(true);              // destroy component when system finishes

Setting Parameters from C++

All setter variants accept the parameter name as FName prefixed with its namespace. User-exposed parameters use the User. prefix.

// Scalar types
NiagaraComp->SetVariableFloat(FName("User.DamageAmount"), 150.f);
NiagaraComp->SetVariableInt(FName("User.ProjectileCount"), 12);
NiagaraComp->SetVariableBool(FName("User.bIsCritical"), bIsCriticalHit);

// Vector types
NiagaraComp->SetVariableVec2(FName("User.UVOffset"), FVector2D(0.5, 0.25));
NiagaraComp->SetVariableVec3(FName("User.TargetPosition"), TargetLocation);
NiagaraComp->SetVariableVec4(FName("User.CustomData"), FVector4(1, 0.5, 0, 1));
NiagaraComp->SetVariableLinearColor(FName("User.TintColor"), FLinearColor::Red);
NiagaraComp->SetVariableQuat(FName("User.Orientation"), GetActorQuat());

// Object / Actor references (binds DI override)
NiagaraComp->SetVariableObject(FName("User.TargetMesh"), SkeletalMeshComponent);
NiagaraComp->SetVariableActor(FName("User.SourceActor"), this);

// Position (LWC-aware alias for vec3)
NiagaraComp->SetVariablePosition(FName("User.WorldOrigin"), WorldSpaceOrigin);

// Material / Texture overrides
NiagaraComp->SetVariableMaterial(FName("User.FXMaterial"), DynamicMaterial);
NiagaraComp->SetVariableTexture(FName("User.FlowMap"), FlowTexture);

// Read a float parameter back (returns bIsValid=false when name not found)
bool bIsValid = false;
float CurrentValue = NiagaraComp->GetVariableFloat(FName("User.EmitRate"), bIsValid);

Blueprint-Accessible Legacy Signatures (prefer FName variants above)

// Old FString signatures still work but are slower due to FName conversion.
NiagaraComp->SetNiagaraVariableFloat(TEXT("User.SpeedScale"), 2.f);
NiagaraComp->SetNiagaraVariableVec3(TEXT("User.ImpactPoint"), Location);
NiagaraComp->SetNiagaraVariableLinearColor(TEXT("User.Color"), FLinearColor::Blue);

Parameter Namespaces Reference

Namespace prefix Settable from C++ Description
User. Yes User-exposed; main runtime override
System. No (read-only) System-level built-ins (Age, DeltaTime, etc.)
Emitter. No (internal) Per-emitter variables
Particle. No (internal) Per-particle variables

See references/niagara-parameter-types.md for the full C++ type to Niagara type mapping.


Data Interfaces from C++

Data interfaces (DIs) are UObject-derived assets that expose structured external data to Niagara scripts. They appear as User.* parameters of DI type in the Niagara editor, and are overridden at runtime via SetVariableObject or the specialized function library helpers.

Binding Skeletal Mesh DI

#include "NiagaraFunctionLibrary.h"

// Override the "User.SourceMesh" skeletal mesh DI on a running component.
UNiagaraFunctionLibrary::OverrideSystemUserVariableSkeletalMeshComponent(
    NiagaraComp,
    TEXT("User.SourceMesh"),    // must match the DI's User parameter name in the asset
    GetMesh()                   // USkeletalMeshComponent*
);

// Restrict which bones spawn from (destructive — modifies the DI instance data).
UNiagaraFunctionLibrary::SetSkeletalMeshDataInterfaceFilteredBones(
    NiagaraComp,
    TEXT("User.SourceMesh"),
    { FName("hand_l"), FName("hand_r") }
);

// Restrict which sampling regions to use.
UNiagaraFunctionLibrary::SetSkeletalMeshDataInterfaceSamplingRegions(
    NiagaraComp,
    TEXT("User.SourceMesh"),
    { FName("HeadRegion") }
);

Binding Static Mesh DI

// Override via component reference.
UNiagaraFunctionLibrary::OverrideSystemUserVariableStaticMeshComponent(
    NiagaraComp,
    TEXT("User.ScatterMesh"),
    StaticMeshComp
);

// Override with a raw UStaticMesh asset pointer.
UNiagaraFunctionLibrary::OverrideSystemUserVariableStaticMesh(
    NiagaraComp,
    TEXT("User.ScatterMesh"),
    LoadedStaticMesh
);

Reading / Modifying an Array DI at Runtime

#include "NiagaraDataInterfaceArrayFunctionLibrary.h"

// Push a new float array into the effect (e.g., damage heatmap values).
TArray<float> HeatValues = ComputeHeatValues();
UNiagaraDataInterfaceArrayFunctionLibrary::SetNiagaraArrayFloat(
    NiagaraComp, FName("User.HeatData"), HeatValues
);

// Update a single element without replacing the whole array.
UNiagaraDataInterfaceArrayFunctionLibrary::SetNiagaraArrayFloatValue(
    NiagaraComp, FName("User.HeatData"), /*Index=*/ 5, /*Value=*/ 0.9f, /*bSizeToFit=*/ false
);

// Other strongly-typed array setters available:
// SetNiagaraArrayVector, SetNiagaraArrayVector4, SetNiagaraArrayColor,
// SetNiagaraArrayQuat, SetNiagaraArrayInt32, SetNiagaraArrayBool, etc.

Direct DI Object Access (Advanced)

// Retrieve the actual DI UObject to mutate its properties directly.
// Template variant resolves the cast automatically.
UNiagaraDataInterfaceCurve* CurveDI =
    UNiagaraFunctionLibrary::GetDataInterface<UNiagaraDataInterfaceCurve>(
        NiagaraComp, FName("User.SpeedCurve")
    );

if (CurveDI)
{
    // Mutate curve keyframes at runtime (rebuilds LUT internally).
    CurveDI->Curve.AddKey(0.f, 0.f);
    CurveDI->Curve.AddKey(1.f, 500.f);
    // UpdateLUT() is WITH_EDITORONLY_DATA — only call in editor builds.
#if WITH_EDITORONLY_DATA
    CurveDI->UpdateLUT();
#endif
}

// Non-template variant when the DI class is only known at runtime.
UNiagaraDataInterface* GenericDI =
    UNiagaraFunctionLibrary::GetDataInterface(
        UNiagaraDataInterfaceStaticMesh::StaticClass(),
        NiagaraComp,
        FName("User.ImpactMesh")
    );

See references/niagara-data-interfaces.md for the full built-in DI catalogue.

Custom Data Interfaces: Subclass UNiagaraDataInterface, override GetFunctions() to define available functions, GetVMExternalFunction() to bind C++ implementations, and optionally ProvidePerInstanceDataForRenderThread() for GPU access. Register in the module's StartupModule. This enables game-specific data (inventory, terrain) to feed directly into Niagara systems.


Completion Callbacks

// Bind a C++ delegate to fire when the Niagara system finishes all particles.
// FOnNiagaraSystemFinished is DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(, UNiagaraComponent*)
NiagaraComp->OnSystemFinished.AddDynamic(this, &UMyComponent::OnVFXFinished);

// The callback signature:
UFUNCTION()
void UMyComponent::OnVFXFinished(UNiagaraComponent* FinishedComponent)
{
    // Called on game thread when every particle has expired and the system is done.
    FinishedComponent->DestroyComponent();
    // or return it to pool, notify gameplay, etc.
}

// Unbind when the owner is destroyed to avoid stale delegates.
NiagaraComp->OnSystemFinished.RemoveDynamic(this, &UMyComponent::OnVFXFinished);

Performance: Pooling

ENCPoolMethod controls the pool behavior on every spawn call:

  • AutoRelease — component returns to the world pool automatically when the system finishes. Pass bAutoDestroy=true; the pool handles actual reclaim.
  • ManualRelease — you control when the component returns; call ReleaseToPool() to reclaim.
  • None — no pooling; component is destroyed when finished if bAutoDestroy=true.
// AutoRelease: most common for one-shots (explosions, impacts).
UNiagaraComponent* Comp = UNiagaraFunctionLibrary::SpawnSystemAtLocation(
    this, ExplosionSystem, Location, FRotator::ZeroRotator,
    FVector(1.f), /*bAutoDestroy=*/true, /*bAutoActivate=*/true,
    ENCPoolMethod::AutoRelease
);

// ManualRelease: for effects you pause/resume (e.g., a beam while a button is held).
// Reclaim by calling ReleaseToPool() when done.
TrailComp->ReleaseToPool();

// Prime the pool before a gameplay-critical moment via FNiagaraWorldManager.
if (FNiagaraWorldManager* NiagaraWorldMan = FNiagaraWorldManager::Get(GetWorld()))
{
    NiagaraWorldMan->GetComponentPool()->PrimePool(ExplosionSystem, GetWorld());
}

Pool capacity is configured per-system in the UNiagaraSystem pooling settings (not a global CVar). Relevant global pool CVars: FX.NiagaraComponentPool.Enable (1/0) and FX.NiagaraComponentPool.KillUnusedTime (seconds before idle components are culled).


Performance: Scalability and LOD

// Allow the scalability manager to cull this component based on distance and budget.
NiagaraComp->SetAllowScalability(true);   // default true; disable for gameplay-critical VFX

// Adjust tick behavior to avoid unnecessary dependency resolution.
// ENiagaraTickBehavior::UsePrereqs  — default; ticks after its prerequisites
// ENiagaraTickBehavior::ForceTickFirst — useful for VFX that leads all tick groups
NiagaraComp->SetTickBehavior(ENiagaraTickBehavior::UsePrereqs);

Scalability per platform is configured in the UNiagaraEffectType asset assigned to the UNiagaraSystem. The effect type defines quality tiers (Low / Medium / High / Epic) and which emitters are stripped at each tier. This is data-driven; no C++ changes needed per platform.

GPU vs CPU simulation trade-offs:

  • CPU sim: particle data is readable/writable from C++ each frame; lower particle counts; supports all DI types.
  • GPU sim: supports hundreds of thousands of particles; DI support is limited (not all CPU-side DIs have GPU equivalents); particle data is not readable back to CPU without readbacks.

Determinism: GPU simulations are inherently non-deterministic. For multiplayer VFX that must match across clients, use CPU simulation with FixedTickDelta on the emitter. Cosmetic-only effects should spawn client-side only — skip them on dedicated servers entirely.


Warm-Up, Server Handling, and Events

Pre-simulation (warm-up): seek to a desired age before the effect is visible.

NiagaraComp->SetDesiredAge(2.5f);    // age in seconds
NiagaraComp->SeekToDesiredAge(2.5f); // perform seek immediately (skips simulation steps)
// FFXSystemSpawnParameters (used by SpawnSystemAtLocationWithParams) also exposes DesiredAge.

Dedicated server: SpawnSystemAtLocation returns nullptr on dedicated servers. Always null-check the returned component and guard VFX spawns with !IsRunningDedicatedServer() where needed.

Gameplay events to Niagara: Niagara's internal event system (Location Events, Death Events, Collision Events) is configured in the Niagara editor between emitters. From C++, trigger a gameplay-driven burst by updating a User bool parameter that the spawn script reads:

NiagaraComp->SetVariableBool(FName("User.bJustDied"), true);
// Niagara reads this flag on the next spawn script tick and fires the burst.
// There is no C++ API to inject raw Niagara events directly — use User parameters as the bridge.

Required Build.cs

PublicDependencyModuleNames.AddRange(new string[]
{
    "Core",
    "CoreUObject",
    "Engine",
    "Niagara",         // UNiagaraComponent, UNiagaraFunctionLibrary, UNiagaraSystem
    "NiagaraCore",     // UNiagaraDataInterface base (NiagaraCore module)
});

Common Mistakes and Anti-Patterns

Spawning a new system component every tick

// BAD: Creates a new UNiagaraComponent each frame. Destroys performance.
void AMyActor::Tick(float DeltaTime)
{
    UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, TrailFX, GetActorLocation());
}

// GOOD: Create the component once in BeginPlay or constructor; activate/deactivate as needed.

Wrong parameter namespace

// BAD: "Emitter.Speed" is an internal emitter parameter; cannot be set from C++.
NiagaraComp->SetVariableFloat(FName("Emitter.Speed"), 300.f);

// GOOD: The Niagara author must expose the parameter under "User.*".
NiagaraComp->SetVariableFloat(FName("User.Speed"), 300.f);

Type mismatch between C++ and Niagara

// BAD: Calling SetVariableVec3 on a parameter that is typed as "Color" in Niagara.
// Silently fails — no runtime error, parameter is just not updated.
NiagaraComp->SetVariableVec3(FName("User.TintColor"), FVector(1, 0, 0));

// GOOD: Match the C++ call to the Niagara parameter type.
NiagaraComp->SetVariableLinearColor(FName("User.TintColor"), FLinearColor::Red);

Setting parameters after system completes

// The component is valid but the system instance may be inactive. Check before setting.
if (NiagaraComp && NiagaraComp->IsActive())
{
    NiagaraComp->SetVariableFloat(FName("User.Intensity"), NewIntensity);
}

Forgetting to check nullptr on spawn (especially on server)

UNiagaraComponent* Comp = UNiagaraFunctionLibrary::SpawnSystemAtLocation(...);
// Comp can be nullptr on dedicated server or when bPreCullCheck rejects the spawn.
if (Comp)
{
    Comp->SetVariableFloat(FName("User.Scale"), 2.f);
}

Not removing delegates before destruction

// BAD: OnSystemFinished fires after owner is garbage collected → crash.
// GOOD: Always RemoveDynamic in BeginDestroy or EndPlay.
void AMyActor::EndPlay(const EEndPlayReason::Type Reason)
{
    if (NiagaraComp)
    {
        NiagaraComp->OnSystemFinished.RemoveDynamic(this, &AMyActor::OnVFXFinished);
    }
    Super::EndPlay(Reason);
}

Niagara Fluids (experimental): The Niagara Fluids plugin provides GPU-based fluid and gas simulations. It is experimental, GPU-only, and carries a high performance cost — profile carefully before shipping and restrict use to hero effects where visual impact justifies the budget.

World space vs local space: Use local-space simulation for effects attached to moving actors (particles inherit the parent component's transform and move with it). Use world-space simulation for effects that should remain stationary after emission (e.g., a ground impact crater where particles should not follow a moving actor). The space setting lives on each emitter in the Niagara editor.


Related Skills

  • ue-actor-component-architecture — component creation, attachment, lifecycle
  • ue-materials-rendering — particle material setup, dynamic material instances
  • ue-cpp-foundations — UObject lifetime, delegates, UPROPERTY references
Weekly Installs
9
GitHub Stars
52
First Seen
6 days ago
Installed on
gemini-cli9
github-copilot9
codex9
amp9
cline9
kimi-cli9