Skip to main content

Design Patterns

Common design patterns and best practices when using SoapKit in your Unity projects.

Observer Pattern with Events

SoapKit's event system implements the Observer pattern, allowing for loose coupling between systems.

Basic Observer Setup

public class HealthSystem : MonoBehaviour
{
[SerializeField] private IntVariable playerHealth;
[SerializeField] private GameEvent onPlayerDeath;

private void Update()
{
if (playerHealth.Value <= 0)
{
onPlayerDeath.Raise();
}
}
}

public class UIHealthBar : MonoBehaviour
{
[SerializeField] private IntVariable playerHealth;
[SerializeField] private Slider healthSlider;

private void OnEnable()
{
playerHealth.OnValueChanged += UpdateHealthBar;
}

private void OnDisable()
{
playerHealth.OnValueChanged -= UpdateHealthBar;
}

private void UpdateHealthBar(int newHealth)
{
healthSlider.value = newHealth;
}
}

State Management Pattern

Use SoapKit variables to manage game state centrally.

Game State Manager

public class GameStateManager : MonoBehaviour
{
[Header("Game State")]
[SerializeField] private BoolVariable isGamePaused;
[SerializeField] private BoolVariable isGameOver;
[SerializeField] private IntVariable currentLevel;

[Header("Events")]
[SerializeField] private GameEvent onGameStart;
[SerializeField] private GameEvent onGameEnd;
[SerializeField] private BoolGameEvent onPauseToggle;

private void Start()
{
InitializeGameState();
}

private void InitializeGameState()
{
isGamePaused.SetValue(false);
isGameOver.SetValue(false);
currentLevel.SetValue(1);

onGameStart.Raise();
}

public void PauseGame()
{
bool newPauseState = !isGamePaused.Value;
isGamePaused.SetValue(newPauseState);
onPauseToggle.Raise(newPauseState);
}
}

Command Pattern with Events

Implement command pattern using events for action handling.

Command System

[CreateAssetMenu(menuName = "SoapKit/Events/Command Event")]
public class CommandEvent : GameEvent<ICommand>
{
private Stack<ICommand> commandHistory = new Stack<ICommand>();

public void ExecuteCommand(ICommand command)
{
command.Execute();
commandHistory.Push(command);
Raise(command);
}

public void UndoLastCommand()
{
if (commandHistory.Count > 0)
{
var lastCommand = commandHistory.Pop();
lastCommand.Undo();
}
}
}

public interface ICommand
{
void Execute();
void Undo();
}

Factory Pattern for Asset Creation

Create factory methods for consistent asset creation.

Variable Factory

public static class VariableFactory
{
public static T CreateVariable<T>(string name, object initialValue) where T : ScriptableObject
{
var asset = ScriptableObject.CreateInstance<T>();
asset.name = name;

// Set initial value using reflection or specific methods
if (asset is BaseVariable<object> variable)
{
variable.SetValue(initialValue);
}

return asset;
}
}

Mediator Pattern

Use events as mediators between systems.

System Communication

public class InventorySystem : MonoBehaviour
{
[SerializeField] private IntGameEvent onItemCollected;
[SerializeField] private IntGameEvent onItemUsed;

private void OnEnable()
{
onItemCollected.AddListener(HandleItemCollected);
onItemUsed.AddListener(HandleItemUsed);
}

private void OnDisable()
{
onItemCollected.RemoveListener(HandleItemCollected);
onItemUsed.RemoveListener(HandleItemUsed);
}

private void HandleItemCollected(int itemId)
{
// Add item to inventory
// Update UI
// Save game state
}

private void HandleItemUsed(int itemId)
{
// Remove item from inventory
// Apply item effects
// Update UI
}
}

Data Binding Pattern

Implement two-way data binding with variables.

UI Data Binding

public class DataBinder : MonoBehaviour
{
[SerializeField] private StringVariable playerName;
[SerializeField] private InputField nameInput;

private void Start()
{
// Bind variable to UI
nameInput.text = playerName.Value;
nameInput.onValueChanged.AddListener(OnNameChanged);

// Bind UI to variable
playerName.OnValueChanged += OnVariableChanged;
}

private void OnNameChanged(string newName)
{
playerName.SetValue(newName);
}

private void OnVariableChanged(string newName)
{
if (nameInput.text != newName)
{
nameInput.text = newName;
}
}
}

Repository Pattern

Use ScriptableObjects as data repositories.

Game Data Repository

[CreateAssetMenu(menuName = "Data/Game Repository")]
public class GameDataRepository : ScriptableObject
{
[Header("Player Data")]
public IntVariable playerHealth;
public IntVariable playerMana;
public StringVariable playerName;

[Header("Game Settings")]
public FloatVariable masterVolume;
public BoolVariable enableVSync;

public void ResetToDefaults()
{
playerHealth.SetValue(100);
playerMana.SetValue(50);
playerName.SetValue("Player");
masterVolume.SetValue(1.0f);
enableVSync.SetValue(true);
}
}

Best Practices

  1. Single Responsibility: Each ScriptableObject should have one clear purpose
  2. Loose Coupling: Use events to communicate between unrelated systems
  3. Consistent Naming: Follow naming conventions for assets and references
  4. Event Lifecycle: Always pair AddListener with RemoveListener
  5. Validation: Use constraints and validation in custom variables
  6. Documentation: Add tooltips and help text to inspector fields

Anti-Patterns to Avoid

  1. Direct References: Avoid direct MonoBehaviour references between systems
  2. Event Spam: Don't raise events in Update() loops without throttling
  3. Memory Leaks: Always unsubscribe from events in OnDisable/OnDestroy
  4. Global State Abuse: Don't make everything a ScriptableObject variable
  5. Complex Dependencies: Keep dependency chains simple and traceable