Scene Management and Transitions in Bappa
Bappa provides a powerful scene management system that allows you to organize your game into discrete, manageable scenes. The framework supports multiple concurrent scenes, enabling sophisticated gameplay scenarios like split-screen multiplayer, mini-maps, level transitions, and more.
Understanding Scenes in Bappa
A scene in Bappa is a self-contained game world with its own entities, systems, and logic. Each scene:
- Has its own entity storage (
warehouse.Storage
) - Can have unique dimensions (width and height)
- Maintains its own loading and activation state
- Has dedicated rendering and logic systems
- Executes its own blueprint plan when initialized
Scene Lifecycles
Scenes in Bappa go through several states during their lifecycle:
- Registered: Added to the scene cache but not necessarily active. The Blueprint Plan (initialization function) hasn’t executed yet, and assets aren’t loaded
- Loading: Loading assets but not ready for gameplay
- Active: Currently running and interacting with other systems
- Inactive: May remain in cache but isn’t being updated or rendered
Scene Management Core APIs
Bappa provides several key APIs for managing scenes:
Scene Registration
// Register a new scene with the client
err := client.RegisterScene(
"LevelOne", // Scene name
640, 360, // Scene dimensions
levelOnePlan, // Blueprint Plan function
renderSystems, // Render systems
clientSystems, // Client systems
coreSystems, // Core systems
)
Scene Activation
The key to Bappa’s multi-scene capabilities is the ActivateScene
method:
// Activate a scene alongside an already active one
// optionally transferring entities between them
err := client.ActivateScene(
targetScene, // Scene to activate
entityToTransfer, // Optional entity to transfer (can be multiple)
)
This method:
- Adds the target scene to the active scenes list
- Keeps the origin scene active
- Optionally transfers specified entities from origin to target
Scene Deactivation
// Remove a scene from the active scenes list
client.DeactivateScene(targetScene)
Scene Transition (Complete Replacement)
When you want to replace the current scene entirely:
// Replace the current active scene with a new one
// optionally transferring entities
err := client.ChangeScene(
targetScene, // Scene to activate
entityToTransfer, // Optional entity to transfer
)
This method:
- Replaces the current active scene with the target scene
- Only works when exactly one scene is active
- Optionally transfers specified entities from origin to target
Implementing a Scene Transfer System
Let’s examine a simple approach for transferring players between scenes:
func (basicTransferSystem) Run(cli coldbrew.Client) error {
var pending []transfer
for activeScene := range cli.ActiveScenes() {
if !activeScene.Ready() {
continue
}
cursor := activeScene.NewCursor(blueprint.Queries.CameraIndex)
for range cursor.Next() {
if inpututil.IsKeyJustPressed(ebiten.Key1) {
currentPlayerEntity, err := cursor.CurrentEntity()
if err != nil {
return err
}
// --- Determine target scene ---
// Simple toggle between scenes
var sceneTargetName string
if activeScene.Name() == sceneOneName {
sceneTargetName = sceneTwoName
} else {
sceneTargetName = sceneOneName
}
transfer := transfer{
targetSceneName: sceneTargetName,
playerEntity: currentPlayerEntity,
}
pending = append(pending, transfer)
}
}
}
for _, transfer := range pending {
cli.ChangeSceneByName(transfer.targetSceneName, transfer.playerEntity)
}
return nil
}
This example demonstrates several important concepts:
- Cooldown Mechanism: A simple way to prevent rapid or accidental scene changes
- Two-Pass Approach: Collecting all transfers before applying them - a pattern born from necessity but valuable by design
- Scene Cleanup: Basic deactivation of scenes that no longer have players
Technical Limitation → Best Practice
The two-pass approach shown here is required because you can’t call ActivateScene()
while iterating through a cursor (the storage is locked during iteration). However, this limitation leads us to a design pattern that’s widely considered best practice in game systems anyway.
1. Camera Management Across Scenes
When working with multiple scenes, you need to manage which cameras are assigned to which scenes:
Automatic Approach
Use default
coldbrew
CameraSceneAssignerSystem
withblueprintclient
:// Register the built-in camera assigner system client.RegisterGlobalClientSystem(&clientsystems.CameraSceneAssignerSystem{}) // In your scene Blueprint Plan, create entities with CameraIndex components func MyScenePlan(height, width int, sto warehouse.Storage) error { // Create player archetype with camera component playerArchetype, err := sto.NewOrExistingArchetype( blueprintspatial.Components.Position, blueprintclient.Components.SpriteBundle, blueprintspatial.Components.Direction, blueprintinput.Components.InputBuffer, blueprintclient.Components.CameraIndex, // This links entities to cameras // Other components... ) // Create player 1 with camera index 0 err = playerArchetype.Generate(1, blueprintspatial.NewPosition(180, 180), blueprintclient.CameraIndex(0), // This entity will be followed by camera 0 blueprintinput.InputBuffer{ReceiverIndex: 0}, // First player input // Other component values... ) // Create player 2 with camera index 1 (for split-screen) err = playerArchetype.Generate(1, blueprintspatial.NewPosition(540, 180), blueprintclient.CameraIndex(1), // This entity will be followed by camera 1 blueprintinput.InputBuffer{ReceiverIndex: 1}, // Second player input // Other component values... ) // The CameraSceneAssignerSystem will automatically: // - Detect entities with CameraIndex components // - Assign the correct cameras to the scene containing those entities // - Handle split-screen layout for multiple active cameras return nil }
Manual Approach (not recommended)
// CameraSceneAssigner system example (simplified)
type CameraSceneAssigner struct{}
func (CameraSceneAssigner) Run(cli coldbrew.Client) error {
// Get active scenes and cameras
activeSceneCount := cli.SceneCount()
cameras := cli.Cameras()
// Assign first camera to first scene
if activeSceneCount > 0 && cameras[0].Active() {
cli.CameraSceneTracker()[cameras[0]] = coldbrew.CameraSceneRecord{
Scene: cli.ActiveScene(0),
Tick: cli.CurrentTick(),
}
}
// Assign second camera to second scene if available
if activeSceneCount > 1 && cameras[1].Active() {
cli.CameraSceneTracker()[cameras[1]] = coldbrew.CameraSceneRecord{
Scene: cli.ActiveScene(0),
Tick: cli.CurrentTick(),
}
}
return nil
}
Scene Instantiation Patterns
1. Template-Based Scenes
Create scenes from reusable templates:
// Define a scene template
func CreateLevelScene(levelNumber int, width, height int) blueprint.Plan {
return func(sceneWidth, sceneHeight int, storage warehouse.Storage) error {
// Common level setup...
// Level-specific configuration
levelData := LoadLevelData(levelNumber)
// Generate entities based on level data
for _, entityData := range levelData.Entities {
CreateEntityFromData(storage, entityData)
}
return nil
}
}
// Register scenes using the template
client.RegisterScene("Level1", 640, 360, CreateLevelScene(1, 640, 360), ...)
client.RegisterScene("Level2", 640, 360, CreateLevelScene(2, 640, 360), ...)
2. Procedural Scene Generation
Generate scenes procedurally:
// Register a procedurally generated level scene
client.RegisterScene(
"ProceduralLevel",
640, 360,
func(width, height int, storage warehouse.Storage) error {
// Generate terrain procedurally
seed := time.Now().UnixNano()
terrain := GenerateTerrain(seed, width, height)
// Create entities based on procedural data
for _, point := range terrain.SpawnPoints {
// Create enemies, items, etc.
}
return nil
},
renderSystems,
clientSystems,
coreSystems,
)
Implementation Constraints and Best Practices
Storage Locking During Entity Iteration
One of the most important constraints to understand when managing scenes in Bappa is storage locking:
// ❌ This will FAIL - never do this!
cursor := scene.NewCursor(someQuery)
for cursor.Next() {
// Get entity
entity, _ := cursor.CurrentEntity()
// WRONG: This will cause panic because storage is locked during cursor iteration
client.ActivateScene(newScene, entity)
}
// ✅ This is correct - two-pass approach
var entitiesToTransfer []warehouse.Entity
cursor := scene.NewCursor(someQuery)
for cursor.Next() {
// Collect entities to transfer
entity, _ := cursor.CurrentEntity()
entitiesToTransfer = append(entitiesToTransfer, entity)
}
// Now storage is unlocked, safe to transfer entities
for _, entity := range entitiesToTransfer {
client.ActivateScene(newScene, entity)
}
The reason for this constraint is that:
- When a cursor is iterating through entities, it locks the underlying storage to prevent modifications that would invalidate the iteration
- Transferring entities between scenes requires modifying the storage (removing from one, adding to another)
- Attempting these operations while the storage is locked will result in an error
Always use a two-pass approach when transferring entities based on query results:
- First collect the entities you want to transfer
- Then transfer them after completing all cursor iterations
Scene Management Performance Considerations
When working with multiple scenes, consider these performance factors:
- Active Scene Count: Each active scene consumes resources and processing time
- Scene Size: Larger scenes with many entities require more memory and processing
- Asset Duplication: Shared assets between scenes are not duplicated in memory
- Scene Loading: Loading happens asynchronously, but can still impact performance
- Transfer Overhead: Moving entities between scenes has some overhead
Best practices:
- Limit the number of simultaneously active scenes (2-3 is usually sufficient)
- Deactivate scenes when they’re not needed
- Consider using scene segmentation for large worlds (load/unload sections as needed)
Conclusion
Bappa’s multi-scene architecture provides exceptional flexibility for structuring your game. By leveraging the ability to have multiple active scenes simultaneously, you can create seamless transitions, split-screen experiences, and sophisticated game structures that would be challenging in single-scene engines.
The scene management API offers a clean, simple interface that hides the complexity of entity transfer and scene coordination, allowing you to focus on creating engaging gameplay experiences.
By understanding the principles and patterns covered in this guide, you can effectively organize your game into manageable scenes while maintaining smooth transitions and optimal performance.