Skip to content

Events

A particle’s life has obvious moments: it’s born, it might bump into something, it dies, it’s destroyed. Without Events, the only thing the plugin does at each of those moments is the default — emit visuals at spawn, animate graphs across life, destroy on lifetime end. Events let you attach more behaviour to each of those moments. Bounce off the wall instead of passing through. Spawn a second emitter when the first dies. Run a short Luau script that mutates the particle, plays a sound, updates game state.

Events are how the plugin connects particle behaviour to game logic without you having to write a per-emitter RenderStepped connection by hand.

Each emitter exposes up to four event types in its property panel. Which ones are available depends on the type — spatial types (Part, Attachment, Model) get all four; non-spatial types (Beam, PointLight, Trail/TrailEmitter, Highlight, the screen-space types, ImageLabel) skip OnHit because they don’t have collisions.

EventFires whenAvailable on
OnEmitA particle clone has just spawned and is entering the simulation loopAll types
OnHitA raycast from the particle’s previous frame position to its current position hits a collision surfacePart, Attachment, Model
OnDeathThe particle’s Lifetime has just expired (before any Linger fade begins)All types
OnDestructionThe visual is about to be destroyed (after Linger, if any)All types

Each event in the panel is collapsed by default with an Enabled toggle. Off costs nothing — the runtime doesn’t pay any per-frame check, the system stays silent. On expands the configuration row for that event.

Every event has the same authoring shape. Once enabled, you can configure any combination of:

  • Emit — pick another emitter (transformed or native) to spawn when the event fires. Common: OnDeath emits an explosion, OnHit emits a spark burst.
  • Mode — controls where the chained emission lands. Four values:
    • AtPosition — at the event’s world position (the spawn point for OnEmit; the hit point for OnHit; the death position for OnDeath / OnDestruction). For OnHit specifically, the spawn position is nudged slightly along the surface normal so the chained emit doesn’t immediately re-collide with the same surface.
    • AtSource — at the originating emitter’s current position. For long-lived emitters that have moved, this is “where the spawner is now” rather than “where this particle was”.
    • AtTarget — clones the picked emitter at its own authored position (the default for non-spatial chained targets).
    • AtCFrame — propagates the source particle’s full CFrame (position and rotation) to the chained emit. Useful when the chained emitter should match the spinner’s orientation — a tumbling fireball that spawns oriented spark trails rather than radial bursts.
  • Script — toggle on to attach a Luau module. The plugin generates a starter script with the event payload’s full API documented in comments. Edit it via the panel’s Edit Script button. Useful for game-logic side effects: play a sound on hit, track damage, update a quest objective on death.
  • Chain depth — a slider from 1 to 32 capping how many nested event hops can fire from this one. Default is 4. If a chained event tries to emit deeper than the cap, the emit is silently dropped (and a once-per-second warning lands in Output). Hard ceiling of 32 prevents runaway recursion.
  • Reset Event — wipes the per-event configuration and any attached script. Confirms before destroying.

The Emit picker and the Script toggle are independent — you can have one without the other, or both. An event with just an Emit triggers a chained particle effect with no extra logic; one with just a Script runs your code without spawning anything; one with both does both.

OnHit is the most feature-rich of the four because collisions are physical events that the plugin can react to, not just observe.

Once you enable OnHit, a Collide dropdown appears with four options. This is the headline feature.

Off (the default) — pass-through. OnHit still fires (so chained emits and scripts run), but the particle’s motion is unaffected. The particle continues through the surface as if it weren’t there. Use this for cosmetic-only impact effects: footstep dust that doesn’t affect the bullet, ricochet sparks where the bullet penetrates anyway.

Kill — snap the visual to the hit position, then destroy the particle. OnDeath fires before destruction (unless you suppress it in a script). If Linger is set, the visual stays visible for that linger window before being destroyed. The classic “bullet hits wall, vanishes on impact” behaviour.

Stop — snap to the hit position and freeze all motion. The particle ages out naturally, firing OnDeath at lifetime end like normal. Used for particles that should stick — a dart embedded in a wall, a magical seal that attaches on contact, an arrow that lands and stays. While stuck, subsequent raycasts don’t re-fire OnHit (the particle is at rest).

