Game Framework
Examples

Scene Management

Learn how to load, unload, and manage Unity scenes from Flutter with async loading, progress tracking, and multi-scene support.

Overview

The SceneController.cs template demonstrates:

  • Loading and unloading scenes
  • Scene transition events
  • Async loading with progress tracking
  • Multi-scene management (additive loading)
  • Scene state management

Scene management is essential for games with multiple levels, menus, or complex scene hierarchies.

Setup

game sync scripts --templates

In Unity:

  1. Create GameObject named SceneController
  2. Attach SceneController.cs script
  3. Mark as DontDestroyOnLoad for persistence
  4. Add your scenes to Build Settings

Load Scene

Basic Scene Loading

Flutter Side

await controller.sendJsonMessage('SceneController', 'loadScene', {
  'sceneName': 'GameScene',
  'mode': 'single', // or 'additive'
});

Unity Side

[FlutterMethod("loadScene")]
public void LoadScene(SceneLoadRequest request)
{
    if (_isLoading)
    {
        SendToFlutter("onSceneError", new SceneErrorEvent
        {
            sceneName = request.sceneName,
            error = "Another scene is currently loading"
        });
        return;
    }

    Debug.Log($"Loading scene: {request.sceneName}");

    try
    {
        LoadSceneMode mode = request.mode == "additive" 
            ? LoadSceneMode.Additive 
            : LoadSceneMode.Single;

        SceneManager.LoadScene(request.sceneName, mode);
    }
    catch (System.Exception e)
    {
        SendToFlutter("onSceneError", new SceneErrorEvent
        {
            sceneName = request.sceneName,
            error = e.Message
        });
    }
}

Async Loading with Progress

Load Scene Asynchronously

Flutter Side

class SceneLoader extends StatefulWidget {
  @override
  State<SceneLoader> createState() => _SceneLoaderState();
}

class _SceneLoaderState extends State<SceneLoader> {
  GameEngineController? _controller;
  double _loadProgress = 0.0;
  bool _isLoading = false;

  Future<void> _loadScene(String sceneName) async {
    setState(() {
      _isLoading = true;
      _loadProgress = 0.0;
    });

    await _controller?.sendJsonMessage('SceneController', 'loadSceneAsync', {
      'sceneName': sceneName,
      'showProgress': true,
    });
  }

  void _onMessage(GameEngineMessage message) {
    switch (message.method) {
      case 'onSceneLoading':
        setState(() => _isLoading = true);
        break;
        
      case 'onSceneProgress':
        final data = jsonDecode(message.data);
        setState(() {
          _loadProgress = (data['progress'] as num).toDouble();
        });
        break;
        
      case 'onSceneLoaded':
        final data = jsonDecode(message.data);
        setState(() {
          _isLoading = false;
          _loadProgress = 1.0;
        });
        print('Scene loaded: ${data['sceneName']}');
        break;
        
      case 'onSceneError':
        final data = jsonDecode(message.data);
        setState(() => _isLoading = false);
        _showError(data['error']);
        break;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        if (_isLoading) ...[
          LinearProgressIndicator(value: _loadProgress),
          Text('Loading... ${(_loadProgress * 100).toStringAsFixed(0)}%'),
        ],
        ElevatedButton(
          onPressed: _isLoading ? null : () => _loadScene('GameScene'),
          child: Text('Load Game Scene'),
        ),
      ],
    );
  }
}

Unity Side

[FlutterMethod("loadSceneAsync")]
public void LoadSceneAsync(SceneLoadRequest request)
{
    if (_isLoading)
    {
        SendToFlutter("onSceneError", new SceneErrorEvent
        {
            sceneName = request.sceneName,
            error = "Another scene is currently loading"
        });
        return;
    }

    StartCoroutine(LoadSceneAsyncCoroutine(request));
}

private IEnumerator LoadSceneAsyncCoroutine(SceneLoadRequest request)
{
    _isLoading = true;
    _currentlyLoadingScene = request.sceneName;

    // Send loading started event
    SendToFlutter("onSceneLoading", new SceneLoadingEvent
    {
        sceneName = request.sceneName
    });

    // Start async load
    LoadSceneMode mode = request.mode == "additive" 
        ? LoadSceneMode.Additive 
        : LoadSceneMode.Single;

    AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(request.sceneName, mode);
    asyncLoad.allowSceneActivation = false;

    // Track progress
    while (!asyncLoad.isDone)
    {
        float progress = Mathf.Clamp01(asyncLoad.progress / 0.9f);

        if (request.showProgress)
        {
            SendToFlutter("onSceneProgress", new SceneProgressEvent
            {
                sceneName = request.sceneName,
                progress = progress
            });
        }

        // Scene is ready, activate it
        if (asyncLoad.progress >= 0.9f)
        {
            // Optional: wait for minimum loading time
            yield return new WaitForSeconds(minLoadingTime);
            
            asyncLoad.allowSceneActivation = true;
        }

        yield return null;
    }

    _isLoading = false;
    _currentlyLoadingScene = null;
}

