Game Framework
SDK DocumentationAPI Reference

Lifecycle API

Complete reference for managing the game engine lifecycle including creation, pause/resume, unload, and disposal.

GameEngineController Lifecycle Methods

create()

Create and initialize the engine player.

Future<bool> create()

Returns: true if creation was successful

Usage:

// Manual engine start (when runImmediately is false)
if (!await controller.isReady()) {
  final success = await controller.create();
  if (success) {
    print('Engine started successfully');
  }
}

If GameEngineConfig.runImmediately is true (default), the engine starts automatically and you don't need to call create().

pause()

Pause the engine execution.

Future<void> pause()

Usage:

// Pause when app goes to background
await controller.pause();

When to use:

  • App goes to background
  • User pauses the game
  • Modal dialog is shown
  • User switches tabs

resume()

Resume the engine execution after pause.

Future<void> resume()

Usage:

// Resume when app comes to foreground
await controller.resume();

unload()

Unload the engine (keeps in memory but stops execution).

Future<void> unload()

Usage:

// Temporarily stop engine execution
await controller.unload();

// Later, restart if needed
await controller.create();

Use cases:

  • Switching between multiple game instances
  • Temporarily freeing resources
  • Preserving state while paused for extended period

quit()

Completely quit and destroy the engine.

Future<void> quit()

Usage:

// Completely terminate the engine
await controller.quit();

After calling quit(), the engine cannot be restarted. You must create a new GameWidget instance.

dispose()

Dispose of the controller and clean up resources.

void dispose()

Usage:

@override
void dispose() {
  _controller?.dispose();
  super.dispose();
}

Important: Always call dispose() in your widget's dispose() method to prevent memory leaks.

State Query Methods

isReady()

Check if the engine is initialized and ready.

Future<bool> isReady()

Usage:

if (await controller.isReady()) {
  // Engine is ready - send messages
  await controller.sendMessage('GameManager', 'Start', 'data');
}

isPaused()

Check if the engine is currently paused.

Future<bool> isPaused()

Usage:

if (await controller.isPaused()) {
  print('Engine is paused');
} else {
  print('Engine is running');
}

isLoaded()

Check if the engine is loaded.

Future<bool> isLoaded()

Usage:

if (await controller.isLoaded()) {
  // Engine is loaded and can be used
}

isInBackground()

Check if the engine is running in background.

Future<bool> isInBackground()

Usage:

if (await controller.isInBackground()) {
  // Engine is in background - reduce updates
}

Engine States

The engine can be in one of these states:

stateDiagram-v2
    [*] --> Created: create()
    Created --> Running: Engine starts
    Running --> Paused: pause()
    Paused --> Running: resume()
    Running --> Unloaded: unload()
    Unloaded --> Running: create()
    Running --> Destroyed: quit()
    Paused --> Destroyed: quit()
    Unloaded --> Destroyed: quit()
    Destroyed --> [*]

State Descriptions

StateDescriptionCan Send Messages?
CreatedEngine initialized but not runningNo
RunningEngine actively runningYes
PausedExecution paused, state preservedYes (buffered)
UnloadedStopped but can be restartedNo
DestroyedCompletely terminatedNo

Lifecycle Patterns

Basic Lifecycle Management

class GameScreen extends StatefulWidget {
  @override
  State<GameScreen> createState() => _GameScreenState();
}

class _GameScreenState extends State<GameScreen> with WidgetsBindingObserver {
  GameEngineController? _controller;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _controller?.dispose();
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    switch (state) {
      case AppLifecycleState.paused:
        _controller?.pause();
        break;
      case AppLifecycleState.resumed:
        _controller?.resume();
        break;
      case AppLifecycleState.inactive:
      case AppLifecycleState.detached:
      case AppLifecycleState.hidden:
        break;
    }
  }

  @override
  Widget build(BuildContext context) {
    return GameWidget(
      engineType: GameEngineType.unity,
      onEngineCreated: (controller) {
        setState(() => _controller = controller);
      },
    );
  }
}

Manual Control Pattern

class _GameScreenState extends State<GameScreen> {
  GameEngineController? _controller;
  bool _isEngineRunning = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GameWidget(
        engineType: GameEngineType.unity,
        config: GameEngineConfig(
          runImmediately: false,  // Manual control
        ),
        onEngineCreated: (controller) {
          setState(() => _controller = controller);
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _toggleEngine,
        child: Icon(_isEngineRunning ? Icons.pause : Icons.play_arrow),
      ),
    );
  }

  Future<void> _toggleEngine() async {
    if (_controller == null) return;

    if (_isEngineRunning) {
      await _controller!.pause();
      setState(() => _isEngineRunning = false);
    } else {
      if (await _controller!.isReady()) {
        await _controller!.resume();
      } else {
        await _controller!.create();
      }
      setState(() => _isEngineRunning = true);
    }
  }

  @override
  void dispose() {
    _controller?.quit();
    super.dispose();
  }
}

