Game Framework
Examples

High-Frequency Messaging

Learn how to optimize Game Framework for high-throughput scenarios with performance testing, latency measurement, and batching techniques.

Overview

The HighFrequencyDemo.cs template demonstrates:

  • Stress testing messaging throughput
  • Latency measurement and tracking
  • Message batching effectiveness
  • Throttling and rate limiting
  • Performance profiling

This example is essential for games that need real-time updates like multiplayer, physics simulations, or sensor data streaming.

Setup

game sync scripts --templates

In Unity:

  1. Create GameObject named HighFrequency
  2. Attach HighFrequencyDemo.cs
  3. Configure performance settings in Inspector

Performance Testing

Start a Test

Flutter Side

await controller.sendJsonMessage('HighFrequency', 'startTest', {
  'messagesPerSecond': 1000,
  'durationSeconds': 10,
  'useBatching': true,
});

Unity Side

[FlutterMethod("startTest")]
public void StartTest(TestConfig config)
{
    _testRunning = true;
    _messagesReceived = 0;
    _messagesSent = 0;
    
    StartCoroutine(RunPerformanceTest(config));
}

private IEnumerator RunPerformanceTest(TestConfig config)
{
    var stopwatch = System.Diagnostics.Stopwatch.StartNew();
    float interval = 1f / config.messagesPerSecond;
    
    while (stopwatch.Elapsed.TotalSeconds < config.durationSeconds)
    {
        SendToFlutter("onTestMessage", new TestMessage
        {
            id = _messagesSent++,
            timestamp = Time.time
        });
        
        yield return new WaitForSeconds(interval);
    }
    
    stopwatch.Stop();
    
    SendToFlutter("onTestComplete", new TestResults
    {
        messagesSent = _messagesSent,
        messagesReceived = _messagesReceived,
        durationSeconds = stopwatch.Elapsed.TotalSeconds,
        messagesPerSecond = _messagesSent / stopwatch.Elapsed.TotalSeconds
    });
}

Latency Measurement

Flutter Side

class LatencyTester {
  final _pendingPings = <int, DateTime>{};
  final _latencies = <int>[];

  Future<void> sendPing(GameEngineController controller, int id) async {
    _pendingPings[id] = DateTime.now();
    
    await controller.sendJsonMessage('HighFrequency', 'ping', {
      'id': id,
      'timestamp': DateTime.now().millisecondsSinceEpoch,
    });
  }

  void handlePong(Map<String, dynamic> data) {
    final id = data['id'] as int;
    final sentTime = _pendingPings.remove(id);
    
    if (sentTime != null) {
      final latency = DateTime.now().difference(sentTime).inMilliseconds;
      _latencies.add(latency);
      
      print('Ping $id: ${latency}ms');
    }
  }

  double get averageLatency {
    if (_latencies.isEmpty) return 0;
    return _latencies.reduce((a, b) => a + b) / _latencies.length;
  }

  int get minLatency => _latencies.isEmpty ? 0 : _latencies.reduce(min);
  int get maxLatency => _latencies.isEmpty ? 0 : _latencies.reduce(max);
}

Unity Side

[FlutterMethod("ping")]
public void Ping(PingMessage message)
{
    // Immediately respond with pong
    SendToFlutter("onPong", new PongMessage
    {
        id = message.id,
        timestamp = message.timestamp,
        serverTime = Time.time
    });
}

Message Batching

Enable Batching

protected override void Awake()
{
    base.Awake();
    
    // Enable message batching
    EnableBatching = true;
    BatchInterval = 0.016f; // ~60 FPS
    MaxBatchSize = 100;
}

Batching Performance

class BatchingTest extends StatefulWidget {
  @override
  State<BatchingTest> createState() => _BatchingTestState();
}

class _BatchingTestState extends State<BatchingTest> {
  int _messagesSent = 0;
  int _messagesReceived = 0;
  Duration? _testDuration;

  Future<void> _runBatchTest(bool useBatching) async {
    setState(() {
      _messagesSent = 0;
      _messagesReceived = 0;
    });

    final stopwatch = Stopwatch()..start();
    
    // Send burst of messages
    for (int i = 0; i < 1000; i++) {
      await _controller?.sendJsonMessage('HighFrequency', 'testMessage', {
        'id': i,
        'useBatching': useBatching,
      });
      _messagesSent++;
    }

    stopwatch.stop();
    
    setState(() {
      _testDuration = stopwatch.elapsed;
    });

    print('Sent 1000 messages in ${stopwatch.elapsedMilliseconds}ms');
    print('Throughput: ${1000 / stopwatch.elapsed.inSeconds} msg/s');
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Messages Sent: $_messagesSent'),
        Text('Messages Received: $_messagesReceived'),
        if (_testDuration != null)
          Text('Duration: ${_testDuration!.inMilliseconds}ms'),
        Row(
          children: [
            ElevatedButton(
              onPressed: () => _runBatchTest(false),
              child: Text('Test Without Batching'),
            ),
            SizedBox(width: 16),
            ElevatedButton(
              onPressed: () => _runBatchTest(true),
              child: Text('Test With Batching'),
            ),
          ],
        ),
      ],
    );
  }
}

Throttling

Flutter-Side Throttling

class MessageThrottler {
  Timer? _throttleTimer;
  DateTime? _lastSent;
  final Duration minInterval;

  MessageThrottler({this.minInterval = const Duration(milliseconds: 16)});

  void send(GameEngineController controller, String target, 
            String method, Map<String, dynamic> data) {
    final now = DateTime.now();
    
    if (_lastSent == null || 
        now.difference(_lastSent!) >= minInterval) {
      controller.sendJsonMessage(target, method, data);
      _lastSent = now;
    } else {
      // Schedule for later
      _throttleTimer?.cancel();
      _throttleTimer = Timer(minInterval, () {
        controller.sendJsonMessage(target, method, data);
        _lastSent = DateTime.now();
      });
    }
  }

