Game Framework
Examples

State Synchronization

Learn efficient state synchronization patterns with full state updates, delta compression, periodic broadcasting, and state versioning.

Overview

The StateManager.cs template demonstrates:

  • Full state synchronization
  • Delta compression (only send changed fields)
  • Periodic state broadcasting
  • State versioning and conflict resolution
  • Subscription management

State synchronization is essential for keeping Flutter UI in sync with Unity game state efficiently.

Setup

game sync scripts --templates

In Unity:

  1. Create GameObject named StateManager
  2. Attach StateManager.cs script
  3. Configure sync settings in Inspector

Full State Synchronization

Request Full State

Flutter Side

await controller.sendJsonMessage('StateManager', 'requestState', {});

Unity Side

[FlutterMethod("requestState")]
public void RequestState()
{
    SendToFlutter("onStateUpdate", _currentState);
}

private GameStateData _currentState = new GameStateData
{
    playerId = "player_123",
    playerName = "Player",
    playerHealth = 100,
    playerMaxHealth = 100,
    playerPosition = Vector3.zero,
    score = 0,
    level = 1,
    isAlive = true,
};

Receive State Updates

class GameStateManager extends StatefulWidget {
  @override
  State<GameStateManager> createState() => _GameStateManagerState();
}

class _GameStateManagerState extends State<GameStateManager> {
  GameEngineController? _controller;
  Map<String, dynamic>? _gameState;