Multiple Game Instances Pattern

class _MultiGameScreenState extends State<MultiGameScreen> {
  GameEngineController? _game1Controller;
  GameEngineController? _game2Controller;
  int _activeGame = 1;

  Future<void> _switchToGame(int gameNumber) async {
    if (gameNumber == 1) {
      // Unload game 2, load game 1
      await _game2Controller?.unload();
      if (await _game1Controller?.isLoaded() ?? false) {
        await _game1Controller?.create();
      }
    } else {
      // Unload game 1, load game 2
      await _game1Controller?.unload();
      if (await _game2Controller?.isLoaded() ?? false) {
        await _game2Controller?.create();
      }
    }
    
    setState(() => _activeGame = gameNumber);
  }

  @override
  void dispose() {
    _game1Controller?.quit();
    _game2Controller?.quit();
    super.dispose();
  }
}

Platform-Specific Behavior

iOS Background Handling

iOS automatically pauses engines when app goes to background:

class _GameScreenState extends State<GameScreen> with WidgetsBindingObserver {
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (Platform.isIOS) {
      switch (state) {
        case AppLifecycleState.paused:
          // iOS: Engine auto-pauses, save state if needed
          _saveGameState();
          break;
        case AppLifecycleState.resumed:
          // iOS: Engine auto-resumes
          _restoreGameState();
          break;
        default:
          break;
      }
    }
  }
}

Android Background Handling

Android requires explicit pause/resume:

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
  if (Platform.isAndroid) {
    switch (state) {
      case AppLifecycleState.paused:
        _controller?.pause();
        break;
      case AppLifecycleState.resumed:
        _controller?.resume();
        break;
      default:
        break;
    }
  }
}

Desktop (macOS, Windows, Linux)

Desktop platforms handle background differently:

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
  if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
    switch (state) {
      case AppLifecycleState.inactive:
        // Window lost focus - optionally pause
        if (_pauseWhenInactive) {
          _controller?.pause();
        }
        break;
      case AppLifecycleState.resumed:
        _controller?.resume();
        break;
      default:
        break;
    }
  }
}

Event Streams

eventStream

Stream of engine lifecycle events.

Stream<GameEngineEvent> get eventStream;

Usage:

controller.eventStream.listen((event) {
  switch (event.type) {
    case GameEngineEventType.created:
      print('Engine created');
      break;
    case GameEngineEventType.started:
      print('Engine started');
      break;
    case GameEngineEventType.paused:
      print('Engine paused');
      break;
    case GameEngineEventType.resumed:
      print('Engine resumed');
      break;
    case GameEngineEventType.stopped:
      print('Engine stopped');
      break;
  }
});

Best Practices

1. Always Dispose Controllers

@override
void dispose() {
  _controller?.dispose();
  super.dispose();
}

2. Handle App Lifecycle

class _GameScreenState extends State<GameScreen> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _controller?.dispose();
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    // Handle state changes
  }
}

3. Check State Before Operations

Future<void> _sendMessage() async {
  if (await _controller?.isReady() ?? false) {
    await _controller!.sendMessage('Target', 'Method', 'data');
  } else {
    print('Engine not ready');
  }
}

4. Graceful Shutdown

Future<void> _cleanup() async {
  // Save state
  await _saveGameState();
  
  // Wait for pending operations
  await Future.delayed(Duration(milliseconds: 100));
  
  // Quit engine
  await _controller?.quit();
}

5. Handle Errors

Future<void> _pauseEngine() async {
  try {
    await _controller?.pause();
  } catch (e) {
    print('Failed to pause engine: $e');
    // Handle error gracefully
  }
}

Common Patterns

Save State on Pause

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
  if (state == AppLifecycleState.paused) {
    _saveGameState().then((_) {
      _controller?.pause();
    });
  }
}

Lazy Engine Start

class _GameScreenState extends State<GameScreen> {
  GameEngineController? _controller;
  bool _hasUserInteracted = false;

  void _onUserInteraction() {
    if (!_hasUserInteracted && _controller != null) {
      _hasUserInteracted = true;
      _controller!.create();
    }
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _onUserInteraction,
      child: GameWidget(
        engineType: GameEngineType.unity,
        config: GameEngineConfig(runImmediately: false),
        onEngineCreated: (controller) {
          setState(() => _controller = controller);
        },
      ),
    );
  }
}

Proper lifecycle management ensures smooth user experience and prevents memory leaks!

See Also