Amazon GameLift StreamsのデータチャネルでUnityアプリとWebクライアントを連携させてみた
こんにちは、ゲームソリューション部の出村です。
Amazon GameLift Streamsは、クラウド上で動作するアプリケーションの映像をエンドユーザーのWebブラウザにストリーミングするサービスです。映像や入力のストリーミングだけでなく、データチャネルという仕組みを使うことで、Amazon GameLift StreamsアプリケーションとWebクライアント(エンドユーザーのWebブラウザで動作するJavaScriptコード)の間で、任意のメッセージを安全に双方向でやり取りできます。
たとえば、以下のようなユースケースが実現可能です。
- アプリケーション内部の状態(スコア、ステータスなど)をWebクライアント側に表示する
- Webクライアント側で入力した情報(設定値、コマンドなど)をアプリケーションに送信する
- Webクライアント上のUIでアプリケーションの操作パネルを構築する
本記事では、Amazon GameLift Streams Web SDKのデータチャネル機能を使い、UnityアプリケーションとWebクライアント間でJSONメッセージを送受信するサンプルコードを解説します。
データチャネルとは
Amazon GameLift Streamsのデータチャネルは、ストリーミングされているアプリケーションとWebクライアントの間で、任意のメッセージを安全に通信するための仕組みです。
通常、Amazon GameLift Streamsはアプリケーションの画面映像をWebブラウザにストリーミングし、Webブラウザからのキーボード・マウス入力をアプリケーションに転送します。データチャネルはこれに加えて、1メッセージあたり最大64KB(65,536バイト)のバイト配列をアプリケーションとWebクライアント間で送受信できる仕組みです。
具体的な通信経路は以下のようになります。
通信経路は3つの層で構成されます。まず、Webクライアント(JavaScript)がAmazon GameLift Streamsランタイムと通信します。Webクライアントからアプリケーションへメッセージを送信するにはsendApplicationMessage関数を使い、アプリケーションからのメッセージを受信するにはclientConnection.applicationMessageコールバックを使います。次に、Amazon GameLift Streamsランタイムがアプリケーション(Unity)と通信します。この区間はTCPのデータチャネルポート(ポート40712)を介し、固定長ヘッダーとイベントデータで構成されるプロトコルでやり取りします。
アプリケーション側では、起動時にlocalhostのデータチャネルポート(ポート40712)に接続し、この接続をアプリケーションの実行中ずっと維持してメッセージを送受信します。
開発環境
この記事のサンプルコードは以下の環境で動作確認しています。
| 項目 | バージョン/詳細 |
|---|---|
| Amazon GameLift Streams Web SDK | v1.1.0 |
| Unity | 2022.3 LTS以降 |
| Node.js(ローカルテスト用) | v18以降 |
開発の進め方
データチャネルの開発は、以下の2ステップで進めると効率的です。
ステップ1: ローカル環境でモックサーバーを使って開発する
最初からAWS環境にデプロイして動作確認するのは手間がかかります。まずはローカル環境で、Unityアプリケーションとモックサーバーをデータチャネルポート(ポート40712)でTCP接続して開発を進めます。
Unity EditorとNode.jsで作成したモックサーバーをTCPのポート40712で直接接続します。
- アプリケーション側とモックサーバーの両方が
localhost:40712で通信するコードを作成します - データフォーマットにはJSONを使用します
- この段階で、送受信のロジックを作り込みます
ステップ2: Web SDKに接続するコードに差し替える
ローカルでの動作確認が完了したら、モックサーバーの代わりにAmazon GameLift Streams Web SDKを使ったWebクライアント側のコードに切り替えます。
- アプリケーション側のコードはステップ1と同じものがそのまま使えます(接続先が同じデータチャネルポートのため)
- Webクライアント側は、メッセージ送信に
sendApplicationMessage関数を使い、メッセージ受信にはclientConnection.applicationMessageコールバックを使って実装します
本番環境では、WebクライアントがWebRTCでAmazon GameLift Streamsランタイムと通信し、ランタイムがTCPのポート40712でアプリケーションと通信します。
この進め方により、AWS環境がなくてもデータチャネルのロジックを先に開発・テストでき、後からAWSのGameLift Streams環境に接続するだけで動作します。
イベントプロトコル
イベントは固定長ヘッダーとそれに続く可変長のイベントデータで構成されます。
| フィールド | サイズ | 説明 |
|---|---|---|
| クライアントID | 1バイト | 特定のクライアント接続を識別するバイト。複数の接続がある場合に使用される |
| イベントタイプ | 1バイト | 0 = クライアントが接続した、1 = クライアントが切断した、2 = クライアントからメッセージが送信された |
| イベントデータの長さ | 2バイト(ビッグエンディアン) | 後続するイベントデータのバイト数 |
| イベントデータ | Nバイト | クライアントメッセージなどのイベントデータ |
なお、切断されたクライアントが再接続した場合、新しいクライアントIDが「クライアント接続」イベントで割り当てられます。
サンプルの概要
ここからは、ボタンカウンターの値をアプリケーションとWebクライアント間で双方向に同期するサンプルを使って解説します。
- Unityアプリケーション側: 画面上にボタンA・ボタンBがあり、押すとカウンターが増加する。カウンターの値が変化するたびに、現在の状態をJSONメッセージでWebクライアントに送信する。Webクライアントからリセットや値の設定コマンドを受信すると、カウンターを更新する
- Webクライアント側: アプリケーションから受信したカウンター値をリアルタイムに表示する。リセットボタンや値の設定ボタンで、アプリケーションにコマンドを送信できる。つまり、アプリケーションの状態を表示・操作するリモートUIパネルとして機能する