  void _onMessage(GameEngineMessage message) {
    if (message.method == 'onStateUpdate') {
      setState(() {
        _gameState = jsonDecode(message.data);
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        if (_gameState != null) ...[
          Text('Player: ${_gameState!['playerName']}'),
          Text('Health: ${_gameState!['playerHealth']}/${_gameState!['playerMaxHealth']}'),
          Text('Score: ${_gameState!['score']}'),
          Text('Level: ${_gameState!['level']}'),
          LinearProgressIndicator(
            value: _gameState!['playerHealth'] / _gameState!['playerMaxHealth'],
          ),
        ],
      ],
    );
  }
}

Delta Compression

Only send changed fields to minimize bandwidth.

Unity Side

private GameStateData _previousState;

private void SendDeltaUpdate()
{
    var delta = ComputeDelta(_previousState, _currentState);
    
    if (delta.Count > 0)
    {
        SendToFlutter("onStateDelta", new StateDelta
        {
            version = ++_stateVersion,
            changes = delta
        });
        
        _previousState = _currentState.Clone();
    }
}

private Dictionary<string, object> ComputeDelta(
    GameStateData previous, 
    GameStateData current)
{
    var delta = new Dictionary<string, object>();
    
    if (current.playerHealth != previous.playerHealth)
        delta["playerHealth"] = current.playerHealth;
    
    if (current.score != previous.score)
        delta["score"] = current.score;
    
    if (current.playerPosition != previous.playerPosition)
        delta["playerPosition"] = current.playerPosition;
    
    // ... check other fields
    
    return delta;
}

Flutter Side

void _onMessage(GameEngineMessage message) {
  if (message.method == 'onStateDelta') {
    final delta = jsonDecode(message.data);
    
    setState(() {
      // Apply delta to current state
      _gameState ??= {};
      (delta['changes'] as Map).forEach((key, value) {
        _gameState![key] = value;
      });
    });
  }
}

Periodic Broadcasting

Subscribe to Updates

Flutter Side

// Subscribe with options
await controller.sendJsonMessage('StateManager', 'subscribe', {
  'deltaOnly': true,
  'intervalMs': 100, // 10 times per second
});

// Unsubscribe
await controller.sendJsonMessage('StateManager', 'unsubscribe', {});

Unity Side

private bool _isSubscribed = false;
private bool _deltaOnly = true;
private float _syncInterval = 0.1f;
private float _lastSyncTime = 0f;

[FlutterMethod("subscribe")]
public void Subscribe(SubscribeRequest request)
{
    _isSubscribed = true;
    _deltaOnly = request.deltaOnly;
    _syncInterval = request.intervalMs / 1000f;
    
    // Send initial full state
    SendToFlutter("onStateUpdate", _currentState);
}

[FlutterMethod("unsubscribe")]
public void Unsubscribe()
{
    _isSubscribed = false;
}

void Update()
{
    if (!_isSubscribed) return;
    
    if (Time.time - _lastSyncTime >= _syncInterval)
    {
        if (_deltaOnly)
        {
            SendDeltaUpdate();
        }
        else
        {
            SendToFlutter("onStateUpdate", _currentState);
        }
        
        _lastSyncTime = Time.time;
    }
}

Update State from Flutter

Flutter Side

// Update player health
await controller.sendJsonMessage('StateManager', 'updateState', {
  'playerHealth': 75,
});

// Update multiple fields
await controller.sendJsonMessage('StateManager', 'updateState', {
  'playerHealth': 75,
  'score': 1500,
  'level': 2,
});

Unity Side

[FlutterMethod("updateState")]
public void UpdateState(StateUpdate update)
{
    bool changed = false;
    
    if (update.playerHealth.HasValue)
    {
        _currentState.playerHealth = update.playerHealth.Value;
        changed = true;
    }
    
    if (update.score.HasValue)
    {
        _currentState.score = update.score.Value;
        changed = true;
    }
    
    if (update.level.HasValue)
    {
        _currentState.level = update.level.Value;
        changed = true;
    }
    
    if (changed)
    {
        SendDeltaUpdate();
    }
}

State Versioning

Handle state conflicts with versioning.

Unity Side

private int _stateVersion = 0;

private void SendStateUpdate()
{
    SendToFlutter("onStateUpdate", new VersionedState
    {
        version = ++_stateVersion,
        state = _currentState
    });
}

[FlutterMethod("updateState")]
public void UpdateState(VersionedStateUpdate update)
{
    if (update.expectedVersion != _stateVersion)
    {
        // Conflict detected
        SendToFlutter("onStateConflict", new StateConflict
        {
            expectedVersion = update.expectedVersion,
            currentVersion = _stateVersion,
            currentState = _currentState
        });
        return;
    }
    
    // Apply update
    ApplyStateUpdate(update.changes);
    SendStateUpdate();
}

Flutter Side

int _stateVersion = 0;

Future<void> _updateState(Map<String, dynamic> changes) async {
  await _controller?.sendJsonMessage('StateManager', 'updateState', {
    'expectedVersion': _stateVersion,
    'changes': changes,
  });
}

void _onMessage(GameEngineMessage message) {
  if (message.method == 'onStateUpdate') {
    final data = jsonDecode(message.data);
    setState(() {
      _stateVersion = data['version'];
      _gameState = data['state'];
    });
  } else if (message.method == 'onStateConflict') {
    // Handle conflict - reload full state
    _requestFullState();
  }
}

Complete Flutter Example

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

class StateSyncDemo extends StatefulWidget {
  @override
  State<StateSyncDemo> createState() => _StateSyncDemoState();
}

class _StateSyncDemoState extends State<StateSyncDemo> {
  GameEngineController? _controller;
  Map<String, dynamic> _gameState = {};
  int _stateVersion = 0;
  bool _isSubscribed = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('State Synchronization')),
      body: Column(
        children: [
          Expanded(
            child: GameWidget(
              engineType: GameEngineType.unity,
              config: GameEngineConfig(runImmediately: true),
              onEngineCreated: (controller) {
                setState(() => _controller = controller);
                _subscribe();
              },
              onMessage: _onMessage,
            ),
          ),
          
          _buildStateDisplay(),
          _buildControls(),
        ],
      ),
    );
  }

  Widget _buildStateDisplay() {
    return Container(
      color: Colors.grey[900],
      padding: EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('Game State (v$_stateVersion)',
            style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
          SizedBox(height: 8),
          ..._gameState.entries.map((e) => Padding(
            padding: EdgeInsets.symmetric(vertical: 2),
            child: Row(
              children: [
                Text('${e.key}: ', style: TextStyle(color: Colors.grey)),
                Text('${e.value}', style: TextStyle(color: Colors.white)),
              ],
            ),
          )),
        ],
      ),
    );
  }

  Widget _buildControls() {
    return Padding(
      padding: EdgeInsets.all(16),
      child: Column(
        children: [
          Row(
            children: [
              Expanded(
                child: ElevatedButton(
                  onPressed: _subscribe,
                  child: Text(_isSubscribed ? 'Subscribed' : 'Subscribe'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: _isSubscribed ? Colors.green : null,
                  ),
                ),
              ),
              SizedBox(width: 8),
              Expanded(
                child: ElevatedButton(
                  onPressed: _unsubscribe,
                  child: Text('Unsubscribe'),
                ),
              ),
            ],
          ),
          SizedBox(height: 8),
          ElevatedButton(
            onPressed: _requestFullState,
            child: Text('Request Full State'),
          ),
        ],
      ),
    );
  }

  Future<void> _subscribe() async {
    await _controller?.sendJsonMessage('StateManager', 'subscribe', {
      'deltaOnly': true,
      'intervalMs': 100,
    });
    setState(() => _isSubscribed = true);
  }

  Future<void> _unsubscribe() async {
    await _controller?.sendJsonMessage('StateManager', 'unsubscribe', {});
    setState(() => _isSubscribed = false);
  }

  Future<void> _requestFullState() async {
    await _controller?.sendJsonMessage('StateManager', 'requestState', {});
  }

  void _onMessage(GameEngineMessage message) {
    if (message.method == 'onStateUpdate') {
      final data = jsonDecode(message.data);
      setState(() {
        _stateVersion = data['version'] ?? _stateVersion;
        _gameState = Map<String, dynamic>.from(data['state'] ?? data);
      });
    } else if (message.method == 'onStateDelta') {
      final delta = jsonDecode(message.data);
      setState(() {
        _stateVersion = delta['version'];
        (delta['changes'] as Map).forEach((key, value) {
          _gameState[key] = value;
        });
      });
    }
  }
}

Best Practices

1. Use Delta Updates for Frequent Changes

// Good - only changed fields
SendDeltaUpdate();

// Avoid - full state every frame
Update() {
    SendToFlutter("onStateUpdate", _currentState);
}

2. Choose Appropriate Sync Intervals

// Real-time game (100ms = 10 Hz)
'intervalMs': 100

// Turn-based game (1000ms = 1 Hz)
'intervalMs': 1000

// UI updates only when needed
'intervalMs': 500
// Good - batch related updates
_currentState.score += points;
_currentState.level = newLevel;
_currentState.experience += exp;
SendDeltaUpdate();

// Avoid - multiple updates
_currentState.score += points;
SendDeltaUpdate();
_currentState.level = newLevel;
SendDeltaUpdate();

Delta compression can reduce bandwidth by 90% for state updates!

What You'll Learn

  • ✅ Full state synchronization
  • ✅ Delta compression
  • ✅ Periodic broadcasting
  • ✅ State versioning
  • ✅ Conflict resolution
  • ✅ Subscription management

Next Steps