# 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