サンプルコード
ここからは、上記のサンプルの実装方法を、コードとともに解説します。
メッセージフォーマット
UnityアプリケーションとWebクライアントは次のようなjsonフォーマットでデータを送っています。
アプリケーション → Webクライアント(状態通知):
{"type":"ready"}
{"type":"state","counterA":5,"counterB":3}
Webクライアント → アプリケーション(コマンド送信):
{"type":"command","action":"reset","target":"counterA"}
{"type":"command","action":"set","target":"counterA","value":42}
{"type":"command","action":"resetAll"}
アプリケーション側(Unity)のコード
アプリケーション側では、起動時にデータチャネルポート(localhost:40712)にTCP接続し、イベントプロトコルでメッセージを送受信します。以下は実装の重要なポイントです。
TCP接続とメッセージ受信ループ
バックグラウンドスレッドでTCP接続を維持し、受信したメッセージをメインスレッドに転送します。
public class DataChannelHandler : MonoBehaviour
{
[SerializeField] private string host = "localhost";
[SerializeField] private int port = 40712;
[SerializeField] private float reconnectInterval = 3f;
[SerializeField] private ButtonCounter buttonCounter;
private TcpClient _tcp;
private NetworkStream _stream;
private Thread _readThread;
private volatile bool _connected;
private volatile bool _running;
private volatile byte _clientId;
// メインスレッドで実行するアクションのキュー
private readonly ConcurrentQueue<Action> _mainThreadActions = new();
void Start()
{
if (buttonCounter != null)
buttonCounter.OnCountChanged += OnCountChanged;
_running = true;
_readThread = new Thread(ReadLoop) { IsBackground = true };
_readThread.Start();
}
void Update()
{
// バックグラウンドスレッドからのアクションをメインスレッドで実行
while (_mainThreadActions.TryDequeue(out var action))
action.Invoke();
}
}
ポイントはConcurrentQueue<Action>を使ったスレッド間連携です。TCPの受信はバックグラウンドスレッドで行い、UnityのAPI呼び出しが必要な処理はメインスレッドのキューに積んでUpdate()で実行します。
メッセージの送信
固定長ヘッダー(クライアントID、イベントタイプ、イベントデータの長さ)を付けてJSON文字列を送信します。
private bool SendString(string message)
{
if (!_connected || _stream == null || !_stream.CanWrite)
return false;
var data = Encoding.UTF8.GetBytes(message);
var header = new byte[4];
header[0] = _clientId;
header[1] = 2; // イベントタイプ = メッセージ
header[2] = (byte)(data.Length >> 8);
header[3] = (byte)(data.Length & 0xFF);
lock (_stream)
{
_stream.Write(header, 0, 4);
if (data.Length > 0)
_stream.Write(data, 0, data.Length);
_stream.Flush();
}
return true;
}
受信コマンドの処理
Webクライアントから受信したJSONコマンドをパースし、対応する処理を実行します。
private void HandleMessage(string json)
{
var msg = JsonUtility.FromJson<IncomingMessage>(json);
if (msg.type != "command") return;
switch (msg.action)
{
case "reset":
buttonCounter.ResetCounter(msg.target);
break;
case "set":
buttonCounter.SetCounter(msg.target, msg.value);
break;
case "resetAll":
buttonCounter.ResetAll();
break;
}
}
[Serializable]
private class IncomingMessage
{
public string type;
public string action;
public string target;
public int value;
}
Webクライアント側のコード
Webクライアント側の実装が、データチャネルの肝になります。Amazon GameLift Streams Web SDKが提供する2つの仕組みを正しく使うことがポイントです。
メッセージの送信: sendApplicationMessage
sendApplicationMessageは、Webクライアントからアプリケーションへメッセージをバイト配列として送信する関数です。ストリームセッションがアクティブで入力がアタッチされている間はいつでも呼び出すことができます。
/**
* アプリケーションにコマンドを送信する。
* @param {Object} command - 送信するJSONコマンドオブジェクト
*/
function dataChannelSendCommand(command) {
if (!window.myGameLiftStreams) {
console.warn('GameLiftStreams not initialized');
return;
}
const json = JSON.stringify(command);
const encoded = new TextEncoder().encode(json);
window.myGameLiftStreams.sendApplicationMessage(encoded);
}
JSONオブジェクトをTextEncoderでUTF-8バイト配列にエンコードし、sendApplicationMessageで送信します。これだけでWebクライアントからアプリケーションへコマンドを送ることができます。
実際のUIからの呼び出し例:
// カウンターをリセット
function dcResetCounter(target) {
dataChannelSendCommand({ type: 'command', action: 'reset', target: target });
}
// 全カウンターをリセット
function dcResetAll() {
dataChannelSendCommand({ type: 'command', action: 'resetAll' });
}
// カウンターに値を設定
function dcSetCounter(target) {
const value = parseInt(document.getElementById('dcSetValueA').value, 10);
dataChannelSendCommand({ type: 'command', action: 'set', target: target, value: value });
}
メッセージの受信: clientConnection.applicationMessageコールバック
clientConnection.applicationMessageは、アプリケーションから送信されたメッセージを受信するコールバック関数です。Amazon GameLift Streams Web SDKの初期化時に登録します。
/**
* アプリケーションからのバイナリメッセージを受信して処理する。
* @param {Uint8Array} message - データチャネル経由で受信したバイト配列
*/
function streamApplicationMessageCallback(message) {
try {
// バイト配列をUTF-8 JSON文字列にデコード
const text = new TextDecoder('utf-8').decode(message);
const data = JSON.parse(text);
if (data.type === 'ready') {
// アプリケーション側の準備完了
document.getElementById('dcStatus').textContent = 'Connected';
document.getElementById('dcStatus').style.color = '#4CAF50';
} else if (data.type === 'state') {
// カウンター状態の更新
document.getElementById('dcCounterA').textContent = data.counterA;
document.getElementById('dcCounterB').textContent = data.counterB;
}
} catch (e) {
console.error('Failed to parse application message:', e);
}
}
受信データはUint8Array(バイト配列)として届くので、TextDecoderでUTF-8文字列にデコードし、JSON.parseでJavaScriptオブジェクトに変換します。
SDKへの登録
上記のコールバック関数は、Amazon GameLift Streams Web SDKの初期化時にclientConnection.applicationMessageプロパティに設定します。
const clientConnection = {
// ... 他の設定 ...
applicationMessage: streamApplicationMessageCallback,
};
window.myGameLiftStreams = new GameLiftStreams({
clientConnection: clientConnection,
});
まとめ
本記事では、Amazon GameLift Streamsのデータチャネルを使い、UnityアプリケーションとWebクライアント間で双方向のメッセージ送受信を行う方法を解説しました。
データチャネルを活用することで、ストリーミングされたアプリケーションに対してWebクライアントから柔軟にメッセージを受け渡すことが可能になります。ゲームの操作パネルや管理画面、リアルタイムなデータ表示など、さまざまな表示や入力に活用してください!