Scene Events

Unity Side - Scene Event Callbacks

protected override void Awake()
{
    base.Awake();

    // Subscribe to Unity scene events
    SceneManager.sceneLoaded += OnSceneLoaded;
    SceneManager.sceneUnloaded += OnSceneUnloaded;
    SceneManager.activeSceneChanged += OnActiveSceneChanged;
    
    // Persist across scenes
    DontDestroyOnLoad(gameObject);
}

protected override void OnDestroy()
{
    SceneManager.sceneLoaded -= OnSceneLoaded;
    SceneManager.sceneUnloaded -= OnSceneUnloaded;
    SceneManager.activeSceneChanged -= OnActiveSceneChanged;

    base.OnDestroy();
}

private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
    Debug.Log($"Scene loaded: {scene.name}");

    SendToFlutter("onSceneLoaded", new SceneLoadedEvent
    {
        sceneName = scene.name,
        buildIndex = scene.buildIndex,
        mode = mode.ToString()
    });
}

private void OnSceneUnloaded(Scene scene)
{
    Debug.Log($"Scene unloaded: {scene.name}");

    SendToFlutter("onSceneUnloaded", new SceneUnloadedEvent
    {
        sceneName = scene.name
    });
}

private void OnActiveSceneChanged(Scene oldScene, Scene newScene)
{
    Debug.Log($"Active scene: {oldScene.name}{newScene.name}");

    SendToFlutter("onActiveSceneChanged", new ActiveSceneChangedEvent
    {
        previousScene = oldScene.name,
        newScene = newScene.name
    });
}

Unload Scene

Flutter Side

await controller.sendJsonMessage('SceneController', 'unloadScene', {
  'sceneName': 'MenuScene',
});

Unity Side

[FlutterMethod("unloadScene")]
public void UnloadScene(SceneUnloadRequest request)
{
    try
    {
        Scene scene = SceneManager.GetSceneByName(request.sceneName);
        
        if (!scene.isLoaded)
        {
            SendToFlutter("onSceneError", new SceneErrorEvent
            {
                sceneName = request.sceneName,
                error = "Scene is not loaded"
            });
            return;
        }

        SceneManager.UnloadSceneAsync(request.sceneName);
    }
    catch (System.Exception e)
    {
        SendToFlutter("onSceneError", new SceneErrorEvent
        {
            sceneName = request.sceneName,
            error = e.Message
        });
    }
}

Multi-Scene Management

Additive Scene Loading

// Load menu as additive
await controller.sendJsonMessage('SceneController', 'loadScene', {
  'sceneName': 'MenuScene',
  'mode': 'additive',
});

// Load UI overlay
await controller.sendJsonMessage('SceneController', 'loadScene', {
  'sceneName': 'UIScene',
  'mode': 'additive',
});

Get Loaded Scenes

Flutter Side

await controller.sendJsonMessage('SceneController', 'getLoadedScenes', {});

// Listen for response
controller.messageStream.listen((msg) {
  if (msg.method == 'onLoadedScenes') {
    final data = jsonDecode(msg.data);
    final scenes = data['scenes'] as List;
    print('Loaded scenes: $scenes');
  }
});

Unity Side

[FlutterMethod("getLoadedScenes")]
public void GetLoadedScenes()
{
    var loadedScenes = new List<SceneInfo>();

    for (int i = 0; i < SceneManager.sceneCount; i++)
    {
        Scene scene = SceneManager.GetSceneAt(i);
        loadedScenes.Add(new SceneInfo
        {
            name = scene.name,
            buildIndex = scene.buildIndex,
            isLoaded = scene.isLoaded,
            isActive = scene == SceneManager.GetActiveScene()
        });
    }

    SendToFlutter("onLoadedScenes", new LoadedScenesEvent
    {
        scenes = loadedScenes,
        activeScene = SceneManager.GetActiveScene().name
    });
}

Complete Flutter Example

import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:gameframework/gameframework.dart';

class SceneManagementDemo extends StatefulWidget {
  @override
  State<SceneManagementDemo> createState() => _SceneManagementDemoState();
}

class _SceneManagementDemoState extends State<SceneManagementDemo> {
  GameEngineController? _controller;
  String? _currentScene;
  List<String> _loadedScenes = [];
  double _loadProgress = 0.0;
  bool _isLoading = false;