Bounce — realistic reflection. Three tunable scalars become editable when this mode is picked:

  • Bounciness (0 to 1, default 0.7) — restitution. 0 is no bounce (acts like Stop); 1 is a perfect mirror reflection. Most natural objects bounce around 0.3 to 0.7.
  • Friction (0 to 1, default 0.2) — tangential damping. Reduces sideways velocity per bounce. Higher values mean each successive bounce visibly slows down; 0 lets a particle slide forever along a surface.
  • Spin (0 to 2, default 0.5) — angular impulse on collision. Cross-product of the hit normal with the tangential velocity feeds rotational spin to the particle, so forward-rolling bouncers tumble naturally down slopes.

The Bounce model splits the particle’s velocity into normal and tangent components, applies the physics, and writes the new velocity back. Successive bounces lose energy through both Bounciness and Friction.

Two compatibility carve-outs to know about:

  • InvertMotion = true silently downgrades Bounce to Kill. The reverse-motion path uses a cached CFrame trajectory that can’t adapt to mid-flight redirection, so the engine can’t honour Bounce there. Author your bouncing rigs with InvertMotion = false.
  • Animate mode skips Kill / Stop / Bounce. Animate mode plays graphs on the source instance directly, with no clones to redirect. OnHit’s collision modes only meaningfully apply to Emit mode where each particle is its own clone.

Filter — collision groups and exclude lists

Section titled “Filter — collision groups and exclude lists”

Below the Collide dropdown, a Filter row lets you scope which surfaces register hits:

  • Collision Group — pick a Roblox collision group name (the same groups you set up in Studio’s Collision Groups editor). If set, only surfaces in that group trigger OnHit. Leave empty to raycast against everything.
  • Exclude List — open the picker and select any number of instances. Their entire descendant trees are filtered out of the raycast. Useful for preventing self-hits (exclude the character that fired the projectile) or for letting projectiles pass through friendly NPCs.

A HitCheckInterval slider (0 to 0.5 seconds, default 0) controls the raycast frequency independent of the Collide mode. 0 raycasts every Heartbeat frame; higher values skip frames between checks to save CPU on high-rate emitters, at the cost of potentially tunnelling through thin surfaces. Most authoring should leave this at 0; reach for it only if you’re emitting hundreds of physically-relevant particles per second and the profiler shows raycasts dominating.

Two near-the-end events look similar but fire at different moments:

  • OnDeath — fires when Lifetime expires. Before any Linger fade begins. The particle’s last animated values are still those at the end of its life graphs.
  • OnDestruction — fires right before the visual instance is destroyed. After Linger completes (or immediately, if PartLife = 0).

If your emitter has PartLife = 0 (no linger), the two events fire on consecutive frames — OnDeath first, then OnDestruction once the engine kills the visual. With a non-zero PartLife, OnDestruction fires later, after the visible fade.

Pick OnDeath for “this particle just ran out — spawn the next thing” effects. Pick OnDestruction for “this particle is finally gone — clean up game state”.

Trust — the security boundary for imported scripts

Section titled “Trust — the security boundary for imported scripts”

The plugin distinguishes between event scripts you authored locally and scripts that arrived in your project from somewhere else (a Toolbox model, a cloud save, a shared rig). Imported scripts are flagged as untrusted by default and do not run, even if their event is Enabled.

When you select an emitter with an untrusted event, the panel shows a gold “Untrusted — review the script, then click Trust before this can run” badge. The flow:

  1. Click Edit Script to open the Luau code in Studio’s Script Editor.
  2. Read it. Make sure it does what it claims to do.
  3. Click Trust Script (the badge becomes a button).
  4. The flag clears and the event can run.

Enabling the event does not implicitly trust it. You always have to inspect the source first. If you delete and re-import the asset, the new copy is independently untrusted.

The reason: arbitrary Luau code from elsewhere shouldn’t run unattended in your project. Trust is per-asset, per-import, and per-event. Re-using a rig you wrote yourself doesn’t re-trigger this — only foreign imports.

