Variables
Variables are available since v1.1.0. GPU Sync was added in v2.0.0.
Purpose
This guide explains how to use Variables to create reactive shared state in your game. You will learn when to choose Variables over Event Channels, how to set them up, and how to use GPU Sync to drive shaders.
What are variables?
Variables are ScriptableObject assets that store a single typed value and automatically raise events when that value changes. They combine the benefits of Event Channels with persistent state.
flowchart LR
subgraph Variable["PlayerHealth (Variable)"]
V[Value: 80]
E[OnValueChanged]
end
W[Player] -->|"Value = 80"| V
V -->|triggers| E
E -->|notifies| R1[HealthBar UI]
E -->|notifies| R2[Enemy AI]
R3[Any System] -.->|reads| V
// Set value - automatically fires event if changed
playerHealth.Value = 80;
// Read value - no event subscription needed
int currentHealth = playerHealth.Value;
When to use variables
Use variables when
- You need shared state that multiple systems can read
- You want automatic change detection (events only fire when value actually changes)
- UI needs to display current value without waiting for events
- You need to persist state between events
Use event channels when
- You only need to notify about an action (no persistent state)
- The data is transient (button click, collision)
- You don’t need to query the current value
Quick decision guide
| Scenario | Use |
|---|---|
| Player died (notification) | Event Channel |
| Player health changed (state + notification) | Variable |
| Score changed (state + notification) | Variable |
| Button clicked (notification) | Event Channel |
| Game paused (state) | Variable |
Available types
| Type | Example Use Cases |
|---|---|
| Int | Score, health, currency, level |
| Long | Timestamps, large numbers |
| Float | Timer, volume, progress (0-1) |
| Double | High-precision values |
| Bool | Game paused, player alive, feature enabled |
| String | Player name, level name, status message |
| Vector2 | 2D position, joystick input |
| Vector3 | 3D position, velocity, spawn point |
| Quaternion | Rotation, orientation |
| Color | UI color, material tint |
| GameObject | Current target, selected object |
Basic usage
Step 1: Create a variable asset
Right-click in the Project window and select the following menu path.
Create > Reactive SO > Variables > Int Variable
Name it descriptively, such as PlayerHealth or CurrentScore.
Step 2: Configure the variable
Select the asset and configure in the Inspector.
- Initial Value - The starting value (e.g., 100 for health)
- Description - What this variable represents
Step 3: Create an event channel (optional)
If you need to be notified of changes, create a matching event channel.
Create > Reactive SO > Channels > Int Event
Assign it to the variable’s On Value Changed field.
Step 4: Use in your scripts
using Tang3cko.ReactiveSO;
using UnityEngine;
public class Player : MonoBehaviour
{
[SerializeField] private IntVariableSO playerHealth;
public void TakeDamage(int damage)
{
// Setting Value automatically fires the event if changed
playerHealth.Value -= damage;
}
public void Heal(int amount)
{
playerHealth.Value += amount;
}
}
public class HealthBar : MonoBehaviour
{
[SerializeField] private IntVariableSO playerHealth;
[SerializeField] private Image fillImage;
private void Update()
{
// Read current value directly
fillImage.fillAmount = playerHealth.Value / 100f;
}
}
Subscribing to changes
For event-driven updates, subscribe to the event channel:
public class HealthDisplay : MonoBehaviour
{
[SerializeField] private IntVariableSO playerHealth;
[SerializeField] private IntEventChannelSO onHealthChanged;
[SerializeField] private Text healthText;
private void OnEnable()
{
onHealthChanged.OnEventRaised += UpdateDisplay;
// Show initial value immediately
UpdateDisplay(playerHealth.Value);
}
private void OnDisable()
{
onHealthChanged.OnEventRaised -= UpdateDisplay;
}
private void UpdateDisplay(int health)
{
healthText.text = $"HP: {health}";
}
}
Change detection
Variables use EqualityComparer<T> to detect changes. Events only fire when the value actually changes.
graph LR
A["Value = newValue"] --> B{"oldValue == newValue?"}
B -->|YES| C["Do nothing"]
B -->|NO| D["Update value"]
D --> E["Fire OnValueChanged"]
E --> F["Notify subscribers"]
style C fill:#ffcccc
style F fill:#ccffcc
playerHealth.Value = 100; // Event fires (initial set)
playerHealth.Value = 80; // Event fires (100 != 80)
playerHealth.Value = 80; // No event (80 == 80)
This prevents unnecessary updates when setting the same value.
Initial value and reset
Initial value
Set the Initial Value in the Inspector. This is the value used in the following cases.
- Entering Play Mode
- Clicking Reset to Initial in the Inspector
Resetting during gameplay
Use the Reset to Initial button in the Inspector during Play Mode to quickly reset values for testing.
Best practice
Set meaningful initial values in the Inspector rather than in code.
// ❌ Bad: Hardcoded initial value
private void Start()
{
playerHealth.Value = 100; // Don't do this
}
// ✅ Good: Use Inspector's Initial Value
// The variable already starts at 100 from Initial Value
GPU Sync
GPU Sync automatically synchronizes Variable values to shader global properties. This allows shaders, VFX Graph, and Compute Shaders to react to gameplay state.
Supported types
| Variable Type | Shader Method | HLSL Type |
|---|---|---|
| Int | SetGlobalInteger | int |
| Float | SetGlobalFloat | float |
| Vector2 | SetGlobalVector | float4 (xy) |
| Vector3 | SetGlobalVector | float4 (xyz) |
| Quaternion | SetGlobalVector | float4 (xyzw) |
| Color | SetGlobalColor | float4 |
| Bool | SetGlobalInteger | int (0 or 1) |
String, GameObject, Long, and Double do not support GPU Sync.
Enabling GPU Sync
- Select a Variable asset
- Enable GPU Sync Enabled
- Set the GPU Property Name (e.g.,
_PlayerHealth)
Using in shaders
The value is automatically available as a global property:
// No C# bridging code needed
float health = _PlayerHealth;
float3 playerPos = _PlayerPosition.xyz;
// React to gameplay state
float healthFactor = saturate(health / 100.0);
Use cases
- Low health vignette - Shader darkens screen edges when health is low
- Player proximity effects - VFX that reacts to player position
- Game state visuals - Danger level affects lighting color
- Compute shader input - Physics simulations using gameplay data
Common patterns
Pattern 1: Multiple readers
Multiple systems can read the same variable:
// ScoreManager writes
currentScore.Value += 10;
// ScoreText reads
scoreText.text = $"Score: {currentScore.Value}";
// HighScoreChecker listens to event
onScoreChanged.OnEventRaised += CheckHighScore;
Pattern 2: Loading from save data
public void LoadGame(SaveData save)
{
// Setting values notifies all listeners
playerHealth.Value = save.health;
playerLevel.Value = save.level;
playerGold.Value = save.gold;
}
Pattern 3: AI reading player state
public class EnemyAI : MonoBehaviour
{
[SerializeField] private IntVariableSO playerHealth;
private void Update()
{
// Read player state without coupling
if (playerHealth.Value < 30)
{
BecomeAggressive();
}
}
}
Best practices
Name descriptively
// ✅ Good: Clear ownership and purpose
PlayerHealth
CurrentScore
IsPaused
// ❌ Bad: Ambiguous
Health
Score
Paused
Use Inspector for configuration
Configure Initial Value and Description in the Inspector rather than hardcoding in scripts.
Don’t overuse events
If a system only needs the current value, read it directly instead of subscribing to events.
Document in Inspector
Use the Description field to explain what each variable represents.
Limitations
No built-in history
Variables only store the current value. For history, implement it in a subscriber:
private List<int> scoreHistory = new();
private void OnScoreChanged(int score)
{
scoreHistory.Add(score);
}
Reference type mutations not detected
For GameObject variables, changing properties doesn’t trigger events:
// ❌ No event (reference didn't change)
target.Value.GetComponent<Enemy>().health = 50;
// ✅ Event fires (reference changed)
target.Value = newEnemy;
Creating custom variable types
For custom data types, inherit from VariableSO<T>:
[CreateAssetMenu(
fileName = "WeaponVariable",
menuName = "Reactive SO/Variables/Weapon Variable"
)]
public class WeaponVariableSO : VariableSO<Weapon>
{
// Inherits all functionality
}
Create a matching event channel:
[CreateAssetMenu(
fileName = "WeaponEvent",
menuName = "Reactive SO/Channels/Weapon Event"
)]
public class WeaponEventChannelSO : EventChannelSO<Weapon>
{
}
Serialization requirements
Your custom type must be fully serializable. Unity does not serialize certain types.
Supported
- Primitives (
int,float,bool,string) - Unity types (
Vector3,Color,Quaternion) List<T>of serializable types- Arrays of serializable types
- Other
[Serializable]structs/classes
Not supported
Dictionary<K,V>- Multidimensional arrays (
int[,]) HashSet<T>,Queue<T>,Stack<T>- Interfaces
- Delegates
Example
// SAFE: All fields are serializable
[System.Serializable]
public struct WeaponData
{
public string name;
public int damage;
public List<string> tags;
}
// UNSAFE: Dictionary is not serialized
[System.Serializable]
public struct InventoryData
{
public Dictionary<string, int> items; // Will be empty after reload!
}
If your custom type contains non-serializable fields, those fields will be lost when Unity unloads and reloads the ScriptableObject (e.g., during scene transitions). See Troubleshooting for details.
References
- Event Channels Guide - For fire-and-forget notifications
- Event Types Reference - All available types
- Runtime Sets Guide - For tracking object collections