  final List<String> _availableScenes = [
    'MenuScene',
    'GameScene',
    'LevelScene',
    'BossScene',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Scene Management')),
      body: Column(
        children: [
          Expanded(
            flex: 2,
            child: GameWidget(
              engineType: GameEngineType.unity,
              config: GameEngineConfig(runImmediately: true),
              onEngineCreated: (controller) {
                setState(() => _controller = controller);
                _refreshSceneInfo();
              },
              onMessage: _onMessage,
            ),
          ),
          
          if (_isLoading) ...[
            LinearProgressIndicator(value: _loadProgress),
            Padding(
              padding: EdgeInsets.all(8),
              child: Text('Loading... ${(_loadProgress * 100).toStringAsFixed(0)}%'),
            ),
          ],
          
          Expanded(
            child: Column(
              children: [
                Padding(
                  padding: EdgeInsets.all(8),
                  child: Text(
                    'Current Scene: ${_currentScene ?? "Unknown"}',
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                ),
                Expanded(
                  child: ListView.builder(
                    itemCount: _availableScenes.length,
                    itemBuilder: (context, index) {
                      final scene = _availableScenes[index];
                      final isLoaded = _loadedScenes.contains(scene);
                      
                      return ListTile(
                        title: Text(scene),
                        trailing: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            if (isLoaded)
                              Chip(label: Text('Loaded')),
                            IconButton(
                              icon: Icon(Icons.play_arrow),
                              onPressed: _isLoading ? null : () => _loadScene(scene, 'single'),
                            ),
                            IconButton(
                              icon: Icon(Icons.add),
                              onPressed: _isLoading ? null : () => _loadScene(scene, 'additive'),
                            ),
                            if (isLoaded)
                              IconButton(
                                icon: Icon(Icons.close),
                                onPressed: () => _unloadScene(scene),
                              ),
                          ],
                        ),
                      );
                    },
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Future<void> _loadScene(String sceneName, String mode) async {
    setState(() {
      _isLoading = true;
      _loadProgress = 0.0;
    });

    await _controller?.sendJsonMessage('SceneController', 'loadSceneAsync', {
      'sceneName': sceneName,
      'mode': mode,
      'showProgress': true,
    });
  }

  Future<void> _unloadScene(String sceneName) async {
    await _controller?.sendJsonMessage('SceneController', 'unloadScene', {
      'sceneName': sceneName,
    });
  }

  Future<void> _refreshSceneInfo() async {
    await _controller?.sendJsonMessage('SceneController', 'getLoadedScenes', {});
  }

  void _onMessage(GameEngineMessage message) {
    switch (message.method) {
      case 'onSceneLoading':
        setState(() => _isLoading = true);
        break;
        
      case 'onSceneProgress':
        final data = jsonDecode(message.data);
        setState(() {
          _loadProgress = (data['progress'] as num).toDouble();
        });
        break;
        
      case 'onSceneLoaded':
        final data = jsonDecode(message.data);
        setState(() {
          _isLoading = false;
          _loadProgress = 1.0;
          _currentScene = data['sceneName'];
        });
        _refreshSceneInfo();
        break;
        
      case 'onSceneUnloaded':
        _refreshSceneInfo();
        break;
        
      case 'onLoadedScenes':
        final data = jsonDecode(message.data);
        setState(() {
          _loadedScenes = List<String>.from(
            (data['scenes'] as List).map((s) => s['name'])
          );
          _currentScene = data['activeScene'];
        });
        break;
        
      case 'onSceneError':
        final data = jsonDecode(message.data);
        setState(() => _isLoading = false);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Error: ${data['error']}')),
        );
        break;
    }
  }
}

Best Practices

1. Use Async Loading for Large Scenes

// Good - smooth loading
await controller.sendJsonMessage('SceneController', 'loadSceneAsync', {
  'sceneName': 'LargeScene',
  'showProgress': true,
});

// Avoid - blocks main thread
await controller.sendJsonMessage('SceneController', 'loadScene', {
  'sceneName': 'LargeScene',
});

2. Persist Controllers Across Scenes

protected override void Awake()
{
    base.Awake();
    DontDestroyOnLoad(gameObject);
}

3. Clean Up on Scene Unload

void OnSceneUnloaded(Scene scene)
{
    // Clean up scene-specific data
    CleanupSceneData(scene.name);
}

4. Handle Loading Errors

void _onMessage(GameEngineMessage message) {
  if (message.method == 'onSceneError') {
    final data = jsonDecode(message.data);
    // Show error to user
    _showErrorDialog(data['error']);
  }
}

Proper scene management enables smooth transitions and modular game architecture!

What You'll Learn

  • ✅ Loading and unloading scenes
  • ✅ Async loading with progress
  • ✅ Scene event handling
  • ✅ Multi-scene management
  • ✅ Error handling

Next Steps