Scene Lifecycle Hooks¶
Lifecycle hooks allow you to inject behavior automatically into the scene update and render loops. Think of them as ASP.NET middleware for games - registered once, executed automatically.
Overview¶
What are lifecycle hooks?
Hooks run before and after scene update/render methods:
| Hook | When It Runs | Use For |
|---|---|---|
PreUpdate |
Before OnUpdate() |
Input layers, camera setup |
PostUpdate |
After OnUpdate() |
ECS systems, physics, AI |
PreRender |
Before OnRender() |
ECS rendering, sprite batching |
PostRender |
After OnRender() |
Debug overlays, UI chrome |
Why use hooks? - ✅ Automatic - registered once, runs for every scene - ✅ Ordered - control execution order - ✅ Composable - multiple hooks work together - ✅ Opt-out - scenes can disable hooks if needed
The ISceneLifecycleHook Interface¶
public interface ISceneLifecycleHook
{
/// <summary>
/// Execution order (lower runs first).
/// Recommended ranges:
/// - 0-50: Pre-processing (input layers, camera setup)
/// - 100-200: ECS systems
/// - 500+: Post-processing (debug overlays, UI)
/// </summary>
int Order { get; }
/// <summary>
/// Called before scene.Update().
/// Use for input processing, camera setup, etc.
/// </summary>
void PreUpdate(GameTime gameTime);
/// <summary>
/// Called after scene.Update().
/// Use for ECS systems, physics, AI, etc.
/// </summary>
void PostUpdate(GameTime gameTime);
/// <summary>
/// Called before scene.Render().
/// Use for ECS rendering, sprite batching, etc.
/// </summary>
void PreRender(GameTime gameTime);
/// <summary>
/// Called after scene.Render().
/// Use for debug overlays, UI chrome, etc.
/// </summary>
void PostRender(GameTime gameTime);
}
Execution Flow¶
Update Loop with Hooks¶
sequenceDiagram
participant SM as SceneManager
participant H1 as Hook 1 (Order: 10)
participant H2 as Hook 2 (Order: 100)
participant S as Scene
participant EW as EntityWorld
participant H3 as Hook 3 (Order: 500)
SM->>H1: PreUpdate(gameTime)
SM->>H2: PreUpdate(gameTime)
SM->>H3: PreUpdate(gameTime)
SM->>S: OnUpdate(gameTime)
Note over S: Scene game logic
SM->>EW: Update(gameTime)
Note over EW: Components updated
SM->>H1: PostUpdate(gameTime)
SM->>H2: PostUpdate(gameTime)
Note over H2: ECS systems run here
SM->>H3: PostUpdate(gameTime)
Render Loop with Hooks¶
sequenceDiagram
participant SM as SceneManager
participant R as Renderer
participant H1 as Hook 1 (Order: 10)
participant H2 as Hook 2 (Order: 100)
participant S as Scene
participant H3 as Hook 3 (Order: 500)
SM->>R: BeginFrame()
SM->>H1: PreRender(gameTime)
SM->>H2: PreRender(gameTime)
Note over H2: ECS rendering
SM->>H3: PreRender(gameTime)
SM->>S: OnRender(gameTime)
Note over S: Scene rendering
SM->>H1: PostRender(gameTime)
SM->>H2: PostRender(gameTime)
SM->>H3: PostRender(gameTime)
Note over H3: Debug overlays, UI
SM->>R: EndFrame()
Built-in Hooks¶
ECSLifecycleHook¶
Brine2D includes a built-in hook for ECS systems:
internal class ECSLifecycleHook : ISceneLifecycleHook
{
private readonly UpdatePipeline _updatePipeline;
public int Order => 100; // Runs in middle range
public ECSLifecycleHook(UpdatePipeline updatePipeline)
{
_updatePipeline = updatePipeline;
}
public void PreUpdate(GameTime gameTime) { }
public void PostUpdate(GameTime gameTime)
{
// Execute all ECS update systems
_updatePipeline.Execute(gameTime);
}
public void PreRender(GameTime gameTime) { }
public void PostRender(GameTime gameTime) { }
}
Registered automatically when you call AddBrine2D() or AddObjectECS().
Creating Custom Hooks¶
Example: Debug Overlay Hook¶
using Brine2D.Core;
using Brine2D.Engine;
using Brine2D.Rendering;
public class DebugOverlayHook : ISceneLifecycleHook
{
private readonly IRenderer _renderer;
private readonly IGameContext _gameContext;
public int Order => 500; // Run after everything else
public DebugOverlayHook(IRenderer renderer, IGameContext gameContext)
{
_renderer = renderer;
_gameContext = gameContext;
}
public void PreUpdate(GameTime gameTime) { }
public void PostUpdate(GameTime gameTime) { }
public void PreRender(GameTime gameTime) { }
public void PostRender(GameTime gameTime)
{
// Draw FPS counter
var fps = 1.0 / gameTime.DeltaTime;
_renderer.DrawText(
$"FPS: {fps:F0}",
10, 10,
Color.Yellow);
// Draw memory usage
var memory = GC.GetTotalMemory(false) / 1024 / 1024;
_renderer.DrawText(
$"Memory: {memory} MB",
10, 30,
Color.Yellow);
}
}
// Register in Program.cs
builder.Services.AddSingleton<ISceneLifecycleHook, DebugOverlayHook>();
Example: Input Layer Hook¶
using Brine2D.Core;
using Brine2D.Engine;
using Brine2D.Input;
public class InputLayerHook : ISceneLifecycleHook
{
private readonly InputLayerManager _inputManager;
public int Order => 10; // Run first
public InputLayerHook(InputLayerManager inputManager)
{
_inputManager = inputManager;
}
public void PreUpdate(GameTime gameTime)
{
// Process input layers before scene update
_inputManager.Update();
}
public void PostUpdate(GameTime gameTime) { }
public void PreRender(GameTime gameTime) { }
public void PostRender(GameTime gameTime) { }
}
// Register
builder.Services.AddSingleton<ISceneLifecycleHook, InputLayerHook>();
Example: Camera Setup Hook¶
using Brine2D.Core;
using Brine2D.Engine;
using Brine2D.Rendering;
public class CameraSetupHook : ISceneLifecycleHook
{
private readonly IRenderer _renderer;
private readonly Camera _camera;
public int Order => 20; // After input, before scene update
public CameraSetupHook(IRenderer renderer, Camera camera)
{
_renderer = renderer;
_camera = camera;
}
public void PreUpdate(GameTime gameTime)
{
// Update camera before scene logic
_camera.Update(gameTime);
}
public void PostUpdate(GameTime gameTime) { }
public void PreRender(GameTime gameTime)
{
// Apply camera transform before rendering
_renderer.SetCamera(_camera);
}
public void PostRender(GameTime gameTime)
{
// Reset camera after rendering
_renderer.ResetCamera();
}
}
// Register
builder.Services.AddSingleton<ISceneLifecycleHook, CameraSetupHook>();
Hook Ordering¶
Recommended Order Ranges¶
| Order Range | Purpose | Examples |
|---|---|---|
| 0-50 | Pre-processing | Input layers, camera setup, state validation |
| 100-200 | Core systems | ECS update systems, physics, AI |
| 300-400 | Effects & animations | Particle systems, animation controllers |
| 500+ | Post-processing | Debug overlays, UI chrome, profilers |
Pattern: Lower order = runs first. Use gaps (10, 20, 30) to allow insertion between hooks.
Multiple Hooks Example¶
var builder = GameApplication.CreateBuilder(args);
// Order: 10 - Input processing
builder.Services.AddSingleton<ISceneLifecycleHook, InputLayerHook>();
// Order: 20 - Camera setup
builder.Services.AddSingleton<ISceneLifecycleHook, CameraSetupHook>();
// Order: 100 - ECS systems (registered automatically)
builder.Services.AddBrine2D(options => { ... });
// Order: 300 - Particle effects
builder.Services.AddSingleton<ISceneLifecycleHook, ParticleSystemHook>();
// Order: 500 - Debug overlay
builder.Services.AddSingleton<ISceneLifecycleHook, DebugOverlayHook>();
var game = builder.Build();
Execution order: 1. InputLayerHook (10) 2. CameraSetupHook (20) 3. ECSLifecycleHook (100) 4. ParticleSystemHook (300) 5. DebugOverlayHook (500)
Disabling Hooks Per Scene¶
Scenes can opt-out of hooks if they need manual control:
public class CustomScene : Scene
{
// Disable automatic lifecycle hooks
public override bool EnableLifecycleHooks { get; set; } = false;
protected override void OnUpdate(GameTime gameTime)
{
// ✅ Full manual control
// No hooks run - you handle everything
}
}
Use cases: - Loading screens - Cutscenes with custom update logic - Performance-critical scenes - Testing/debugging
Advanced Patterns¶
Conditional Hook Execution¶
public class ConditionalDebugHook : ISceneLifecycleHook
{
private readonly IRenderer _renderer;
private readonly IConfiguration _config;
public int Order => 500;
public ConditionalDebugHook(IRenderer renderer, IConfiguration config)
{
_renderer = renderer;
_config = config;
}
public void PreUpdate(GameTime gameTime) { }
public void PostUpdate(GameTime gameTime) { }
public void PreRender(GameTime gameTime) { }
public void PostRender(GameTime gameTime)
{
// Only run in debug builds or when enabled
if (!_config.GetValue<bool>("Debug:ShowOverlay"))
{
return;
}
// Draw debug info
_renderer.DrawText("Debug Mode", 10, 10, Color.Red);
}
}
Hook with Scene Access¶
public class SceneInfoHook : ISceneLifecycleHook
{
private readonly ISceneManager _sceneManager;
private readonly IRenderer _renderer;
public int Order => 510;
public SceneInfoHook(ISceneManager sceneManager, IRenderer renderer)
{
_sceneManager = sceneManager;
_renderer = renderer;
}
public void PreUpdate(GameTime gameTime) { }
public void PostUpdate(GameTime gameTime) { }
public void PreRender(GameTime gameTime) { }
public void PostRender(GameTime gameTime)
{
// Access current scene
var scene = _sceneManager.CurrentScene;
if (scene != null)
{
_renderer.DrawText(
$"Scene: {scene.Name}",
10, 50,
Color.Cyan);
}
}
}
Hook with Error Handling¶
Hooks run inside try-catch blocks - errors are logged but don't crash the game:
public class RiskyHook : ISceneLifecycleHook
{
private readonly ILogger<RiskyHook> _logger;
public int Order => 100;
public RiskyHook(ILogger<RiskyHook> logger)
{
_logger = logger;
}
public void PreUpdate(GameTime gameTime) { }
public void PostUpdate(GameTime gameTime)
{
try
{
// Risky operation
PerformComplexCalculation();
}
catch (Exception ex)
{
// ✅ SceneManager catches and logs this
// Other hooks still run
_logger.LogError(ex, "Complex calculation failed");
}
}
public void PreRender(GameTime gameTime) { }
public void PostRender(GameTime gameTime) { }
}
Pattern: SceneManager wraps hook calls in try-catch, logs errors, continues execution.
Complete Example¶
Game with Multiple Hooks¶
using Brine2D.Hosting;
using Brine2D.SDL;
using Microsoft.Extensions.DependencyInjection;
var builder = GameApplication.CreateBuilder(args);
// Configure Brine2D (includes ECSLifecycleHook at order 100)
builder.Services.AddBrine2D(options =>
{
options.WindowTitle = "Hook Demo";
options.WindowWidth = 1280;
options.WindowHeight = 720;
});
// Register custom hooks
builder.Services.AddSingleton<ISceneLifecycleHook, InputLayerHook>(); // Order: 10
builder.Services.AddSingleton<ISceneLifecycleHook, CameraSetupHook>(); // Order: 20
builder.Services.AddSingleton<ISceneLifecycleHook, ParticleSystemHook>(); // Order: 300
builder.Services.AddSingleton<ISceneLifecycleHook, DebugOverlayHook>(); // Order: 500
// Register scenes
builder.Services.AddScene<GameScene>();
var game = builder.Build();
await game.RunAsync<GameScene>();
Hook Implementations¶
// 1. Input Layer Hook (Order: 10)
public class InputLayerHook : ISceneLifecycleHook
{
private readonly InputLayerManager _inputManager;
public int Order => 10;
public InputLayerHook(InputLayerManager inputManager)
=> _inputManager = inputManager;
public void PreUpdate(GameTime gameTime)
=> _inputManager.Update();
public void PostUpdate(GameTime gameTime) { }
public void PreRender(GameTime gameTime) { }
public void PostRender(GameTime gameTime) { }
}
// 2. Camera Setup Hook (Order: 20)
public class CameraSetupHook : ISceneLifecycleHook
{
private readonly IRenderer _renderer;
private readonly Camera _camera;
public int Order => 20;
public CameraSetupHook(IRenderer renderer, Camera camera)
{
_renderer = renderer;
_camera = camera;
}
public void PreUpdate(GameTime gameTime)
=> _camera.Update(gameTime);
public void PostUpdate(GameTime gameTime) { }
public void PreRender(GameTime gameTime)
=> _renderer.SetCamera(_camera);
public void PostRender(GameTime gameTime)
=> _renderer.ResetCamera();
}
// 3. ECS Hook (Order: 100) - registered automatically
// 4. Particle System Hook (Order: 300)
public class ParticleSystemHook : ISceneLifecycleHook
{
private readonly ParticleManager _particles;
public int Order => 300;
public ParticleSystemHook(ParticleManager particles)
=> _particles = particles;
public void PreUpdate(GameTime gameTime) { }
public void PostUpdate(GameTime gameTime)
=> _particles.Update(gameTime);
public void PreRender(GameTime gameTime)
=> _particles.Render();
public void PostRender(GameTime gameTime) { }
}
// 5. Debug Overlay Hook (Order: 500)
public class DebugOverlayHook : ISceneLifecycleHook
{
private readonly IRenderer _renderer;
public int Order => 500;
public DebugOverlayHook(IRenderer renderer)
=> _renderer = renderer;
public void PreUpdate(GameTime gameTime) { }
public void PostUpdate(GameTime gameTime) { }
public void PreRender(GameTime gameTime) { }
public void PostRender(GameTime gameTime)
{
var fps = 1.0 / gameTime.DeltaTime;
_renderer.DrawText($"FPS: {fps:F0}", 10, 10, Color.Yellow);
}
}
Best Practices¶
✅ DO¶
1. Use appropriate order ranges
// ✅ Good - clear separation
public class InputHook : ISceneLifecycleHook
{
public int Order => 10; // Pre-processing
}
public class PhysicsHook : ISceneLifecycleHook
{
public int Order => 150; // Core systems
}
public class DebugHook : ISceneLifecycleHook
{
public int Order => 500; // Post-processing
}
2. Keep hooks focused
// ✅ Good - single responsibility
public class InputLayerHook : ISceneLifecycleHook
{
public void PreUpdate(GameTime gameTime)
{
// Only handle input layers
_inputManager.Update();
}
}
3. Use constructor injection
// ✅ Good - DI services
public class MyHook : ISceneLifecycleHook
{
private readonly IRenderer _renderer;
private readonly ILogger<MyHook> _logger;
public MyHook(IRenderer renderer, ILogger<MyHook> logger)
{
_renderer = renderer;
_logger = logger;
}
}
4. Handle errors gracefully
// ✅ Good - defensive programming
public void PostUpdate(GameTime gameTime)
{
try
{
PerformComplexOperation();
}
catch (Exception ex)
{
_logger.LogError(ex, "Operation failed");
// Don't throw - let other hooks run
}
}
❌ DON'T¶
1. Don't use conflicting orders
// ❌ Bad - same order as ECS hook
public class MyHook : ISceneLifecycleHook
{
public int Order => 100; // Conflicts with ECSLifecycleHook!
}
// ✅ Good - use gap between ranges
public class MyHook : ISceneLifecycleHook
{
public int Order => 150; // Clear gap
}
2. Don't do heavy work in wrong phase
// ❌ Bad - rendering in update
public void PostUpdate(GameTime gameTime)
{
_renderer.DrawText("Hello", 10, 10, Color.White); // Wrong phase!
}
// ✅ Good - render in render phase
public void PostRender(GameTime gameTime)
{
_renderer.DrawText("Hello", 10, 10, Color.White); // Correct!
}
3. Don't store scene references
// ❌ Bad - scene reference can become stale
private IScene? _currentScene;
public void PreUpdate(GameTime gameTime)
{
_currentScene?.Update(gameTime); // Stale!
}
// ✅ Good - get scene from SceneManager
public void PreUpdate(GameTime gameTime)
{
var scene = _sceneManager.CurrentScene; // Fresh!
}
4. Don't block the hook
// ❌ Bad - blocking operation
public void PostUpdate(GameTime gameTime)
{
Thread.Sleep(1000); // Freezes entire game!
var data = DownloadData().Result; // Blocks!
}
// ✅ Good - async operations off-thread
public void PostUpdate(GameTime gameTime)
{
// Queue async work for later
_asyncQueue.Enqueue(async () => await DownloadData());
}
Troubleshooting¶
Problem: Hook not executing¶
Symptom: Your hook's methods never run.
Solutions:
-
Check registration:
// ✅ Correct builder.Services.AddSingleton<ISceneLifecycleHook, MyHook>(); -
Verify scene has hooks enabled:
public class MyScene : Scene { public override bool EnableLifecycleHooks { get; set; } = true; // ✅ } -
Check hook is resolved:
// Debug in startup var hooks = serviceProvider.GetServices<ISceneLifecycleHook>(); Console.WriteLine($"Registered hooks: {hooks.Count()}");
Problem: Hooks running in wrong order¶
Symptom: Hook A runs after Hook B, but should run before.
Solution:
// Check Order properties
public class HookA : ISceneLifecycleHook
{
public int Order => 10; // Should run first
}
public class HookB : ISceneLifecycleHook
{
public int Order => 100; // Should run second
}
Problem: Hook errors crash game¶
Symptom: Exception in hook stops entire game.
Solution:
Hooks are wrapped in try-catch by SceneManager:
// This is handled automatically - errors are logged
public void PostUpdate(GameTime gameTime)
{
throw new Exception("Oops!"); // Logged, other hooks continue
}
// But you should still handle your own errors
public void PostUpdate(GameTime gameTime)
{
try
{
RiskyOperation();
}
catch (Exception ex)
{
_logger.LogError(ex, "Operation failed");
}
}
Summary¶
Lifecycle hooks:
| Hook | Phase | Use For |
|---|---|---|
PreUpdate |
Before scene update | Input processing, state setup |
PostUpdate |
After scene update | ECS systems, physics, AI |
PreRender |
Before scene render | ECS rendering, sprite batching |
PostRender |
After scene render | Debug overlays, UI chrome |
Key benefits:
| Benefit | Description |
|---|---|
| Automatic | Registered once, runs for all scenes |
| Ordered | Control execution order with Order property |
| Composable | Multiple hooks work together seamlessly |
| Flexible | Scenes can opt-out with EnableLifecycleHooks = false |
Common patterns:
| Pattern | Usage |
|---|---|
| Pre-processing hooks | Order 0-50 (input, camera) |
| Core system hooks | Order 100-200 (ECS, physics) |
| Post-processing hooks | Order 500+ (debug, UI) |
| Conditional execution | Check config/flags before running |
Next Steps¶
- Scene Transitions - Smooth scene changes
- ECS Systems - Build custom ECS systems
- Scene Management - Complete scene lifecycle
- Dependency Injection - Service registration patterns
Remember: Lifecycle hooks run automatically for every scene (unless disabled). Use them to inject cross-cutting behavior like input processing, ECS systems, or debug overlays! 🎮