Each emitter type’s property panel has a collapsible Events section, usually near the bottom of the panel below the type-specific properties. Inside it, one row per applicable event type — OnEmit, OnHit (if the type supports it), OnDeath, OnDestruction.

Each row is a toggle at the top. Off shows just the toggle and the event’s name. On expands the row to show the Description / Emit picker / Mode dropdown / Script toggle / Chain depth slider / (OnHit only) Collide controls / (if untrusted) Trust badge / Reset button.

Multi-select editing works the way it does elsewhere in the panel — if two emitters have different Mode values, the dropdown shows a tristate ”~” so you don’t accidentally overwrite. Edits propagate to every selected emitter.

For scripter-side authoring, the public runtime API gains a new method:

Part_Icles:EnableEmitAt(item, originCFrame, emitCtx?)

This fires item (a transformed emitter, a native PE, or a Trail) at a specific world CFrame, overriding the target’s authored position. The optional emitCtx carries chain-tracking metadata (depth, parent chain id) — Events use it internally to enforce the Chain depth cap and to keep nested emits associated with the originating particle.

When to reach for :EnableEmitAt:

  • You’re writing an event script and need to spawn an emitter at a position you computed dynamically — not the authored Link or default position, but a runtime-computed point.
  • You want chain-depth enforcement and the standard chain-id propagation, which :Emit and :EnableEmit don’t provide on their own.

For the common case of “fire this emitter at its authored position”, continue using :EnableEmit(item) or :AbsoluteEmit(item) — they’re shorter and handle the common case directly.

A canonical Events-driven setup:

  1. Author a Fireball Part emitter. Lifetime 3 sec, Speed 20 studs/sec, PartLife = 0.5 for a fade-out, red colour graph.

  2. OnEmit event — Mode = AtCFrame, Emit = a separate SparkTrail Attachment emitter. Each fireball burst emits a spark trail oriented to match the fireball’s pose at spawn.

  3. OnHit event — Collide = Bounce, Bounciness 0.8, Friction 0.3, Spin 0.7. No Emit set on this event. The fireball bounces realistically off walls; its rotation tumbles with each bounce.

  4. OnDeath event — Mode = AtPosition, Emit = an Explosion Part emitter. When the fireball’s 3-second lifetime expires, an explosion bursts at its final position.

  5. OnDestruction event — Mode = AtPosition, Emit = a LingeringGlow PointLight emitter. After the 0.5-sec fade completes and the fireball visual is destroyed, a slow PointLight pulse marks the spot for another second or two.

That’s the full chain. No custom Luau scripts — every behaviour comes from the Emit picker + Mode dropdown + the Bounce mode. If you want a hit-sound on bounce, attach a tiny Script to OnHit that calls SoundService on the hit position.

A few quirks and gotchas.

Chain depth caps at 32, defaults to 4. A chain of length 4 covers most authoring: fireball → spark trail → individual spark → afterglow. Past that, complex chains start hitting limits silently. Bump the slider on the specific event if you need deeper, but think hard about whether the structure can be flattened first.

Enabling an event doesn’t auto-trust its script. A common confusion: flip Enabled to on, expect the script to run, and it doesn’t. Check for the gold Untrusted badge. Trust is an explicit second click.

InvertMotion and Animate mode both disable Bounce. Bounce needs a per-frame mutable trajectory, which neither of those modes provides. The runtime silently downgrades to Kill (for InvertMotion) or treats Collide as Off (for Animate mode).

OnHit doesn’t refire on the same surface. Once a hit is registered, the surface is held until the particle clears (moves away from contact); only then does OnHit re-arm. So Off mode doesn’t spam OnHit for every frame the particle’s inside a wall — it fires once, on entry.

Events fire even if the Emit picker is empty. If you’ve enabled an event purely for its Script, you don’t need an Emit. The script runs; nothing chains.

Events are the plugin’s “what happens when” layer. The next chapter — Why Mesh Particles Beat Native ParticleEmitter — steps out of authoring and into architecture: how the plugin’s per-particle hot loop manages to outrun a sprite-quad system on the same hardware.