Game State Manager Service¶
Overview¶
The Game State Manager Service provides a robust state machine implementation for managing game states such as Main Menu, Gameplay, Paused, etc. It handles state transitions, lifecycle events, and integrates with the LoL Engine's event system to notify the rest of the game about state changes.
Quick Start¶
Enable the service via the Enable Game State Manager toggle in your ServiceConfiguration asset (on by default; off in the minimal profile). Then define states, register them, and transition:
// Game-specific state IDs are cast from GameStateType at 100+ (0-2 are reserved by the engine).
public class GameplayState : BaseGameState
{
public override GameStateType StateType => (GameStateType)100;
public override void Enter() { /* start gameplay */ }
public override void Exit() { /* tear down */ }
}
// Resolve the service, register the state, and transition to it.
var gameState = ServiceLocator.Instance.Get<IGameStateManagerService>();
gameState.RegisterState(new GameplayState());
gameState.ChangeState((GameStateType)100);
Features¶
- State Registration: Register custom game states that define behavior on enter/exit
- State Transitions: Smooth transitions between different game states
- Event Broadcasting: Notifies the game about state changes through both EventService and Unity Events
- MonoBehaviour Integration: Updates active states through Unity's lifecycle methods
- Re-entrant safety: Queues transitions requested during
Enter()/Exit()instead of recursing - Manual registration: You register game-specific states at bootstrap — the engine does not ship MainMenu/Gameplay/Paused
Architecture¶
The Game State Manager Service follows these architectural patterns:
- Service Locator Pattern: Accessed through
ServiceLocator.Instance.Get<IGameStateManagerService>() - State Pattern: Each game state encapsulates its own behavior
- Observer Pattern: Broadcasts state changes through the EventService
- MonoBehaviour: Runs as a component for Unity lifecycle integration (Update/FixedUpdate)
- Event Pooling: Uses object pooling for event instances to reduce garbage collection
Defining Game States (Engine vs Game)¶
The engine GameStateType enum reserves 0–99 for engine use only:
| Value | Name | Purpose |
|---|---|---|
0 |
None |
Uninitialized / no active state |
1 |
Bootstrap |
Engine and core services starting |
2 |
Loading |
Scene or asset transition in progress |
Do not add MainMenu, Gameplay, Paused, etc. to the engine enum. Define game states in your project starting at 100+:
using LoLEngine.Core.GameState.Enums;
public static class MyGameStateType
{
public const GameStateType MainMenu = (GameStateType)100;
public const GameStateType Gameplay = (GameStateType)101;
public const GameStateType Paused = (GameStateType)102;
public const GameStateType GameOver = (GameStateType)103;
public const GameStateType LevelSelect = (GameStateType)104;
}
See Samples~/GameState/SampleGameStateType.cs and the sample state classes (MainMenuState, GameplayState, PausedState) for a complete working pattern.
Bootstrap checklist:
- Define
MyGameStateTypeconstants (100+) - Implement states inheriting
BaseGameState - Call
RegisterState()for each before callingChangeState() - Optionally start in
MainMenufrom your game initializer'sOnServicesReady
Usage¶
Accessing the Service¶
using LoLEngine.Core.GameState.Interfaces;
using LoLEngine.Core.ServiceManagement.Service;
using LoLEngine.Runtime;
// Recommended: Use ServiceAwaiter to ensure service is ready
void Start()
{
ServiceAwaiter.WaitForServices(this, OnServicesReady, OnTimeout);
}
private void OnServicesReady()
{
var gameStateManager = ServiceLocator.Instance.Get<IGameStateManagerService>();
// Register your states before changing state (see below)
}
private void OnTimeout()
{
Debug.LogError("Failed to initialize services");
}
// Alternative: Direct access (only if you're sure services are initialized)
var gameStateManager = ServiceLocator.Instance.Get<IGameStateManagerService>();
Registering Custom States¶
using LoLEngine.Core.GameState.Enums;
using LoLEngine.Core.GameState.States;
public class LevelSelectState : BaseGameState
{
public override GameStateType StateType => MyGameStateType.LevelSelect;
public override void Enter() { /* Show level selection UI */ }
public override void Exit() { /* Hide level selection UI */ }
public override void Update() { /* Handle level selection input */ }
}
// Register during bootstrap (before any ChangeState call)
gameStateManager.RegisterState(new MainMenuState());
gameStateManager.RegisterState(new GameplayState());
gameStateManager.RegisterState(new PausedState());
gameStateManager.RegisterState(new LevelSelectState());
State Transitions¶
// Change by type (must be registered first)
gameStateManager.ChangeState(MyGameStateType.Gameplay);
// Or transition using a state instance
var gameplayState = gameStateManager.GetState<GameplayState>(MyGameStateType.Gameplay);
gameStateManager.ChangeState(gameplayState);
Listening for State Changes¶
Using the EventService (recommended: MonoBehaviour + IEventListener)¶
using LoLEngine.Core.Events.GameEvents;
using LoLEngine.Core.Events.Interfaces;
using LoLEngine.Core.Events.Providers;
using LoLEngine.Core.GameState.Events;
using LoLEngine.Core.GameState.Enums;
using UnityEngine;
public class GameUI : MonoBehaviour, IEventListener<GameStateEvents.AfterStateChangeEvent>
{
void OnEnable() => this.EventStartListening<GameStateEvents.AfterStateChangeEvent>();
void OnDisable() => this.EventStopListening<GameStateEvents.AfterStateChangeEvent>();
public void OnGameEvent(GameStateEvents.AfterStateChangeEvent evt)
{
Debug.Log($"Game state changed from {evt.PreviousStateType} to {evt.NewCurrentStateType}");
if (evt.NewCurrentStateType == MyGameStateType.MainMenu)
ShowMainMenuUI();
else if (evt.NewCurrentStateType == MyGameStateType.Gameplay)
ShowGameplayHUD();
else if (evt.NewCurrentStateType == MyGameStateType.Paused)
ShowPauseMenu();
}
private void ShowMainMenuUI() { /* Implementation */ }
private void ShowGameplayHUD() { /* Implementation */ }
private void ShowPauseMenu() { /* Implementation */ }
}
Plain classes can use IEventManager.AddListener / RemoveListener with the same IEventListener<T> pattern, or GameEvent.Subscribe(this) after services are ready.
Event System Integration¶
The Game State Manager publishes events when changing states using an event pooling system to reduce garbage collection:
// In GameStateManagerService - Current Implementation
public void ChangeState(IGameState newState)
{
// Get pooled BeforeStateChangeEvent
var beforeEvent = GameEvent.GetEvent<GameStateEvents.BeforeStateChangeEvent>();
beforeEvent.Setup(_currentState, newState);
_eventManager.TriggerEvent(beforeEvent);
GameEvent.ReleaseEvent(beforeEvent); // Return to pool
// Perform state transition...
_currentState?.Exit();
IGameState previousState = _currentState;
_currentState = newState;
_currentState.Enter();
// Get pooled AfterStateChangeEvent
var afterEvent = GameEvent.GetEvent<GameStateEvents.AfterStateChangeEvent>();
afterEvent.Setup(previousState, _currentState);
_eventManager.TriggerEvent(afterEvent);
GameEvent.ReleaseEvent(afterEvent); // Return to pool
}
Note: The event pooling system reuses event objects to minimize garbage collection during frequent state changes. Events are automatically returned to the pool after being triggered.
This integration means you can create systems that respond to state changes without direct dependencies on the GameStateManagerService
Using Unity Events¶
// Subscribe to state change events
gameStateManager.OnBeforeStateChangeUnityEvent += OnBeforeStateChange;
gameStateManager.OnAfterStateChangeUnityEvent += OnAfterStateChange;
// Event handlers
private void OnBeforeStateChange(IGameState oldState, IGameState newState)
{
Debug.Log($"About to change from {oldState?.StateType} to {newState.StateType}");
}
private void OnAfterStateChange(IGameState newState)
{
Debug.Log($"Now in state: {newState.StateType}");
}
Checking Current State¶
// Get current state
IGameState currentState = gameStateManager.CurrentState;
GameStateType stateType = gameStateManager.CurrentStateType;
// Check if in specific state
if (gameStateManager.IsInState(MyGameStateType.Paused))
{
// Handle paused state specific logic
}
Common Use Cases¶
Pause System¶
Note: This example uses the legacy
InputManager (Input.GetKeyDown) for brevity. New projects should read the pause key via the Unity Input System — see Input.
using LoLEngine.Core.GameState.Interfaces;
using LoLEngine.Core.GameState.Enums;
using LoLEngine.Core.ServiceManagement.Service;
public class PauseManager : MonoBehaviour
{
private IGameStateManagerService _gameStateManager;
void Start()
{
_gameStateManager = ServiceLocator.Instance.Get<IGameStateManagerService>();
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Escape))
{
TogglePause();
}
}
public void TogglePause()
{
if (_gameStateManager == null) return;
if (_gameStateManager.IsInState(MyGameStateType.Gameplay))
{
_gameStateManager.ChangeState(MyGameStateType.Paused);
Time.timeScale = 0f; // Freeze game time
}
else if (_gameStateManager.IsInState(MyGameStateType.Paused))
{
_gameStateManager.ChangeState(MyGameStateType.Gameplay);
Time.timeScale = 1f; // Resume game time
}
}
}
Game Over Handling¶
using LoLEngine.Core.GameState.Interfaces;
using LoLEngine.Core.GameState.Enums;
using LoLEngine.Core.ServiceManagement.Service;
public class PlayerHealth : MonoBehaviour
{
private IGameStateManagerService _gameStateManager;
void Start()
{
_gameStateManager = ServiceLocator.Instance.Get<IGameStateManagerService>();
}
public void HandlePlayerDeath()
{
if (_gameStateManager != null)
{
_gameStateManager.ChangeState(MyGameStateType.GameOver);
}
}
}
Best Practices¶
- Define States Clearly: Create specific states for distinct game phases
- Keep States Focused: Each state should have a single responsibility
- Clean Up Resources: Properly clean up resources in the
Exit()method - Scene Management: Consider changing scenes within state Enter/Exit methods
- State Data: Pass data between states using a shared context object if needed
- Avoid Cyclic Dependencies: Don't have states directly reference each other
When to Use a State Machine¶
A Game State Management system is crucial for organizing the flow and logic of almost any game beyond the very simplest. Here's why and when it comes into play:
-
Organizing Game Flow & Logic:
- Use Case: Your game will have distinct phases: a main menu, active gameplay, a pause screen, a game over screen, settings menus, loading screens, etc.
- Justification: A state machine clearly defines these phases. Each state encapsulates the specific logic, UI, input handling, and rules relevant only to that phase. This prevents your main game loop from becoming a massive, unmanageable series of if-else statements.
MainMenuState: Handles UI navigation, starting a new game, loading a saved game, opening settings. Input is for UI interaction.GameplayState: Handles player input for character control, enemy AI, physics, scoring, win/loss conditions. Time is running.PausedState: Freezes gameplay (often Time.timeScale = 0), shows a pause menu (Resume, Settings, Quit to Menu). Input is for menu interaction.LoadingState: Shows a loading screen, loads/unloads scenes and assets, prepares the next state.
-
Decoupling Systems:
- Use Case: Different systems in your game need to behave differently based on the current game phase. For example, the audio system plays menu music in
MainMenuStateand gameplay music/SFX inGameplayState. The input system processes UI clicks in menus but character actions in gameplay. - Justification: By listening to
AfterStateChangeEvent, systems can react to state transitions without needing direct references to each other or to theGameStateManager. TheAudioManagerjust needs to know "we entered Gameplay, so switch tracks," not why or how gameplay started. This reduces coupling and makes systems more modular and easier to maintain.
- Use Case: Different systems in your game need to behave differently based on the current game phase. For example, the audio system plays menu music in
-
Managing Resources:
- Use Case: You want to load heavy assets (like a large level or detailed characters) only when entering the gameplay state and unload them when returning to the main menu to save memory.
- Justification: The
Enter()method of aGameplayStatecan trigger asset loading, and itsExit()method can trigger unloading. ALoadingStatecan manage this process, showing progress to the player.
-
Controlling Game Rules and Behavior:
- Use Case: Certain actions are only valid in specific states. Players can't move their character while in the
MainMenuState. Enemies don't attack inPausedState. - Justification: Each state's
Update()method implements the logic pertinent to that state. This naturally enforces game rules by only running the relevant code.
- Use Case: Certain actions are only valid in specific states. Players can't move their character while in the
-
Simplifying Complex Transitions:
- Use Case: Transitioning from gameplay to a game over screen might involve playing an animation, saving high scores, and then showing the game over UI with options.
- Justification: This can be managed by a sequence of states (e.g.,
Gameplay -> PreGameOverAnimation -> GameOver) or by complex logic within theEnter()method of theGameOverState. The state machine provides a clear structure for these transitions.
-
Clear Entry and Exit Points:
- Use Case: When pausing the game, you need to save the current
Time.timeScale, display the pause menu, and potentially disable certain inputs. When unpausing, you need to restoreTime.timeScale, hide the menu, and re-enable inputs. - Justification: The
Enter()andExit()methods of each state provide well-defined points to set up and tear down the conditions for that state, ensuring consistency and preventing logic from being scattered.
- Use Case: When pausing the game, you need to save the current
In essence, a Game State Manager acts as the central coordinator for the overall "mode" or "phase" your game is in, allowing other systems to query the current state or react to changes in a clean, organized manner. It's a foundational piece for building scalable and maintainable game logic.
See Also¶
- Events — react to
AfterStateChangeEventacross systems - Service Locator — how
IGameStateManagerServiceis resolved - Input — reading input per state with the Unity Input System
- Getting Started