  void dispose() {
    _throttleTimer?.cancel();
  }
}

// Usage
final throttler = MessageThrottler(minInterval: Duration(milliseconds: 16));

void onSliderChanged(double value) {
  throttler.send(_controller!, 'Player', 'setSpeed', {
    'speed': value,
  });
}

Unity-Side Rate Limiting

private float _lastSendTime = 0f;
private float _minSendInterval = 0.016f; // 60 FPS

private void SendUpdateIfReady(object data)
{
    float now = Time.time;
    
    if (now - _lastSendTime >= _minSendInterval)
    {
        SendToFlutter("onUpdate", data);
        _lastSendTime = now;
    }
}

Performance Monitoring

Complete Flutter Example

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

class PerformanceMonitor extends StatefulWidget {
  @override
  State<PerformanceMonitor> createState() => _PerformanceMonitorState();
}

class _PerformanceMonitorState extends State<PerformanceMonitor> {
  GameEngineController? _controller;
  
  int _messagesSent = 0;
  int _messagesReceived = 0;
  List<int> _latencies = [];
  double _throughput = 0;
  
  Timer? _statsTimer;

  @override
  void initState() {
    super.initState();
    
    // Update stats every second
    _statsTimer = Timer.periodic(Duration(seconds: 1), (_) {
      setState(() {
        _throughput = _messagesReceived.toDouble();
        _messagesReceived = 0;
      });
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Performance Monitor')),
      body: Column(
        children: [
          Expanded(
            child: GameWidget(
              engineType: GameEngineType.unity,
              config: GameEngineConfig(runImmediately: true),
              onEngineCreated: (controller) {
                setState(() => _controller = controller);
              },
              onMessage: _onMessage,
            ),
          ),
          
          // Performance stats
          Container(
            color: Colors.black87,
            padding: EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'Performance Stats',
                  style: TextStyle(
                    color: Colors.white,
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                SizedBox(height: 8),
                _statRow('Messages Sent', _messagesSent.toString()),
                _statRow('Throughput', '${_throughput.toStringAsFixed(0)} msg/s'),
                _statRow('Avg Latency', _avgLatency),
                _statRow('Min Latency', _minLatency),
                _statRow('Max Latency', _maxLatency),
              ],
            ),
          ),
          
          // Controls
          Padding(
            padding: EdgeInsets.all(8),
            child: Row(
              children: [
                Expanded(
                  child: ElevatedButton(
                    onPressed: _startStressTest,
                    child: Text('Start Stress Test'),
                  ),
                ),
                SizedBox(width: 8),
                Expanded(
                  child: ElevatedButton(
                    onPressed: _runLatencyTest,
                    child: Text('Latency Test'),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _statRow(String label, String value) {
    return Padding(
      padding: EdgeInsets.symmetric(vertical: 4),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label, style: TextStyle(color: Colors.grey)),
          Text(value, style: TextStyle(color: Colors.white)),
        ],
      ),
    );
  }

  String get _avgLatency {
    if (_latencies.isEmpty) return '-';
    final avg = _latencies.reduce((a, b) => a + b) / _latencies.length;
    return '${avg.toStringAsFixed(1)}ms';
  }

  String get _minLatency {
    if (_latencies.isEmpty) return '-';
    return '${_latencies.reduce((a, b) => a < b ? a : b)}ms';
  }

  String get _maxLatency {
    if (_latencies.isEmpty) return '-';
    return '${_latencies.reduce((a, b) => a > b ? a : b)}ms';
  }

  void _onMessage(GameEngineMessage message) {
    _messagesReceived++;
    
    if (message.method == 'onPong') {
      final data = jsonDecode(message.data);
      final latency = DateTime.now().millisecondsSinceEpoch - 
                     (data['timestamp'] as int);
      setState(() {
        _latencies.add(latency);
        if (_latencies.length > 100) {
          _latencies.removeAt(0);
        }
      });
    }
  }

  Future<void> _startStressTest() async {
    await _controller?.sendJsonMessage('HighFrequency', 'startTest', {
      'messagesPerSecond': 1000,
      'durationSeconds': 10,
      'useBatching': true,
    });
  }

  Future<void> _runLatencyTest() async {
    setState(() => _latencies.clear());
    
    for (int i = 0; i < 100; i++) {
      await _controller?.sendJsonMessage('HighFrequency', 'ping', {
        'id': i,
        'timestamp': DateTime.now().millisecondsSinceEpoch,
      });
      await Future.delayed(Duration(milliseconds: 10));
    }
  }
}

Best Practices

1. Use Batching for High Frequency

// Enable for position updates, sensor data, etc.
EnableBatching = true;
BatchInterval = 0.016f; // 60 FPS

2. Throttle User Input

// Throttle slider changes
void onSliderChanged(double value) {
  throttler.send(_controller!, 'Player', 'setParameter', {
    'value': value,
  });
}

3. Monitor Performance

// Track message throughput
int _lastCount = 0;
Timer.periodic(Duration(seconds: 1), (_) {
  final throughput = _messageCount - _lastCount;
  print('Throughput: $throughput msg/s');
  _lastCount = _messageCount;
});

4. Use Appropriate Message Types

// High frequency → Binary (if applicable)
// Medium frequency → JSON with batching
// Low frequency → JSON without batching

With proper optimization, Game Framework can handle thousands of messages per second!

What You'll Learn

  • ✅ Performance testing and profiling
  • ✅ Latency measurement
  • ✅ Message batching
  • ✅ Throttling and rate limiting
  • ✅ Throughput optimization

Next Steps