Popcorning Guinea Pig
Search Results for

    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

    1. Single Responsibility: Each scene should have a clear, focused purpose
    2. Proper Resource Management: Always unload resources in OnUnload()
    3. Input Isolation: Use scene-specific input contexts to prevent conflicts
    4. State Management: Implement clear state machines for complex scenes

    Performance Guidelines

    1. Minimize Allocations: Avoid creating objects in Update() loops
    2. Resource Sharing: Use AssetRegistry for shared resources
    3. Selective Updates: Only update elements that need updating
    4. Scene Pooling: Reuse scene instances for frequently accessed scenes

    Architecture Patterns

    1. Dependency Injection: Pass services through constructors
    2. Event-Driven Communication: Use events for loose coupling
    3. Data Binding: Leverage the binding system for dynamic UI updates
    4. 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
    • Edit this page
    In this article
    Back to top © 2024 Popcorning Guinea Pig Game Engine. A custom C# game engine for indie developers.