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 --templatesIn Unity:
- Create GameObject named
StateManager - Attach
StateManager.csscript - 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': 5003. Batch Related Changes
// 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