Messaging API
Complete reference for sending messages to and receiving messages from game engines.
GameEngineController Methods
The GameEngineController provides methods for bidirectional communication between Flutter and game engines.
sendMessage
Send a string message to the engine.
Future<void> sendMessage(
String target,
String method,
String data,
)Parameters:
target: Target object/actor name in the enginemethod: Method/function name to calldata: String data to send
Example:
await controller.sendMessage(
'GameManager',
'SetDifficulty',
'Hard',
);sendJsonMessage
Send structured data as JSON to the engine.
Future<void> sendJsonMessage(
String target,
String method,
Map<String, dynamic> data,
)Parameters:
target: Target object/actor name in the enginemethod: Method/function name to calldata: Map that will be automatically serialized to JSON
Example:
await controller.sendJsonMessage(
'Player',
'LoadProfile',
{
'playerId': 'user_123',
'level': 15,
'inventory': ['sword', 'shield'],
'stats': {
'health': 100,
'mana': 50,
},
},
);JSON messages are automatically serialized and deserialized, providing type-safe communication.
Receiving Messages
messageStream
Stream of messages from the engine.
Stream<GameEngineMessage> get messageStream;Usage:
controller.messageStream.listen((message) {
print('Received from ${message.target}.${message.method}');
print('Data: ${message.data}');
});GameEngineMessage
Message object received from the engine.
class GameEngineMessage {
final String target; // Target object name
final String method; // Method name
final String data; // Message data (string or JSON string)
}Message Types
String Messages
Simple string-based communication.
Flutter → Engine
await controller.sendMessage('Player', 'Jump', '');
await controller.sendMessage('GameManager', 'LoadLevel', 'Level2');Engine → Flutter
controller.messageStream.listen((message) {
if (message.method == 'OnLevelComplete') {
final levelName = message.data;
print('Completed: $levelName');
}
});JSON Messages
Structured data with type safety.
Flutter → Engine
await controller.sendJsonMessage('Player', 'TakeDamage', {
'amount': 25,
'damageType': 'fire',
'source': 'Dragon',
});
await controller.sendJsonMessage('GameManager', 'SaveGame', {
'slotId': 1,
'playerData': {
'position': {'x': 10.5, 'y': 0, 'z': 20.3},
'rotation': {'x': 0, 'y': 180, 'z': 0},
},
'gameState': {
'level': 5,
'score': 1500,
'time': 3600,
},
});Engine → Flutter
controller.messageStream.listen((message) {
if (message.method == 'OnScoreUpdate') {
// Parse JSON data
final data = jsonDecode(message.data);
final score = data['score'] as int;
final combo = data['combo'] as int;
final multiplier = data['multiplier'] as double;
setState(() {
_score = score;
_combo = combo;
});
}
});Binary Messages
For images, files, and large data transfers. Available in platform-specific implementations.
// Send binary data
final bytes = await file.readAsBytes();
await controller.sendBinaryMessage('TextureManager', 'LoadTexture', bytes);Binary messaging is engine-specific. Check your engine plugin documentation for availability.
Message Patterns
Request-Response Pattern
Implement async request-response using message IDs.
class MessageManager {
final _pending = <String, Completer<dynamic>>{};
Future<dynamic> request(
GameEngineController controller,
String target,
String method,
Map<String, dynamic> params,
) async {
final requestId = Uuid().v4();
final completer = Completer<dynamic>();
_pending[requestId] = completer;
await controller.sendJsonMessage(target, method, {
'requestId': requestId,
...params,
});
return completer.future.timeout(
Duration(seconds: 5),
onTimeout: () {
_pending.remove(requestId);
throw TimeoutException('Request timed out');
},
);
}
void handleResponse(GameEngineMessage message) {
final data = jsonDecode(message.data);
final requestId = data['requestId'] as String?;
if (requestId != null && _pending.containsKey(requestId)) {
_pending[requestId]!.complete(data);
_pending.remove(requestId);
}
}
}
// Usage
final manager = MessageManager();
// Listen for responses
controller.messageStream.listen((message) {
if (message.method == 'OnResponse') {
manager.handleResponse(message);
}
});
// Make request
final result = await manager.request(
controller,
'GameManager',
'GetPlayerStats',
{},
);
print('Stats: $result');Event Broadcasting
Listen for specific game events.
class GameEventHandler {
final Map<String, List<Function(dynamic)>> _listeners = {};
void on(String eventName, Function(dynamic) callback) {
_listeners.putIfAbsent(eventName, () => []).add(callback);
}
void off(String eventName, Function(dynamic) callback) {
_listeners[eventName]?.remove(callback);
}
void handleMessage(GameEngineMessage message) {
if (message.method == 'OnEvent') {
final data = jsonDecode(message.data);
final eventName = data['event'] as String;
final eventData = data['data'];
_listeners[eventName]?.forEach((callback) {
callback(eventData);
});
}
}
}
// Usage
final events = GameEventHandler();
// Listen to message stream
controller.messageStream.listen(events.handleMessage);
// Register event handlers
events.on('PlayerDied', (data) {
print('Player died at: ${data['position']}');
});
events.on('LevelComplete', (data) {
print('Level ${data['level']} completed in ${data['time']}s');
});
events.on('AchievementUnlocked', (data) {
_showAchievementNotification(data['achievementId']);
});Typed Messages
Create type-safe message classes.
// Define message types
class PlayerMoveMessage {
final double x;
final double y;
final double z;
PlayerMoveMessage({
required this.x,
required this.y,
required this.z,
});
Map<String, dynamic> toJson() => {
'x': x,
'y': y,
'z': z,
};
}
class GameStateMessage {
final int health;
final int score;
final String status;
GameStateMessage.fromJson(Map<String, dynamic> json)
: health = json['health'],
score = json['score'],
status = json['status'];
}
// Send typed message
await controller.sendJsonMessage(
'Player',
'MoveTo',
PlayerMoveMessage(x: 10.5, y: 0, z: 20.3).toJson(),
);
// Receive typed message
controller.messageStream.listen((message) {
if (message.method == 'OnStateUpdate') {
final state = GameStateMessage.fromJson(jsonDecode(message.data));
print('Health: ${state.health}, Score: ${state.score}');
}
});Performance Tips
Message Batching
For high-frequency updates, enable batching on the Unity side:
// Unity
protected override void Awake()
{
base.Awake();
EnableBatching = true;
BatchInterval = 0.016f; // 60 FPS
}Throttling
Throttle rapid Flutter messages:
class MessageThrottler {
Timer? _timer;
final Duration interval;
MessageThrottler(this.interval);
void send(
GameEngineController controller,
String target,
String method,
Map<String, dynamic> data,
) {
_timer?.cancel();
_timer = Timer(interval, () {
controller.sendJsonMessage(target, method, data);
});
}
void dispose() {
_timer?.cancel();
}
}
// Usage
final throttler = MessageThrottler(Duration(milliseconds: 16));
void onSliderChanged(double value) {
throttler.send(_controller!, 'Player', 'SetSpeed', {
'speed': value,
});
}Minimize Data Size
// Good - only send what's needed
await controller.sendJsonMessage('Player', 'UpdatePosition', {
'x': position.x,
'z': position.z,
});
// Avoid - sending unnecessary data
await controller.sendJsonMessage('Player', 'UpdatePosition', {
'x': position.x,
'y': position.y, // Not needed for 2D movement
'z': position.z,
'timestamp': DateTime.now().toString(), // Usually not needed
'metadata': {...}, // Unnecessary
});Error Handling
Message Send Errors
try {
await controller.sendJsonMessage('Player', 'DoAction', {
'action': 'jump',
});
} catch (e) {
print('Failed to send message: $e');
// Handle error - maybe retry or notify user
}Message Receive Validation
controller.messageStream.listen((message) {
try {
final data = jsonDecode(message.data);
// Validate expected fields
if (!data.containsKey('score')) {
print('Warning: score field missing');
return;
}
final score = data['score'];
if (score is! int) {
print('Warning: score is not an integer');
return;
}
// Process valid data
setState(() => _score = score);
} catch (e) {
print('Failed to parse message: $e');
}
});Best Practices
1. Use JSON for Structured Data
// Good
await controller.sendJsonMessage('Player', 'SetStats', {
'health': 100,
'mana': 50,
});
// Avoid
await controller.sendMessage('Player', 'SetStats', 'health:100,mana:50');2. Document Your Message Protocol
/// Send player movement command
///
/// Target: Player
/// Method: MoveTo
/// Data: {
/// 'x': double, // X position
/// 'z': double, // Z position
/// 'speed': double, // Movement speed (optional)
/// }
Future<void> movePlayer(double x, double z, {double? speed}) async {
await controller.sendJsonMessage('Player', 'MoveTo', {
'x': x,
'z': z,
if (speed != null) 'speed': speed,
});
}3. Handle Message Stream Lifecycle
class _GameScreenState extends State<GameScreen> {
GameEngineController? _controller;
StreamSubscription<GameEngineMessage>? _messageSubscription;
void _setupMessageListener() {
_messageSubscription = _controller?.messageStream.listen((message) {
// Handle messages
});
}
@override
void dispose() {
_messageSubscription?.cancel();
_controller?.dispose();
super.dispose();
}
}Well-structured messaging enables powerful, type-safe communication between Flutter and game engines!