Actions
Actions are available since v2.1.0. This feature implements the Command pattern using ScriptableObjects.
Purpose
This guide explains how to use Actions (ActionSO) to define reusable, data-driven commands. You will learn when to use them, how to set them up, and best practices for common scenarios.
What are actions?
Actions are ScriptableObject assets that encapsulate executable commands. They implement the Command pattern, allowing you to define reusable behaviors as assets that can be configured in the Inspector and triggered from code.
flowchart LR
subgraph Callers
C1[Button]
C2[EventTable]
C3[QuestSystem]
end
A[("SpawnReward<br>(ActionSO)")]
subgraph Effects
E1[Instantiate prefab]
E2[Play sound]
E3[Show UI]
end
C1 -->|Execute| A
C2 -->|Execute| A
C3 -->|Execute| A
A --> E1
A --> E2
A --> E3
This pattern offers several benefits.
- Actions can be configured in the Inspector without code changes
- The same action can be reused across multiple systems
- Caller information is tracked automatically for debugging
- Actions integrate with the Monitor Window for real-time visibility
When to use actions
Use Actions for the following scenarios.
- Data-driven systems - Quest rewards, event tables, dialogue responses
- Configurable behaviors - Spawn effects, play sounds, show UI
- Reusable commands - Actions that need to be triggered from multiple places
- Inspector-configurable logic - When designers need to tweak behavior without code
Do not use Actions for the following scenarios.
- Simple method calls - When you only call the method from one place
- State queries - Actions are for execution, not for returning values
- High-frequency operations - Actions have slight overhead for tracking
For simple notifications, consider Event Channels instead.
Basic usage
Step 1: Create an action class
Create a new class that inherits from ActionSO.
using Tang3cko.ReactiveSO;
using UnityEngine;
[CreateAssetMenu(
fileName = "SpawnEffect",
menuName = "Game/Actions/Spawn Effect"
)]
public class SpawnEffectAction : ActionSO
{
[Header("Settings")]
[SerializeField] private GameObject effectPrefab;
[SerializeField] private Vector3 offset;
[SerializeField] private float duration = 2f;
public override void Execute(
string callerMember = "",
string callerFile = "",
int callerLine = 0)
{
var instance = Object.Instantiate(effectPrefab);
instance.transform.position += offset;
Object.Destroy(instance, duration);
#if UNITY_EDITOR
var callerInfo = new CallerInfo(callerMember, callerFile, callerLine);
NotifyActionExecuted(callerInfo);
LogAction($"Spawned {effectPrefab.name}");
#endif
}
}
Step 2: Create an action asset
Right-click in the Project window and select the menu path you defined.
Create > Game > Actions > Spawn Effect
Configure the action’s parameters in the Inspector.
Step 3: Execute the action
Reference the action asset and call Execute().
using Tang3cko.ReactiveSO;
using UnityEngine;
public class RewardSystem : MonoBehaviour
{
[SerializeField] private ActionSO rewardAction;
public void GiveReward()
{
// Always use null-conditional to avoid NullReferenceException
rewardAction?.Execute();
}
}
Generic actions
Use ActionSO<T> when the action needs to receive a parameter at execution time.
using Tang3cko.ReactiveSO;
using UnityEngine;
[CreateAssetMenu(
fileName = "DamageAction",
menuName = "Game/Actions/Damage"
)]
public class DamageAction : ActionSO<int>
{
[Header("Settings")]
[SerializeField] private GameObject damageVFX;
[SerializeField] private AudioClip damageSound;
public override void Execute(int damage,
string callerMember = "",
string callerFile = "",
int callerLine = 0)
{
Debug.Log($"Dealing {damage} damage");
// Apply damage logic here
#if UNITY_EDITOR
var callerInfo = new CallerInfo(callerMember, callerFile, callerLine);
NotifyActionExecuted(callerInfo);
LogAction($"Damage: {damage}");
#endif
}
}
Execute with a parameter.
[SerializeField] private ActionSO<int> damageAction;
public void Attack()
{
int damage = CalculateDamage();
damageAction?.Execute(damage);
}
Use cases
Quest reward system
[CreateAssetMenu(menuName = "Game/Actions/Give Item")]
public class GiveItemAction : ActionSO
{
[SerializeField] private ItemData item;
[SerializeField] private int quantity = 1;
public override void Execute(
string callerMember = "",
string callerFile = "",
int callerLine = 0)
{
InventoryManager.Instance.AddItem(item, quantity);
#if UNITY_EDITOR
NotifyActionExecuted(new CallerInfo(callerMember, callerFile, callerLine));
#endif
}
}
// Quest definition with configurable rewards
public class Quest : ScriptableObject
{
public string questName;
public ActionSO[] rewardActions;
public void Complete()
{
foreach (var action in rewardActions)
{
action?.Execute();
}
}
}
Event table
[CreateAssetMenu(menuName = "Game/Event Table")]
public class EventTableSO : ScriptableObject
{
[System.Serializable]
public class EventEntry
{
public string eventId;
public ActionSO[] actions;
}
[SerializeField] private EventEntry[] entries;
public void TriggerEvent(string eventId)
{
var entry = System.Array.Find(entries, e => e.eventId == eventId);
if (entry != null)
{
foreach (var action in entry.actions)
{
action?.Execute();
}
}
}
}
Dialogue response
[System.Serializable]
public class DialogueChoice
{
public string text;
public ActionSO[] onChosenActions;
}
public class DialogueUI : MonoBehaviour
{
public void OnChoiceSelected(DialogueChoice choice)
{
foreach (var action in choice.onChosenActions)
{
action?.Execute();
}
}
}
Best practices
Use null-conditional operator
Prevent errors when the action is not assigned.
// Good: Safe if not assigned
rewardAction?.Execute();
// Bad: Throws NullReferenceException if not assigned
rewardAction.Execute();
Keep actions focused
Each action should do one thing well.
// Good: Single responsibility
[CreateAssetMenu(menuName = "Game/Actions/Play Sound")]
public class PlaySoundAction : ActionSO { ... }
[CreateAssetMenu(menuName = "Game/Actions/Spawn VFX")]
public class SpawnVFXAction : ActionSO { ... }
// Bad: Does too many things
[CreateAssetMenu(menuName = "Game/Actions/Play Sound And Spawn VFX And Update Score")]
public class DoEverythingAction : ActionSO { ... }
Use description field
Document what the action does in the Inspector.
public class MyAction : ActionSO
{
// The base class provides a description field
// Set it in the Inspector to document the action
}
Organize action assets
Create a folder structure for your action assets.
Assets/
└── ScriptableObjects/
└── Actions/
├── Rewards/
│ ├── GiveGold.asset
│ └── GiveItem.asset
├── Effects/
│ ├── SpawnExplosion.asset
│ └── PlayFanfare.asset
└── UI/
└── ShowNotification.asset
Debugging
Reactive SO includes debugging tools to help you track action execution.
- Monitor Window - See action executions in real-time during Play Mode
- Console Logging - Enable
showInConsolein the Inspector to log executions - Caller Information - Automatically tracks which code triggered the action
For detailed debugging instructions, see the Debugging Overview.
Common issues
Action not executing
- Check that the action asset is assigned in the Inspector
- Verify the action’s
Executemethod contains your logic - Confirm you are calling
Execute()not just referencing the action - Check the Monitor Window for execution logs
NullReferenceException when executing
- Use the null-conditional operator (
?.Execute()) - Verify the action asset is assigned in the Inspector
Caller info not showing
- Do not pass explicit values to the caller parameters
- Ensure
NotifyActionExecutedis called in theExecutemethod - Check that
showInMonitoris enabled in the action’s Inspector
References
- Action Types Reference
- Event Channels Guide - For notification-style communication
- Debugging Overview