# 2. VFX Exposed Properties, Events and Binders Part of [[VFX Graph]]. Previous: [[1. VFX Graph Setup and Mental Model]]. Next: [[3. Worked Effect - Impact Sparks with One Graph]]. > [!note] Verification status > From the 17.5 Component API page (2026-11-10): string names are "least optimized", `Shader.PropertyToID` ints "most optimized", `ExposedProperty` combines both; `SendEvent(nameOrId, eventAttribute)`; event attributes are **copied** when sent; commands are processed in the component's next update, **after LateUpdate**. Binder API (`VFXBinderBase`, `IsValid`, `UpdateBinding`, `[VFXBinder]`, `[VFXPropertyBinding]`) from the Property Binders page. **Code not compiled in Unity.** ## 2.1 Property IDs Use `ExposedProperty` (namespace `UnityEngine.VFX.Utility`) as `static readonly` fields. It's a string you can read in code that resolves to the integer ID. ```csharp // Assets/Scripts/VFX/VfxIds.cs using UnityEngine.VFX.Utility; /// One place for every property/event name used by C#. Rename here, not in 20 scripts. public static class VfxIds { // Blackboard properties public static readonly ExposedProperty BeaconColor = "BeaconColor"; public static readonly ExposedProperty Intensity = "Intensity"; public static readonly ExposedProperty Radius = "Radius"; // Custom events public static readonly ExposedProperty OnImpact = "OnImpact"; public static readonly ExposedProperty OnPulse = "OnPulse"; // Built-in attribute names used in event payloads public static readonly ExposedProperty Position = "position"; public static readonly ExposedProperty Color = "color"; public static readonly ExposedProperty Size = "size"; public static readonly ExposedProperty Velocity = "velocity"; } ``` ## 2.2 Script: beacon controller (properties + events) ```csharp // Assets/Scripts/VFX/BeaconController.cs using UnityEngine; using UnityEngine.VFX; [RequireComponent(typeof(VisualEffect))] public class BeaconController : MonoBehaviour { [ColorUsage(true, true)] public Color idleColor = new Color(0.4f, 0.8f, 1f) * 2f; [ColorUsage(true, true)] public Color activeColor = new Color(1f, 0.7f, 0.2f) * 4f; public float pulseDecay = 2f; VisualEffect vfx; VFXEventAttribute payload; float pulse; // 0..1, decays void Awake() { vfx = GetComponent<VisualEffect>(); payload = vfx.CreateVFXEventAttribute(); // cache: matches this graph } void Update() { pulse = Mathf.MoveTowards(pulse, 0f, pulseDecay * Time.deltaTime); if (vfx.HasVector4(VfxIds.BeaconColor)) vfx.SetVector4(VfxIds.BeaconColor, Color.Lerp(idleColor, activeColor, pulse)); if (vfx.HasFloat(VfxIds.Intensity)) vfx.SetFloat(VfxIds.Intensity, 1f + pulse * 2f); } /// Call from your quest system when an objective updates. public void Pulse() { pulse = 1f; payload.SetVector3(VfxIds.Position, transform.position); vfx.SendEvent(VfxIds.OnPulse, payload); // payload is copied; safe to reuse } public void SetActive(bool on) { if (on) vfx.Play(); else vfx.Stop(); // Stop = stop spawning; live particles finish } } ``` **Explanation** - `Has*` guards make the script tolerant of a designer renaming or removing a property - it silently does nothing instead of throwing. In production, log once when a guard fails. - Colors are `Vector4` on the VFX side; `SetVector4` accepts a `Color` via implicit conversion. - In the graph: add an **Event** context named `OnPulse` -> **Spawn** with **Single Burst** 150 -> **Initialize** with **Inherit Source Position**. The default `OnPlay` keeps the constant column running. - Because the component processes queued commands after `LateUpdate`, sending an event and reading `aliveParticleCount` in the same frame tells you nothing (and `aliveParticleCount` updates roughly once per second anyway). ## 2.3 Playback controls | Call | Effect | |---|---| | `Play()` / `Stop()` | Send the default play/stop events to Spawn contexts | | `pause = true` + `AdvanceOneFrame()` | Step debugging | | `Reinit()` | Reset time, resend the initial event - a full restart | | `playRate` | Time scale for this effect (slow-mo, hit-stop) | | `Simulate(dt, steps)` | Pre-warm, e.g. a fire that should already be burning when the scene loads | | `resetSeedOnPlay` / `startSeed` | Deterministic randomness for replays and screenshots | Hit-stop (Level Up! combat feel) is a one-liner: set `playRate = 0.05f` on the impact effect for 60 ms while the game freezes, then restore. ## 2.4 Script: a custom Property Binder Binders push scene values into properties without a custom MonoBehaviour per effect. Built-in binders exist for transforms, colliders, lights, terrain, audio spectrum, raycasts and UI. Here's one that maps **player health** to an effect's intensity - smoke and sparks rising off a damaged mech, for instance. ```csharp // Assets/Scripts/VFX/HealthToFloatBinder.cs using UnityEngine; using UnityEngine.VFX; using UnityEngine.VFX.Utility; public interface IHealthSource { float Current { get; } float Max { get; } } [AddComponentMenu("VFX/Property Binders/Health To Float Binder")] [VFXBinder("Gameplay/Health To Float")] public class HealthToFloatBinder : VFXBinderBase { [VFXPropertyBinding("System.Single")] public ExposedProperty property = "Damage01"; [Tooltip("Any component implementing IHealthSource")] public MonoBehaviour healthComponent; [Tooltip("Output = 1 - health/max, shaped by this curve")] public AnimationCurve response = AnimationCurve.Linear(0f, 0f, 1f, 1f); IHealthSource Health => healthComponent as IHealthSource; public override bool IsValid(VisualEffect component) => Health != null && component.HasFloat(property); public override void UpdateBinding(VisualEffect component) { float damage01 = Health.Max > 0f ? 1f - Mathf.Clamp01(Health.Current / Health.Max) : 0f; component.SetFloat(property, response.Evaluate(damage01)); } public override string ToString() => quot;Health -> {property}"; } ``` **Explanation** - `[VFXBinder("Gameplay/Health To Float")]` adds the binder to the **VFX Property Binder** component's add menu. - `[VFXPropertyBinding("System.Single")]` gives the field a drawer that lists only float properties from the graph. - `IsValid` runs before `UpdateBinding`; put all null/Has checks there. - Implement `IHealthSource` on your health component (see [[1.Stat Definitions and Base vs Derived Stats]] or [[High Scores, Saves and Player Stats]]). Interface over concrete type keeps the binder reusable - the **Component** pattern's decoupling. ## 2.5 Exercise Wire `BeaconController.Pulse()` to a quest objective update event from [[Quest Systems]]. The landmark now *answers* the player: progress = the beacon flares. That is Level Up!'s positive feedback loop in one line. ## Sources - Component API (IDs, events, payload copy, update timing): https://docs.unity3d.com/Packages/[email protected]/manual/ComponentAPI.html - ExposedProperty helper: https://docs.unity3d.com/Packages/[email protected]/manual/ExposedPropertyHelper.html - Property Binders (custom binder example): https://docs.unity3d.com/Packages/[email protected]/manual/PropertyBinders.html - Events: https://docs.unity3d.com/Packages/[email protected]/manual/Events.html - `VisualEffect.SendEvent`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/VFX.VisualEffect.SendEvent.html