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 --templatesIn Unity:
- Create GameObject named
SceneController - Attach
SceneController.csscript - Mark as DontDestroyOnLoad for persistence
- 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