Scene Management
The Popcorning Guinea Pig engine uses a sophisticated scene management system that handles the lifecycle, transitions, and organization of different game states and screens. Understanding this system is crucial for building structured, maintainable games.
Overview
The scene management system is built around three core components:
- SceneManager: Orchestrates scene transitions, queuing, and lifecycle management
- Scene: Base class providing the fundamental scene structure and lifecycle methods
- Scene3D: Extended scene class with 3D rendering capabilities
Scene Architecture
Scene Lifecycle
Every scene follows a predictable lifecycle managed by the SceneManager:
public abstract class Scene
{
// Scene state tracking
public bool IsLoaded { get; set; } = false;
// Lifecycle methods
public virtual void Load() { } // Called when scene becomes active
public virtual void Update(double deltaTime) { } // Called every frame
public virtual void Render(int width, int height) { } // Called for drawing
public virtual void Unload() { } // Called when scene is deactivated
}
Scene Types
Base Scene (2D)
- Handles 2D UI elements and rendering
- Manages user input through InputContext
- Supports PGPSCN scene file loading
- Suitable for menus, 2D games, UI screens
Scene3D (3D)
- Extends base Scene with 3D capabilities
- Manages 3D cameras and models
- Handles 3D UI elements and interactions
- Suitable for 3D games and environments
SceneManager
The SceneManager is responsible for coordinating scene transitions and managing the scene lifecycle.
Core Features
public class SceneManager
{
// Current active scene
public Scene GetCurrentScene()
// Scene transition status
public bool IsTransitioning { get; }
// Queue management
public int GetQueueCount()
public void ClearSceneQueue()
// Scene change methods
public void ChangeScene(Scene newScene)
public void ChangeSceneImmediate(Scene newScene)
public void QueueScene(Scene scene, float transitionSeconds = 0f)
}
Scene Transitions
The SceneManager supports both immediate and timed scene transitions:
Immediate Transition
// Switch scenes immediately
sceneManager.ChangeScene(new GameplayScene());
// Switch scenes immediately and clear queue
sceneManager.ChangeSceneImmediate(new MainMenuScene());
Timed Transition
// Transition with fade effect (2 seconds)
sceneManager.QueueScene(new GameplayScene(), 2.0f);
Scene Queuing System
The SceneManager includes a sophisticated queuing system that prevents duplicate scene requests and manages complex scene sequences:
// Queue multiple scenes in sequence
sceneManager.QueueScene(new LoadingScene(), 0.5f);
sceneManager.QueueScene(new GameplayScene(), 1.0f);
sceneManager.QueueScene(new ResultsScene(), 0.5f);
// Check queue status
int queueSize = sceneManager.GetQueueCount();
bool isTransitioning = sceneManager.IsTransitioning;
// Clear queue if needed
sceneManager.ClearSceneQueue();
Duplicate Prevention
The SceneManager automatically prevents duplicate scene transitions:
// These calls will be ignored if already transitioning to GameplayScene
sceneManager.QueueScene(new GameplayScene());
sceneManager.QueueScene(new GameplayScene()); // Ignored
sceneManager.QueueScene(new GameplayScene()); // Ignored
Creating Scenes
Basic Scene Implementation
public class MainMenuScene : Scene
{
private List<Button> buttons = new();
private ISoundPlayer soundPlayer;
public MainMenuScene(ISoundPlayer soundPlayer)
{
this.soundPlayer = soundPlayer;
}
protected override void OnLoad()
{
// Create UI elements
var playButton = GetElement<Button>("playButton");
playButton.SetOnClick(OnPlayClicked);
var settingsButton = GetElement<Button>("settingsButton");
settingsButton.SetOnClick(OnSettingsClicked);
// Start background music
soundPlayer.PlayMusic("assets/music/menu-theme.ogg");
}
protected override void OnUnload()
{
// Clean up resources
soundPlayer.StopMusic();
buttons.Clear();
}
public override void Update(double deltaTime)
{
base.Update(deltaTime); // Updates PGPSCN elements
// Custom update logic
// Handle animations, timers, etc.
}
public override void Render(int width, int height)
{
Raylib.ClearBackground(Color.DARKBLUE);
// Render PGPSCN elements
base.Render(width, height);
// Custom rendering
Raylib.DrawText("Main Menu", 10, 10, 20, Color.WHITE);
}
private void OnPlayClicked()
{
soundPlayer.PlaySound("assets/sounds/menu-click.wav");
// Transition to gameplay
var gameplayScene = new GameplayScene(soundPlayer);
Game.Instance.SceneManager.QueueScene(gameplayScene, 1.0f);
}
private void OnSettingsClicked()
{
soundPlayer.PlaySound("assets/sounds/menu-click.wav");
// Transition to settings
var settingsScene = new SettingsScene(soundPlayer);
Game.Instance.SceneManager.QueueScene(settingsScene, 0.5f);
}
}
3D Scene Implementation
public class GameplayScene : Scene3D
{
private Camera3D gameCamera;
private Model worldModel;
private Vector3 playerPosition;
protected override void OnLoad()
{
// Set up 3D camera
gameCamera = new Camera3D
{
position = new Vector3(0, 10, 10),
target = new Vector3(0, 0, 0),
up = new Vector3(0, 1, 0),
fovy = 45.0f,
projection = CameraProjection.CAMERA_PERSPECTIVE
};
SetMainCamera(gameCamera);
// Load 3D models
worldModel = Raylib.LoadModel("assets/models/world.obj");
SetWorldModel(worldModel);
// Initialize player
playerPosition = Vector3.Zero;
}
protected override void OnUnload()
{
// Unload 3D resources
if (worldModel.meshCount > 0)
{
Raylib.UnloadModel(worldModel);
}
}
public override void Update(double deltaTime)
{
base.Update(deltaTime);
// Update 3D game logic
UpdatePlayer(deltaTime);
UpdateCamera(deltaTime);
}
private void UpdatePlayer(double deltaTime)
{
// Player movement logic
if (Raylib.IsKeyDown(KeyboardKey.KEY_W))
playerPosition.Z -= (float)(5.0 * deltaTime);
if (Raylib.IsKeyDown(KeyboardKey.KEY_S))
playerPosition.Z += (float)(5.0 * deltaTime);
if (Raylib.IsKeyDown(KeyboardKey.KEY_A))
playerPosition.X -= (float)(5.0 * deltaTime);
if (Raylib.IsKeyDown(KeyboardKey.KEY_D))
playerPosition.X += (float)(5.0 * deltaTime);
}
private void UpdateCamera(double deltaTime)
{
// Follow player camera
gameCamera.target = playerPosition;
UpdateCamera(gameCamera);
}
}
PGPSCN Scene File Integration
Scenes can automatically load UI elements from PGPSCN scene files, providing a powerful declarative approach to UI design.
Automatic Scene File Loading
public class MenuScene : Scene
{
// Scene automatically loads MenuScene.pgpscn if it exists
protected override void OnLoad()
{
// Access elements defined in the scene file
var titleLabel = GetElement<Label>("titleLabel");
var playButton = GetElement<Button>("playButton");
var exitButton = GetElement<Button>("exitButton");
// Configure event handlers
playButton.SetOnClick(() => {
// Handle play button click
});
exitButton.SetOnClick(() => {
// Handle exit button click
});
}
}
Scene File Structure
Example MenuScene.pgpscn:
{
"elements": [
{
"id": "backgroundPanel",
"type": "Panel",
"position": { "x": 0, "y": 0 },
"size": { "x": 800, "y": 600 },
"color": "#2C3E50"
},
{
"id": "titleLabel",
"type": "Label",
"position": { "x": 400, "y": 100 },
"text": "Game Title",
"fontSize": 48,
"color": "#FFFFFF"
},
{
"id": "playButton",
"type": "Button",
"position": { "x": 350, "y": 300 },
"size": { "x": 100, "y": 40 },
"text": "Play"
}
]
}
Dynamic Element Access
// Safe element access with error handling
var optionalElement = GetElementOrNull<Button>("optionalButton");
if (optionalElement != null)
{
optionalElement.SetOnClick(HandleOptionalAction);
}
// Required element access (throws exception if not found)
try
{
var requiredElement = GetElement<Label>("requiredLabel");
requiredElement.SetText("Required text");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Required UI element not found: {ex.Message}");
}
Input Management
Scenes automatically manage input contexts, providing isolated input handling for each scene.
Input Context Setup
public class GameplayScene : Scene
{
protected override void ConfigureInputBindings()
{
// Add input bindings specific to this scene
SceneInputContext.AddBinding(KeyboardKey.KEY_ESCAPE, "pause");
SceneInputContext.AddBinding(KeyboardKey.KEY_SPACE, "jump");
SceneInputContext.AddBinding(KeyboardKey.KEY_ENTER, "interact");
// Set up input event handlers
SceneInputContext.OnActionPressed += HandleActionPressed;
}
private void HandleActionPressed(string action)
{
switch (action)
{
case "pause":
PauseGame();
break;
case "jump":
PlayerJump();
break;
case "interact":
InteractWithObject();
break;
}
}
protected override void OnUnload()
{
// Input context is automatically cleaned up
base.OnUnload();
}
}
Input Context Priority
// Scenes get priority 100 by default
// Higher priority contexts receive input first
SceneInputContext = new InputContext(GetType().Name, priority: 100);
// For overlay scenes (like pause menus), use higher priority
SceneInputContext = new InputContext(GetType().Name, priority: 200);
Resource Management
Proper resource management is critical for preventing memory leaks and ensuring smooth performance.
Scene Resource Patterns
public class ResourceManagedScene : Scene
{
private List<Texture2D> textures = new();
private List<Sound> sounds = new();
private List<Font> fonts = new();
protected override void OnLoad()
{
// Load and track resources
LoadTexture("assets/backgrounds/game-bg.png");
LoadSound("assets/sounds/ambient.ogg");
LoadFont("assets/fonts/game-font.ttf");
}
protected override void OnUnload()
{
// Clean up all resources
foreach (var texture in textures)
Raylib.UnloadTexture(texture);
textures.Clear();
foreach (var sound in sounds)
Raylib.UnloadSound(sound);
sounds.Clear();
foreach (var font in fonts)
Raylib.UnloadFont(font);
fonts.Clear();
}
private Texture2D LoadTexture(string path)
{
var texture = Raylib.LoadTexture(path);
textures.Add(texture);
return texture;
}
private Sound LoadSound(string path)
{
var sound = Raylib.LoadSound(path);
sounds.Add(sound);
return sound;
}
private Font LoadFont(string path)
{
var font = Raylib.LoadFont(path);
fonts.Add(font);
return font;
}
}
Asset Registry Integration
public class OptimizedScene : Scene
{
protected override void OnLoad()
{
// Use asset registry for shared resources
var assetRegistry = Game.Instance.AssetRegistry;
// Register reusable assets
assetRegistry.RegisterAsset("ui-button", "assets/ui/button.png", AssetType.Texture);
assetRegistry.RegisterAsset("menu-font", "assets/fonts/menu.ttf", AssetType.Font);
// Access registered assets
var buttonTexture = assetRegistry.GetAsset<Texture2D>("ui-button");
var menuFont = assetRegistry.GetAsset<Font>("menu-font");
}
// No need to manually unload registry-managed assets
}
Data Binding System
Scenes support dynamic data binding for connecting UI elements to data sources.
Basic Data Binding
public class HUDScene : Scene
{
private int playerHealth = 100;
private string playerName = "Player";
protected override void OnLoad()
{
base.OnLoad();
// Set up data bindings
var healthLabel = GetElement<Label>("healthLabel");
var nameLabel = GetElement<Label>("nameLabel");
// Create dynamic bindings
AddBinding(new DynamicBinding
{
Element = healthLabel,
Binding = new BindingDefinition
{
Property = "Text",
Source = new BindingSource { Type = "property", Key = "playerHealth" },
Fallback = "0"
}
});
}
public override void Update(double deltaTime)
{
base.Update(deltaTime); // Updates bindings automatically
// Update data values
if (Raylib.IsKeyPressed(KeyboardKey.KEY_H))
{
playerHealth = Math.Max(0, playerHealth - 10);
}
}
}
Scene Communication
Scene Parameters
// Pass data to scenes through constructors
public class GameplayScene : Scene
{
private GameSettings settings;
private PlayerProfile playerProfile;
public GameplayScene(GameSettings settings, PlayerProfile playerProfile)
{
this.settings = settings;
this.playerProfile = playerProfile;
}
}
// Usage
var gameSettings = LoadGameSettings();
var playerProfile = LoadPlayerProfile();
var gameplayScene = new GameplayScene(gameSettings, playerProfile);
sceneManager.QueueScene(gameplayScene);
Global State Access
public class ScoreScene : Scene
{
protected override void OnLoad()
{
// Access global game state
var gameState = Game.Instance.GetService<IGameStateService>();
var currentScore = gameState.GetCurrentScore();
// Update UI with current data
var scoreLabel = GetElement<Label>("scoreLabel");
scoreLabel.SetText($"Score: {currentScore}");
}
}
Scene Events
public class MenuScene : Scene
{
public static event Action<string> OnMenuAction;
private void HandlePlayButton()
{
OnMenuAction?.Invoke("play");
// Transition to gameplay
var gameplayScene = new GameplayScene();
Game.Instance.SceneManager.QueueScene(gameplayScene);
}
}
// Other scenes can listen for menu events
public class GameCoordinator
{
public GameCoordinator()
{
MenuScene.OnMenuAction += HandleMenuAction;
}
private void HandleMenuAction(string action)
{
switch (action)
{
case "play":
StartNewGame();
break;
case "settings":
OpenSettings();
break;
}
}
}
Advanced Scene Patterns
Loading Scenes
public class LoadingScene : Scene
{
private float loadingProgress = 0f;
private string loadingText = "Loading...";
private Task loadingTask;
protected override void OnLoad()
{
// Start background loading
loadingTask = LoadGameAssetsAsync();
}
public override void Update(double deltaTime)
{
base.Update(deltaTime);
// Update loading progress
var progressLabel = GetElementOrNull<Label>("progressLabel");
if (progressLabel != null)
{
progressLabel.SetText($"{loadingText} {(int)(loadingProgress * 100)}%");
}
// Check if loading is complete
if (loadingTask.IsCompleted)
{
// Transition to main game
var mainGameScene = new MainGameScene();
Game.Instance.SceneManager.QueueScene(mainGameScene, 1.0f);
}
}
private async Task LoadGameAssetsAsync()
{
var assetRegistry = Game.Instance.AssetRegistry;
// Load assets with progress updates
loadingText = "Loading textures...";
loadingProgress = 0.2f;
await Task.Delay(500); // Simulate loading
loadingText = "Loading sounds...";
loadingProgress = 0.5f;
await Task.Delay(500);
loadingText = "Loading models...";
loadingProgress = 0.8f;
await Task.Delay(500);
loadingText = "Complete!";
loadingProgress = 1.0f;
}
}
Overlay Scenes
public class PauseMenuScene : Scene
{
private Scene backgroundScene;
public PauseMenuScene(Scene backgroundScene)
{
this.backgroundScene = backgroundScene;
}
public override void Render(int width, int height)
{
// Render the background scene (paused)
backgroundScene?.Render(width, height);
// Draw semi-transparent overlay
Raylib.DrawRectangle(0, 0, width, height,
new Color(0, 0, 0, 128));
// Render pause menu UI
base.Render(width, height);
}
protected override void ConfigureInputBindings()
{
// High priority to intercept input
SceneInputContext = new InputContext(GetType().Name, priority: 200);
SceneInputContext.AddBinding(KeyboardKey.KEY_ESCAPE, "resume");
}
private void HandleResumeAction()
{
// Return to background scene
Game.Instance.SceneManager.ChangeScene(backgroundScene);
}
}
Scene State Management
public class StatefulScene : Scene
{
public enum SceneState
{
Initializing,
Running,
Paused,
Transitioning,
Cleanup
}
private SceneState currentState = SceneState.Initializing;
private Dictionary<SceneState, Action<double>> stateHandlers;
protected override void OnLoad()
{
// Set up state machine
stateHandlers = new Dictionary<SceneState, Action<double>>
{
{ SceneState.Initializing, HandleInitializing },
{ SceneState.Running, HandleRunning },
{ SceneState.Paused, HandlePaused },
{ SceneState.Transitioning, HandleTransitioning },
{ SceneState.Cleanup, HandleCleanup }
};
SetState(SceneState.Running);
}
public override void Update(double deltaTime)
{
base.Update(deltaTime);
// Handle current state
if (stateHandlers.ContainsKey(currentState))
{
stateHandlers[currentState](deltaTime);
}
}
private void SetState(SceneState newState)
{
if (currentState != newState)
{
Console.WriteLine($"Scene state: {currentState} -> {newState}");
currentState = newState;
}
}
private void HandleRunning(double deltaTime)
{
// Normal scene update logic
if (Raylib.IsKeyPressed(KeyboardKey.KEY_ESCAPE))
{
SetState(SceneState.Paused);
}
}
private void HandlePaused(double deltaTime)
{
// Paused scene logic
if (Raylib.IsKeyPressed(KeyboardKey.KEY_ESCAPE))
{
SetState(SceneState.Running);
}
}
// Other state handlers...
}
Performance Optimization
Scene Pooling
public class ScenePool
{
private Dictionary<Type, Queue<Scene>> pools = new();
public T GetScene<T>() where T : Scene, new()
{
var type = typeof(T);
if (pools.ContainsKey(type) && pools[type].Count > 0)
{
return (T)pools[type].Dequeue();
}
return new T();
}
public void ReturnScene<T>(T scene) where T : Scene
{
var type = typeof(T);
// Clean up scene state
if (scene.IsLoaded)
{
scene.Unload();
}
// Return to pool
if (!pools.ContainsKey(type))
{
pools[type] = new Queue<Scene>();
}
pools[type].Enqueue(scene);
}
}
Selective Updates
public class OptimizedScene : Scene
{
private List<IUpdatable> activeUpdaters = new();
private List<IUpdatable> passiveUpdaters = new();
private bool needsPassiveUpdate = false;
public override void Update(double deltaTime)
{
// Always update active elements
foreach (var updater in activeUpdaters)
{
updater.Update(deltaTime);
}
// Update passive elements only when needed
if (needsPassiveUpdate)
{
foreach (var updater in passiveUpdaters)
{
updater.Update(deltaTime);
}
needsPassiveUpdate = false;
}
base.Update(deltaTime);
}
public void MarkNeedsPassiveUpdate()
{
needsPassiveUpdate = true;
}
}
Best Practices
Scene Organization
- Single Responsibility: Each scene should have a clear, focused purpose
- Proper Resource Management: Always unload resources in OnUnload()
- Input Isolation: Use scene-specific input contexts to prevent conflicts
- State Management: Implement clear state machines for complex scenes
Performance Guidelines
- Minimize Allocations: Avoid creating objects in Update() loops
- Resource Sharing: Use AssetRegistry for shared resources
- Selective Updates: Only update elements that need updating
- Scene Pooling: Reuse scene instances for frequently accessed scenes
Architecture Patterns
- Dependency Injection: Pass services through constructors
- Event-Driven Communication: Use events for loose coupling
- Data Binding: Leverage the binding system for dynamic UI updates
- Scene Hierarchies: Create base scene classes for common functionality
Integration Examples
Complete Game Flow
public class GameApplication : Game
{
private SceneManager sceneManager;
private ISoundPlayer soundPlayer;
private ISettingsService settingsService;
public override void Load()
{
// Initialize services
soundPlayer = GetService<ISoundPlayer>();
settingsService = GetService<ISettingsService>();
// Initialize scene management
sceneManager = new SceneManager();
// Start with splash screen
var splashScene = new SplashScene(soundPlayer);
sceneManager.ChangeScene(splashScene);
// Queue main menu after splash
var mainMenuScene = new MainMenuScene(soundPlayer, settingsService);
sceneManager.QueueScene(mainMenuScene, 3.0f);
}
public override void Update(double deltaTime)
{
sceneManager.Update(deltaTime);
}
public override void Render()
{
sceneManager.Render();
}
protected override void CleanupResources()
{
sceneManager.UnloadAllScenes();
}
}
Troubleshooting
Common Issues
Scene Not Loading
- Verify scene constructor parameters
- Check for exceptions in OnLoad()
- Ensure PGPSCN file exists and is valid
UI Elements Not Responding
- Verify element IDs match PGPSCN file
- Check input context priority
- Ensure Update() is calling base.Update()
Memory Leaks
- Always call Unload() when transitioning
- Track and dispose of loaded resources
- Use using statements for disposable resources
Performance Issues
- Minimize Update() allocations
- Use selective update patterns
- Profile resource usage in complex scenes
The scene management system provides a robust foundation for organizing game logic while maintaining clean separation of concerns and efficient resource management.
Further Reading
- Game Architecture - Overall engine architecture
- Services - Service-oriented design patterns
- PGPSCN Format - Scene file format specification
- 3D Scenes - 3D scene implementation guide
- UI System - UI component documentation