A Unity package for managing global observables across scenes and components. Share data and state throughout your Unity project without creating tight coupling between systems.
- Features
- Installation
- How It Works
- Getting Started
- Core Components
- Complete Examples
- Best Practices
- API Reference
- FAQ
- Cross-Scene Data Sharing: Share state between different scenes without complex serialization
- Type-Safe API: Generic-based design ensures compile-time type safety
- Decoupled Architecture: Components communicate without direct references
- Observable Pattern: Subscribe to changes and react automatically
- Zero Boilerplate: Simple, clean API for quick setup
- Memory Efficient: Automatic cleanup and Unity domain reload support
- Well Tested: Comprehensive test suite with 50+ unit tests
- Open Window > Package Manager
- Click the + button and select Add package from git URL
- Enter your repository URL
Add this to your Packages/manifest.json:
{
"dependencies": {
"com.biswajit.globalobservables": "1.0.0"
}
}The Global Observables package follows a two-step pattern:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 1: Define Keys (What data/events exist globally) β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Your Custom Class : GlobalVariablesRegistryKeys β β
β β Your Custom Class : GlobalEventsRegistryKeys β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β Step 2: Use Handlers (Access and manipulate the data) β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β GlobalVariablesHandler.SetValue() β β
β β GlobalEventsHandler.Invoke() β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
Abstract Registry Classes (provided by package):
GlobalVariablesRegistryKeys- base class for variable keysGlobalEventsRegistryKeys- base class for event keys
-
Your Custom Registry Classes (you create):
- Inherit from abstract classes
- Define static key fields for your specific needs
-
Global Handlers (provided by package):
GlobalVariablesHandler- manage variablesGlobalEventsHandler- manage events
IMPORTANT: You must inherit from the abstract base classes to define your keys. These act as a central registry of all global data in your project.
Create two classes in your project (recommended location: Scripts/GlobalObservables/ or similar):
using GlobalObservables.Abstract;
using GlobalObservables.Core.Registries;
namespace YourGame
{
/// <summary>
/// Central registry for all global variable keys in the game
/// </summary>
public class MyGlobalVariables : GlobalVariablesRegistryKeys
{
//===================================================================
// Player Stats
//===================================================================
public static RegistryVariableKey<int> PlayerHealth = new RegistryVariableKey<int>();
public static RegistryVariableKey<int> PlayerMaxHealth = new RegistryVariableKey<int>();
public static RegistryVariableKey<int> PlayerScore = new RegistryVariableKey<int>();
public static RegistryVariableKey<string> PlayerName = new RegistryVariableKey<string>();
//===================================================================
// Game State
//===================================================================
public static RegistryVariableKey<bool> IsGamePaused = new RegistryVariableKey<bool>();
public static RegistryVariableKey<float> GameTimeElapsed = new RegistryVariableKey<float>();
public static RegistryVariableKey<int> CurrentLevel = new RegistryVariableKey<int>();
//===================================================================
// Custom Types
//===================================================================
public static RegistryVariableKey<PlayerData> CurrentPlayerData = new RegistryVariableKey<PlayerData>();
}
// Your custom data class
public class PlayerData
{
public string Name;
public int Level;
public float Experience;
public Vector3 Position;
}
}using GlobalObservables.Abstract;
using GlobalObservables.Core.Registries;
using UnityEngine;
namespace YourGame
{
/// <summary>
/// Central registry for all global event keys in the game
/// </summary>
public class MyGlobalEvents : GlobalEventsRegistryKeys
{
//===================================================================
// Simple Events (no parameters)
//===================================================================
public static RegistryEventKey OnGameStarted = new RegistryEventKey();
public static RegistryEventKey OnGameOver = new RegistryEventKey();
public static RegistryEventKey OnGamePaused = new RegistryEventKey();
public static RegistryEventKey OnGameResumed = new RegistryEventKey();
public static RegistryEventKey OnLevelComplete = new RegistryEventKey();
//===================================================================
// Parameterized Events (with data)
//===================================================================
public static RegistryEventKey<int> OnScoreChanged = new RegistryEventKey<int>();
public static RegistryEventKey<int> OnHealthChanged = new RegistryEventKey<int>();
public static RegistryEventKey<string> OnPlayerDied = new RegistryEventKey<string>();
public static RegistryEventKey<EnemySpawnData> OnEnemySpawned = new RegistryEventKey<EnemySpawnData>();
public static RegistryEventKey<ItemData> OnItemCollected = new RegistryEventKey<ItemData>();
}
// Event data classes
public class EnemySpawnData
{
public Vector3 Position;
public int EnemyType;
public int Health;
}
public class ItemData
{
public string ItemName;
public int Quantity;
}
}Now use the Global Handlers to interact with your defined keys from anywhere in your code.
using GlobalObservables.Core.GlobalHandlers;
using UnityEngine;
public class GameController : MonoBehaviour
{
void Start()
{
// Set initial values using your keys
GlobalVariablesHandler.SetValue(MyGlobalVariables.PlayerHealth, 100);
GlobalVariablesHandler.SetValue(MyGlobalVariables.PlayerScore, 0);
// Invoke an event
GlobalEventsHandler.Invoke(MyGlobalEvents.OnGameStarted);
}
public void PlayerTakeDamage(int damage)
{
// Get current health
int health = GlobalVariablesHandler.GetValue(MyGlobalVariables.PlayerHealth);
// Update health
health -= damage;
GlobalVariablesHandler.SetValue(MyGlobalVariables.PlayerHealth, health);
// Invoke event with parameter
GlobalEventsHandler.Invoke(MyGlobalEvents.OnHealthChanged, health);
if (health <= 0)
{
GlobalEventsHandler.Invoke(MyGlobalEvents.OnGameOver);
}
}
}Global Variables are observable values that automatically notify subscribers when they change.
using GlobalObservables.Core.GlobalHandlers;
public class HealthManager : MonoBehaviour
{
void Start()
{
// Set a value
GlobalVariablesHandler.SetValue(MyGlobalVariables.PlayerHealth, 100);
// Get a value
int currentHealth = GlobalVariablesHandler.GetValue(MyGlobalVariables.PlayerHealth);
Debug.Log($"Current Health: {currentHealth}");
}
public void Heal(int amount)
{
int health = GlobalVariablesHandler.GetValue(MyGlobalVariables.PlayerHealth);
int maxHealth = GlobalVariablesHandler.GetValue(MyGlobalVariables.PlayerMaxHealth);
health = Mathf.Min(health + amount, maxHealth);
GlobalVariablesHandler.SetValue(MyGlobalVariables.PlayerHealth, health);
}
}The power of observables is that other components can react automatically when values change:
using GlobalObservables.Core.GlobalHandlers;
using UnityEngine;
using UnityEngine.UI;
public class HealthUI : MonoBehaviour
{
[SerializeField] private Slider healthSlider;
[SerializeField] private Text healthText;
void OnEnable()
{
// Subscribe to health changes
GlobalVariablesHandler.SubscribeOnChange(
MyGlobalVariables.PlayerHealth,
OnHealthChanged
);
// Get initial value and update UI
int currentHealth = GlobalVariablesHandler.GetValue(MyGlobalVariables.PlayerHealth);
UpdateHealthUI(currentHealth);
}
void OnDisable()
{
// IMPORTANT: Always unsubscribe to prevent memory leaks
GlobalVariablesHandler.UnsubscribeOnChange(
MyGlobalVariables.PlayerHealth,
OnHealthChanged
);
}
// This gets called automatically whenever health changes
void OnHealthChanged(int newHealth)
{
UpdateHealthUI(newHealth);
}
void UpdateHealthUI(int health)
{
healthSlider.value = health / 100f;
healthText.text = $"HP: {health}";
}
}// First, define your custom data class
public class PlayerData
{
public string Name;
public int Level;
public float Experience;
public Vector3 LastCheckpoint;
}
// In your registry keys class
public static RegistryVariableKey<PlayerData> CurrentPlayerData =
new RegistryVariableKey<PlayerData>();
// Usage
public class PlayerManager : MonoBehaviour
{
void Start()
{
// Create and set player data
var playerData = new PlayerData
{
Name = "Hero",
Level = 5,
Experience = 1250.5f,
LastCheckpoint = new Vector3(10, 0, 5)
};
GlobalVariablesHandler.SetValue(MyGlobalVariables.CurrentPlayerData, playerData);
}
void LevelUp()
{
// Get current data
var data = GlobalVariablesHandler.GetValue(MyGlobalVariables.CurrentPlayerData);
// Modify it
data.Level++;
data.Experience = 0;
// Update the global variable (this triggers OnChange for subscribers)
GlobalVariablesHandler.SetValue(MyGlobalVariables.CurrentPlayerData, data);
}
}
// Another component reacting to player data changes
public class PlayerInfoUI : MonoBehaviour
{
void OnEnable()
{
GlobalVariablesHandler.SubscribeOnChange(
MyGlobalVariables.CurrentPlayerData,
OnPlayerDataChanged
);
}
void OnDisable()
{
GlobalVariablesHandler.UnsubscribeOnChange(
MyGlobalVariables.CurrentPlayerData,
OnPlayerDataChanged
);
}
void OnPlayerDataChanged(PlayerData data)
{
Debug.Log($"Player: {data.Name}, Level: {data.Level}, XP: {data.Experience}");
// Update your UI
}
}Global Events enable decoupled communication between components without direct references.
using GlobalObservables.Core.GlobalHandlers;
using UnityEngine;
// Component that triggers the event
public class GameController : MonoBehaviour
{
public void EndGame()
{
// Invoke a simple event
GlobalEventsHandler.Invoke(MyGlobalEvents.OnGameOver);
}
public void PauseGame()
{
GlobalEventsHandler.Invoke(MyGlobalEvents.OnGamePaused);
}
}
// Component that listens to the event
public class GameOverUI : MonoBehaviour
{
[SerializeField] private GameObject gameOverPanel;
void OnEnable()
{
// Subscribe to the event
GlobalEventsHandler.Subscribe(MyGlobalEvents.OnGameOver, OnGameOver);
}
void OnDisable()
{
// Unsubscribe
GlobalEventsHandler.Unsubscribe(MyGlobalEvents.OnGameOver, OnGameOver);
}
void OnGameOver()
{
Debug.Log("Game Over!");
gameOverPanel.SetActive(true);
// Show game over UI, stop gameplay, etc.
}
}
// Another component also listening (multiple subscribers allowed!)
public class AudioManager : MonoBehaviour
{
void OnEnable()
{
GlobalEventsHandler.Subscribe(MyGlobalEvents.OnGameOver, OnGameOver);
GlobalEventsHandler.Subscribe(MyGlobalEvents.OnGamePaused, OnGamePaused);
}
void OnDisable()
{
GlobalEventsHandler.Unsubscribe(MyGlobalEvents.OnGameOver, OnGameOver);
GlobalEventsHandler.Unsubscribe(MyGlobalEvents.OnGamePaused, OnGamePaused);
}
void OnGameOver()
{
// Play game over music
}
void OnGamePaused()
{
// Pause background music
}
}using GlobalObservables.Core.GlobalHandlers;
using UnityEngine;
// Component that triggers events with data
public class ScoreManager : MonoBehaviour
{
private int currentScore = 0;
public void AddScore(int points)
{
currentScore += points;
// Invoke event with the new score
GlobalEventsHandler.Invoke(MyGlobalEvents.OnScoreChanged, currentScore);
}
}
// Component listening to score changes
public class ScoreUI : MonoBehaviour
{
[SerializeField] private Text scoreText;
void OnEnable()
{
// Subscribe to parameterized event
GlobalEventsHandler.Subscribe(MyGlobalEvents.OnScoreChanged, OnScoreChanged);
}
void OnDisable()
{
GlobalEventsHandler.Unsubscribe(MyGlobalEvents.OnScoreChanged, OnScoreChanged);
}
// The callback receives the parameter
void OnScoreChanged(int newScore)
{
scoreText.text = $"Score: {newScore}";
}
}// Define your event data in the registry keys file
public class EnemySpawnData
{
public Vector3 Position;
public int EnemyType;
public int Health;
}
// In your registry keys
public static RegistryEventKey<EnemySpawnData> OnEnemySpawned =
new RegistryEventKey<EnemySpawnData>();
// Spawner - invokes the event
public class EnemySpawner : MonoBehaviour
{
public void SpawnEnemy(Vector3 position, int enemyType)
{
// Create the enemy spawn data
var spawnData = new EnemySpawnData
{
Position = position,
EnemyType = enemyType,
Health = 100
};
// Actually spawn the enemy (your code here)
GameObject enemy = Instantiate(enemyPrefab, position, Quaternion.identity);
// Notify all listeners
GlobalEventsHandler.Invoke(MyGlobalEvents.OnEnemySpawned, spawnData);
}
}
// Minimap - listens and shows enemy on minimap
public class MinimapController : MonoBehaviour
{
void OnEnable()
{
GlobalEventsHandler.Subscribe(MyGlobalEvents.OnEnemySpawned, OnEnemySpawned);
}
void OnDisable()
{
GlobalEventsHandler.Unsubscribe(MyGlobalEvents.OnEnemySpawned, OnEnemySpawned);
}
void OnEnemySpawned(EnemySpawnData data)
{
Debug.Log($"Enemy spawned at {data.Position}");
// Add marker to minimap
}
}
// Enemy counter - also listens
public class EnemyCounter : MonoBehaviour
{
private int activeEnemies = 0;
void OnEnable()
{
GlobalEventsHandler.Subscribe(MyGlobalEvents.OnEnemySpawned, OnEnemySpawned);
}
void OnDisable()
{
GlobalEventsHandler.Unsubscribe(MyGlobalEvents.OnEnemySpawned, OnEnemySpawned);
}
void OnEnemySpawned(EnemySpawnData data)
{
activeEnemies++;
Debug.Log($"Active enemies: {activeEnemies}");
}
}// 1. Define your keys
public class GameKeys : GlobalVariablesRegistryKeys
{
public static RegistryVariableKey<int> PlayerHealth = new RegistryVariableKey<int>();
public static RegistryVariableKey<int> PlayerMaxHealth = new RegistryVariableKey<int>();
}
public class GameEvents : GlobalEventsRegistryKeys
{
public static RegistryEventKey OnPlayerDied = new RegistryEventKey();
public static RegistryEventKey<int> OnPlayerDamaged = new RegistryEventKey<int>();
}
// 2. Health Manager - manages health logic
public class HealthManager : MonoBehaviour
{
void Start()
{
// Initialize health
GlobalVariablesHandler.SetValue(GameKeys.PlayerMaxHealth, 100);
GlobalVariablesHandler.SetValue(GameKeys.PlayerHealth, 100);
}
public void TakeDamage(int damage)
{
int health = GlobalVariablesHandler.GetValue(GameKeys.PlayerHealth);
health = Mathf.Max(0, health - damage);
GlobalVariablesHandler.SetValue(GameKeys.PlayerHealth, health);
// Invoke damage event
GlobalEventsHandler.Invoke(GameEvents.OnPlayerDamaged, damage);
if (health == 0)
{
GlobalEventsHandler.Invoke(GameEvents.OnPlayerDied);
}
}
public void Heal(int amount)
{
int health = GlobalVariablesHandler.GetValue(GameKeys.PlayerHealth);
int maxHealth = GlobalVariablesHandler.GetValue(GameKeys.PlayerMaxHealth);
health = Mathf.Min(health + amount, maxHealth);
GlobalVariablesHandler.SetValue(GameKeys.PlayerHealth, health);
}
}
// 3. Health UI - displays health
public class HealthUI : MonoBehaviour
{
[SerializeField] private Slider healthSlider;
[SerializeField] private Text healthText;
void OnEnable()
{
GlobalVariablesHandler.SubscribeOnChange(GameKeys.PlayerHealth, UpdateHealthDisplay);
UpdateHealthDisplay(GlobalVariablesHandler.GetValue(GameKeys.PlayerHealth));
}
void OnDisable()
{
GlobalVariablesHandler.UnsubscribeOnChange(GameKeys.PlayerHealth, UpdateHealthDisplay);
}
void UpdateHealthDisplay(int health)
{
int maxHealth = GlobalVariablesHandler.GetValue(GameKeys.PlayerMaxHealth);
healthSlider.value = (float)health / maxHealth;
healthText.text = $"{health}/{maxHealth}";
}
}
// 4. Damage Indicator - shows damage numbers
public class DamageIndicator : MonoBehaviour
{
void OnEnable()
{
GlobalEventsHandler.Subscribe(GameEvents.OnPlayerDamaged, OnPlayerDamaged);
}
void OnDisable()
{
GlobalEventsHandler.Unsubscribe(GameEvents.OnPlayerDamaged, OnPlayerDamaged);
}
void OnPlayerDamaged(int damage)
{
// Show floating damage text
Debug.Log($"-{damage} HP");
}
}
// 5. Game Over Manager
public class GameOverManager : MonoBehaviour
{
[SerializeField] private GameObject gameOverPanel;
void OnEnable()
{
GlobalEventsHandler.Subscribe(GameEvents.OnPlayerDied, OnPlayerDied);
}
void OnDisable()
{
GlobalEventsHandler.Unsubscribe(GameEvents.OnPlayerDied, OnPlayerDied);
}
void OnPlayerDied()
{
gameOverPanel.SetActive(true);
Time.timeScale = 0f;
}
}// Scene A: Main Menu
public class MainMenuController : MonoBehaviour
{
[SerializeField] private InputField playerNameInput;
public void OnStartGameClicked()
{
// Save data to global variables before scene change
GlobalVariablesHandler.SetValue(GameKeys.PlayerName, playerNameInput.text);
GlobalVariablesHandler.SetValue(GameKeys.PlayerLevel, 1);
GlobalVariablesHandler.SetValue(GameKeys.PlayerScore, 0);
// Load next scene
SceneManager.LoadScene("GameScene");
}
}
// Scene B: Game Scene
public class GameSceneInitializer : MonoBehaviour
{
[SerializeField] private Text welcomeText;
void Start()
{
// Retrieve data from previous scene
string playerName = GlobalVariablesHandler.GetValue(GameKeys.PlayerName);
int playerLevel = GlobalVariablesHandler.GetValue(GameKeys.PlayerLevel);
welcomeText.text = $"Welcome back, {playerName}! (Level {playerLevel})";
// Continue using the data...
}
}CRITICAL: Always unsubscribe in OnDisable() or OnDestroy() to prevent memory leaks.
// β
GOOD
void OnEnable()
{
GlobalEventsHandler.Subscribe(MyGlobalEvents.OnGameOver, OnGameOver);
}
void OnDisable()
{
GlobalEventsHandler.Unsubscribe(MyGlobalEvents.OnGameOver, OnGameOver);
}
// β BAD - Memory leak! Object won't be garbage collected
void OnEnable()
{
GlobalEventsHandler.Subscribe(MyGlobalEvents.OnGameOver, OnGameOver);
}
// Missing OnDisable!// β
GOOD - Clear and organized
public static RegistryVariableKey<int> Player_Health = new RegistryVariableKey<int>();
public static RegistryEventKey MainMenu_StartButton_Clicked = new RegistryEventKey();
// β AVOID - Unclear naming
public static RegistryVariableKey<int> h = new RegistryVariableKey<int>();
public static RegistryEventKey event1 = new RegistryEventKey();public class MyGlobalVariables : GlobalVariablesRegistryKeys
{
//===================================================================
// Player System
//===================================================================
public static RegistryVariableKey<int> PlayerHealth = new RegistryVariableKey<int>();
public static RegistryVariableKey<string> PlayerName = new RegistryVariableKey<string>();
//===================================================================
// UI System
//===================================================================
public static RegistryVariableKey<bool> IsMenuOpen = new RegistryVariableKey<bool>();
public static RegistryVariableKey<bool> IsInventoryOpen = new RegistryVariableKey<bool>();
//===================================================================
// Audio System
//===================================================================
public static RegistryVariableKey<float> MasterVolume = new RegistryVariableKey<float>();
public static RegistryVariableKey<float> MusicVolume = new RegistryVariableKey<float>();
}public class GameInitializer : MonoBehaviour
{
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
static void Initialize()
{
// Set all default values at game start
GlobalVariablesHandler.SetValue(MyGlobalVariables.PlayerHealth, 100);
GlobalVariablesHandler.SetValue(MyGlobalVariables.PlayerScore, 0);
GlobalVariablesHandler.SetValue(MyGlobalVariables.IsGamePaused, false);
}
}- Variables: For state that needs to be read frequently (health, score, settings)
- Events: For one-time notifications (button clicked, enemy died, level completed)
// Subscribe to value changes
void SubscribeOnChange<T>(RegistryVariableKey<T> key, Action<T> callback)
// Unsubscribe from value changes
void UnsubscribeOnChange<T>(RegistryVariableKey<T> key, Action<T> callback)
// Get current value
T GetValue<T>(RegistryVariableKey<T> key)
// Set new value (triggers OnChange for subscribers)
void SetValue<T>(RegistryVariableKey<T> key, T value)// Simple Events (no parameters)
void Subscribe(RegistryEventKey key, Action callback)
void Unsubscribe(RegistryEventKey key, Action callback)
void Invoke(RegistryEventKey key)
// Parameterized Events
void Subscribe<T>(RegistryEventKey<T> key, Action<T> callback)
void Unsubscribe<T>(RegistryEventKey<T> key, Action<T> callback)
void Invoke<T>(RegistryEventKey<T> key, T value)Q: Do I need to instantiate the registry key classes?
A: No! The registry key classes (MyGlobalVariables, MyGlobalEvents, etc.) should only contain static fields. Never create instances of them.
Q: Why inherit from abstract classes?
A: Inheriting from GlobalVariablesRegistryKeys and GlobalEventsRegistryKeys provides structure and follows the intended pattern. It also allows the package to potentially add shared functionality in the future.
Q: Can I use this across different scenes?
A: Yes! That's one of the main benefits. All global observables persist across scene loads automatically.
Q: What happens if I forget to unsubscribe?
A: Memory leak! The subscribed object won't be garbage collected even when destroyed. Always unsubscribe in OnDisable() or OnDestroy().
Q: Can multiple objects subscribe to the same observable?
A: Yes! Any number of objects can subscribe. All subscribers will be notified of changes.
Q: Are these thread-safe?
A: The package is designed for Unity's main thread. Don't use from background threads.
Q: How is this different from ScriptableObjects?
A: ScriptableObjects are asset-based and require serialization. Global Observables are runtime-only, require no assets, and provide automatic subscription/notification.
Q: Can I reset all values?
A: The registry automatically clears when entering play mode (if Domain Reload is enabled). For runtime resets, you'll need to manually reset values you care about.
Q: What's the performance impact?
A: Minimal. The registry uses GUID-based O(1) lookups. Subscriptions use C# events/delegates which are highly optimized.
This package is licensed under a custom license. See LICENSE.txt for details.
Contributions are welcome! Please feel free to submit issues and pull requests.
- Email: bsahoo1995@gmail.com
- GitHub: IamBiswajitSahoo
Version: 1.0.0
Unity: 2022.3+
Package: com.biswajit.globalobservables