SoapKit Events System
The Events System is one of SoapKitβs core pillars, offering a professional, type-safe event architecture that enables decoupled communication between systems while maintaining high performance and advanced debugging capabilities.
Overviewβ
SoapKit events are ScriptableObject-based channels that allow different parts of your game to communicate without direct references. Instead of components calling each other, they raise events that other systems can listen to.
Key Benefitsβ
- π§ Decoupled Architecture β No direct dependencies between systems
- π Type Safety β Full compile-time validation and IntelliSense
- β‘ Performance β Lightweight, optimized for frequent usage
- π§ Debuggable β Built-in history tracking and monitoring tools
- π§ͺ Testable β Easily raise events in unit tests and editor scripts
Basic Usageβ
Creating Eventsβ
Create events using the context menu:
Right-click in Project β Create β SoapKit β Events β [Type] Event
Available Event Types:
UnitGameEventβ No parametersBoolGameEventIntGameEventFloatGameEventStringGameEventVector2GameEventVector3GameEventVector2IntGameEventColorGameEventGameObjectGameEventTransformGameEvent
Raising Eventsβ
From MonoBehaviours:
public class PlayerController : MonoBehaviour
{
[SerializeField] private Vector3GameEvent onPlayerMoved;
[SerializeField] private IntGameEvent onScoreChanged;
[SerializeField] private UnitGameEvent onPlayerDied;
void Update()
{
if (moved)
onPlayerMoved.Raise(transform.position);
}
public void AddScore(int points)
{
score += points;
onScoreChanged.Raise(score);
}
public void Die()
{
onPlayerDied.Raise(); // No parameters
}
}
From Scripts or Tests:
var eventAsset = ScriptableObject.CreateInstance<IntGameEvent>();
eventAsset.Raise(25);
Resources.Load<IntGameEvent>("Events/OnHealthChanged").Raise(50);
Listening to Eventsβ
Approach 1: AddListener / RemoveListener
public class HealthUI : MonoBehaviour
{
[SerializeField] private IntGameEvent onHealthChanged;
[SerializeField] private UnitGameEvent onPlayerDied;
void OnEnable()
{
onHealthChanged.AddListener(UpdateHealthBar);
onPlayerDied.AddListener(ShowGameOver);
}
void OnDisable()
{
onHealthChanged?.RemoveListener(UpdateHealthBar);
onPlayerDied?.RemoveListener(ShowGameOver);
}
private void UpdateHealthBar(int value) => healthSlider.value = value;
private void ShowGameOver() => gameOverPanel.SetActive(true);
}
Approach 2: Listener Components
public class AudioEventListener : MonoBehaviour
{
[SerializeField] private StringGameEvent onSoundRequested;
[SerializeField] private AudioSource audioSource;
[SerializeField] private AudioClip[] soundClips;
void OnEnable() => onSoundRequested.AddListener(PlaySound);
void OnDisable() => onSoundRequested?.RemoveListener(PlaySound);
private void PlaySound(string clipName)
{
var clip = Array.Find(soundClips, c => c.name == clipName);
if (clip != null)
audioSource.PlayOneShot(clip);
}
}
Advanced Patternsβ
Event Chainingβ
public class GameManager : MonoBehaviour
{
[SerializeField] private UnitGameEvent onEnemyKilled;
[SerializeField] private IntGameEvent onScoreChanged;
[SerializeField] private UnitGameEvent onLevelComplete;
private int enemiesKilled = 0;
void OnEnable() => onEnemyKilled.AddListener(HandleKill);
void OnDisable() => onEnemyKilled?.RemoveListener(HandleKill);
private void HandleKill()
{
enemiesKilled++;
onScoreChanged.Raise(enemiesKilled * 100);
if (enemiesKilled >= 10)
onLevelComplete.Raise();
}
}
Conditional Listenersβ
public class PowerUpSystem : MonoBehaviour
{
[SerializeField] private IntGameEvent onHealthChanged;
[SerializeField] private BoolVariable isPoweredUp;
[SerializeField] private FloatVariable multiplier;
void OnEnable() => onHealthChanged.AddListener(OnHealthChanged);
private void OnHealthChanged(int health)
{
if (!isPoweredUp.Value) return;
int bonus = Mathf.RoundToInt(health * multiplier.Value);
Debug.Log($"Power-up bonus: {bonus}");
}
}
Event Aggregationβ
public class ComboSystem : MonoBehaviour
{
[SerializeField] private UnitGameEvent onJump;
[SerializeField] private UnitGameEvent onAttack;
[SerializeField] private UnitGameEvent onComboAchieved;
private bool jumped, attacked;
private float lastActionTime;
private float comboWindow = 2f;
void OnEnable()
{
onJump.AddListener(OnJump);
onAttack.AddListener(OnAttack);
}
void OnJump() { jumped = true; lastActionTime = Time.time; CheckCombo(); }
void OnAttack() { attacked = true; lastActionTime = Time.time; CheckCombo(); }
void CheckCombo()
{
if (jumped && attacked && Time.time - lastActionTime < comboWindow)
{
onComboAchieved.Raise();
jumped = attacked = false;
}
}
}
Debugging & Testingβ
Event History (Editor)β
#if UNITY_EDITOR
var history = onScoreChanged.GetEventHistory(10);
foreach (var entry in history)
Debug.Log($"[{entry.timestamp}] Value: {entry.value}");
#endif
Debug Windowβ
Access via:
Tools β SoapKit β Debug Window
Features:
- π Real-time event monitoring
- π Listener count and event stats
- π§ͺ Manual test triggering
- π History of recent raises
Inspector Infoβ
Each event shows:
- Listener Count
- Raise Count
- Last Raised
- Manual Raise Button
Performanceβ
β Do Thisβ
private int cachedValue;
private void OnEnable() => onHealthChanged.AddListener(OnChanged);
private void OnDisable() => onHealthChanged?.RemoveListener(OnChanged);
private void OnChanged(int value)
{
if (value == cachedValue) return;
cachedValue = value;
UpdateUI(value);
}
β Avoid Thisβ
// Anti-pattern: expensive lookup & missing unsubscribe
void Start() => onHealthChanged.AddListener(OnChanged);
private void OnChanged(int value)
{
FindObjectOfType<HealthBar>().Set(value); // Avoid this
}
Benchmarksβ
| Method | Time per call |
|---|---|
SendMessage (Unity) | ~2000 ns |
UnityEvent | ~800 ns |
| SoapKit Event | ~200 ns β‘ |
π§ͺ Unit Testingβ
[Test]
public void ShouldRaiseDeathEventWhenHealthZero()
{
var healthEvent = ScriptableObject.CreateInstance<IntGameEvent>();
var deathEvent = ScriptableObject.CreateInstance<UnitGameEvent>();
var system = new GameObject().AddComponent<HealthSystem>();
bool died = false;
deathEvent.AddListener(() => died = true);
healthEvent.Raise(0);
Assert.IsTrue(died);
}
π§© Common Event Use Casesβ
State Transitionsβ
public class GameStateManager : MonoBehaviour
{
[SerializeField] private GameStateGameEvent onStateChanged;
private GameState current = GameState.Menu;
public void ChangeState(GameState next)
{
if (next == current) return;
var previous = current;
current = next;
onStateChanged.Raise(new GameStateData { previousState = previous, newState = next });
}
}
Resource Trackingβ
public class ResourceManager : MonoBehaviour
{
[SerializeField] private IntGameEvent onCoinsChanged;
[SerializeField] private BoolGameEvent onCanAfford;
public void Spend(int amount)
{
if (coins >= amount)
{
coins -= amount;
onCoinsChanged.Raise(coins);
onCanAfford.Raise(coins >= itemCost);
}
}
}
Animation Integrationβ
public class AnimationEventBridge : MonoBehaviour
{
[SerializeField] private UnitGameEvent onStart;
[SerializeField] private UnitGameEvent onEnd;
[SerializeField] private StringGameEvent onTrigger;
public void OnAnimationStart() => onStart.Raise();
public void OnAnimationEnd() => onEnd.Raise();
public void OnAnimationTrigger(string name) => onTrigger.Raise(name);
}
Custom Event Typesβ
Create your own specialized event types for complex data structures and custom validation:
Defining Custom Eventsβ
Example: Player Action Event
// Define custom data structure
[System.Serializable]
public struct PlayerActionData
{
public string actionName;
public Vector3 position;
public float intensity;
public GameObject target;
}
// Create custom event type
[CreateAssetMenu(menuName = "SoapKit/Events/PlayerAction Event")]
public class PlayerActionEvent : GameEvent<PlayerActionData>
{
// Custom validation for player actions
protected override bool ValidateEventData(PlayerActionData data)
{
if (string.IsNullOrEmpty(data.actionName))
{
Debug.LogWarning("Player action name cannot be empty");
return false;
}
if (data.intensity < 0f || data.intensity > 1f)
{
Debug.LogWarning("Player action intensity must be between 0 and 1");
return false;
}
return true;
}
// Custom logging for debugging
protected override void OnEventRaised(PlayerActionData data)
{
base.OnEventRaised(data);
#if UNITY_EDITOR
Debug.Log($"Player Action: {data.actionName} at {data.position} with intensity {data.intensity:F2}");
#endif
}
}
Event Configurationβ
Advanced Event Setup:
Name: OnHealthChanged
Type: IntGameEvent
Description: "Raised when player health changes"
Debug Mode: Enabled // Show in debug window
History Size: 100 // Remember last 100 raises
Performance Tracking: Enabled // Track performance metrics
Event Categories:
- Gameplay: Core game mechanics
- UI: User interface interactions
- Audio: Sound and music triggers
- System: Low-level system events
- Debug: Development and testing events
Event System Templatesβ
Complete Health System Events:
// Health system event collection
IntGameEvent: OnHealthChanged // Health value updates
IntGameEvent: OnDamageTaken // Damage amount
UnitGameEvent: OnPlayerDied // Death notification
UnitGameEvent: OnPlayerHealed // Healing notification
BoolGameEvent: OnCriticalHealth // Low health warning
Inventory System Events:
StringGameEvent: OnItemAdded // Item name added
StringGameEvent: OnItemRemoved // Item name removed
BoolGameEvent: OnInventoryFull // Capacity reached
IntGameEvent: OnCountChanged // Item count updates
GameObjectGameEvent: OnItemUsed // Item object used
Next Stepsβ
- Variables System β Reactive data channels
- Quick Guide β Common usage patterns
- Debug Tools β Live event analysis
- Custom Events β Define new event types
The Events System is the backbone of SoapKit's architecture. Mastering events gives you the power to build flexible, testable, professional Unity systems β no tight coupling required.