Scriptable Object Architecture (sometimes called SOAP) uses ScriptableObjects as shared data containers instead of singletons. They’re assets that live in your project, persist across scenes, and can be referenced in the inspector—no static classes, no DontDestroyOnLoad juggling.

This means shared state lives in assets, not in code. Scripts reference the same ScriptableObject asset, so they automatically share data without knowing about each other. For example:

  • Player health as a FloatVariable asset—UI reads it, combat writes it, neither knows the other exists
  • Game events as ScriptableObject assets—raise from anywhere, listeners subscribe in inspector
  • Configuration data that designers can tweak without touching code

Runtime Variable Example

[CreateAssetMenu(menuName = "Variables/Float")]
public class FloatVariable : ScriptableObject
{
    public float value;
    
    public void Set(float newValue) => value = newValue;
    public void Add(float amount) => value += amount;
}

Create the asset: Right-click → Create → Variables → Float → name it “PlayerHealth”.

public class PlayerHealth : MonoBehaviour
{
    [SerializeField] private FloatVariable health;
    
    public void TakeDamage(float damage)
    {
        health.Add(-damage);
    }
}
 
public class HealthBarUI : MonoBehaviour
{
    [SerializeField] private FloatVariable health;
    [SerializeField] private Slider slider;
    
    void Update()
    {
        slider.value = health.value;
    }
}

Both scripts reference the same PlayerHealth.asset in their inspector. They don’t know each other exists.

Game Event Example

[CreateAssetMenu(menuName = "Events/Game Event")]
public class GameEvent : ScriptableObject
{
    private readonly List<Action> _listeners = new();
 
    public void Raise()
    {
        for (int i = _listeners.Count - 1; i >= 0; i--)
            _listeners[i]?.Invoke();
    }
 
    public void Subscribe(Action listener) => _listeners.Add(listener);
    public void Unsubscribe(Action listener) => _listeners.Remove(listener);
}
public class PlayerDeath : MonoBehaviour
{
    [SerializeField] private GameEvent onPlayerDied;
    
    void Die() => onPlayerDied.Raise();
}
 
public class GameOverScreen : MonoBehaviour
{
    [SerializeField] private GameEvent onPlayerDied;
    
    void OnEnable() => onPlayerDied.Subscribe(Show);
    void OnDisable() => onPlayerDied.Unsubscribe(Show);
    
    void Show() => gameObject.SetActive(true);
}

The magic: No singletons, no static references, no scene dependency issues. Data and events are assets you wire up in the inspector. Great for game jams and prototypes where you need things talking to each other fast—and clean enough to scale.