$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

MQTT+Protocol Buffersを使ってリアルタイムに2次元位置を同期するサーバーを書いてみた

clusterのリアルタイム通信サーバーの漸進的な進化のような仕組みを理解したいなと思い、手習い用にMQTT+Protocol Buffersを使ってリアルタイムに2次元位置を同期するサーバーを書いてみた。

できたもの

簡単にclientも書いて、こういうものを作った。 terminal-shooter

  • clientを起動すると、serverに対してMQTTプロトコルで接続する
  • 30x30の盤面に自分の位置を緑で表示し、他のプレイヤーの位置を赤で表示する
  • あるクライアントで矢印キーを押すと移動でき、それがリアルタイムで他のクライアントに反映される

今はプレイヤーの位置だけ出すだけだが、今後弾を打ってプレイヤー同士戦えるようにしてみたいと思っている。

ざっくりとした仕組み

普通のMQTTのpubsubの仕組みだと、PUBLISHをserverが受け取ったら他のSUBSCRIBEしているclientに自動でブロードキャストする。ただし今回はそのままブロードキャストするのではなく、いったんserverで解釈した上で、PUBLISHパケットを再構築し、それをclientに送信するという仕組みにした。このようにしておけば、今後clientのPUBLISH以外のタイミングでの情報配信するのも簡単になる。

まずクライアント側からは移動したらMQTTのPublishを使って、serverに自分の位置を送信する。そうするとserverはメモリ上に保持したGameStateを更新し、プレイヤー位置を他のclientにPublishを使ってブロードキャストする。ここまでは普通のPubSubっぽい感じ。

接続時には他の人の情報をすぐに受け取って反映したい。そこで接続が来たらserverがすべてのプレイヤー情報を接続してきたclientにPublishを使って送信する。これによって接続後すぐに他の人の情報を受け取ることができる1

また切断時(クライアントが落ちてしまった時も含む)も、他のクライアントに通知して反映したい。そこで切断を検知したらserverは他のclientに切断したclientの情報をDISCONNECTED statusをつけて送信する。

このような仕組みで、接続時にすぐに他のプレイヤーも含めて位置情報を反映、移動するとすぐに他のクライアントにも反映、切断するとすぐに他のクライアントから消えるという仕様を実現している。

実装のざっくり解説

server/server.goでMQTTの最低限のハンドリングやコネクション管理、シャットダウンなどを実装している。今回の場合、MQTTにおけるCONNECT、SUBSCRIBE、PUBLISHの最低限だけを実装し、Topic Treeのような仕組みは何も提供していない。

このServer structにはhookを渡すことができ、そのフックポイントごとに実装を作れる。フックポイントを活用して、今回やりたいメイン実装をしているのがserver/controller.go。この中で以下の実装をすることで仕様を実現している。

  • OnConnected: プレイヤー情報の記録をする
  • OnSubscribed: 他のプレイヤーの全位置情報を送る
  • OnPublished: クライアントからの情報が来た時にハンドリングしてブロードキャストする
    • 現在は、player_stateトピックが来たらGameStateに反映し、他のクライアントにブロードキャストしている
    • PUBLISHで送るPayloadについてはshared/proto/game.protoで管理している
  • OnDisconnected: クライアントが切断した時に、他のクライアントに通知

server/state.goではゲーム状態の管理をしている。この中でプレイヤー位置や状態を全て保持している。

今後

今後は弾を打ってプレイヤー同士戦えるようにしてみたいと思っている。そのために、アイテム状態はserver側で完全に管理し、クライアントはserver側から配信された情報を信じるみたいな仕組みにしてみたい。こういう雰囲気。


  1. この仕組みはMQTTのRetained Messagesという仕組みを使っても実現可能だが、今回はそれを使わない。

MacでYouTube用にclusterの録画を撮る

下記のイベントで、MacでYouTubeアーカイブ用の録画をしたので、どのように設定したかをまとめておく。

cluster.mu

最終系

以下のように録画したい。

  • YouTubeのアスペクト比である16:9となるように
  • clusterのアプリケーションのヘッダー部分は映さない
  • clusterの音声と自分の音声の両方を録画

手順概要

  • OBSの基本設定
  • OBSソースの設定
  • clusterウインドウを適したサイズに変更
  • OBS側でヘッダー部分のクロップ

OBSの基本設定

OBS > Preferencesより、YouTubeのアスペクト比に設定し、FPSも60FPSに上げておく。録画フォーマットはmkvにしておいた方がPCが落ちてしまった時なども復旧しやすい。

OBSソースの設定

macOS スクリーンキャプチャ、macOS 音声キャプチャ、音声入力キャプチャの3つをソースに追加する。

  • macOS スクリーンキャプチャ: clusterの画面収録用。プロパティでウィンドウキャプチャを選択し、clusterを選択
  • macOS 音声キャプチャ: clusterの音声収録用。プロパティでアプリケーション音声キャプチャを選択肢、clusterを選択
  • 音声入力キャプチャ: 自分のマイク音声収録用。

clusterウインドウを適したサイズに変更

Macでコマンド一発で特定のアプリケーションのウインドウを指定サイズにするで紹介したように、ターミナルもしくはショートカットアプリを使うことでclusterのウインドウを指定サイズに変更できる。

clusterのウインドウのヘッダーサイズを測ってみると28pxということが分かったので、1280x(720+28)のサイズに変更しておくと、ヘッダークロップした後のサイズが綺麗に16:9になる。上の記事で紹介したコマンドを使うならこういう感じ。

resize-window cluster 1280 $((720+28))

OBS側でヘッダー部分のクロップ

今の状態だとまだOBS側でclusterのウインドウヘッダーが表示された状態になってしまっている。そこでmacOS スクリーンキャプチャのクロップ機能を使ってヘッダー部分を取り除く。

  • 右クリック > 変換 > 変換の編集
  • Retinaディスプレイ系の問題で解像度が2倍になっているので、大きさを1280x748に。クロップ > 上で56pxを指定(56px = ヘッダー 28px * 2)
  • 右クリック > 変換 > 画面を引き伸ばして置くで変化がないことを確認し、サイズ合わせがうまくいっていることを確認

あとは録画して、終わった後に再多重化

ここまできたら設定は完了しているので録画するだけ。録画前のチェック項目としては以下を見ておくと良い。

  • スクリーンキャプチャが動いているか
  • macOS音声キャプチャ、音声入力キャプチャが音声を取れているか、音声バランスがあっているか

録画を終了するとmkvファイルができているので、Macで確認するためにmp4形式に変換する。これはファイル > 録画の再多重化をすれば変換できる。

まとめ

今回はMacを使っているときに、clusterでヘッダーは取り除いた上でYouTube用のアスペクト比16:9で、自分のマイク音声とclusterの音声を取り込んだ状態で録画する設定をまとめてみた。clusterで録画/配信する時に参考になると嬉しい。

Macでコマンド一発で特定のアプリケーションのウインドウを指定サイズにする

cluster.mu

clusterでこちらのイベントスタッフをやることになり、そこでYouTube用のアーカイブ動画を撮る役割を担うことになった。うまくYouTubeにアップロードするために、OBSを使ってclusterをYouTubeに適切な16:9サイズ + ヘッダー無しで録画したいと考えた。

ただ意外とMacで指定のウインドウサイズに変更することが難しかったので、やってみたことをメモする。

ターミナルでコマンド一発で変更する方式

osascriptコマンドでAppleScriptを実行することでウインドウサイズを変更できる。同じ手法がhttps://dev.classmethod.jp/articles/mac-app-resize/で紹介されているが、最近のmacOSのプライバシー周りの変更などでうまく動かなくなっていたので調整した。

まずAppleScript的には以下のようにすると、clusterのウインドウサイズを1920x1080に変更できる。

set theSize to {1920, 1080} -- user set as desired

tell application "System Events"
    tell process "cluster"
        set size of every window to theSize
    end tell
end tell

これをターミナルで柔軟に実行できるようにしたbashのスクリプトが以下。

resize-window

#!/bin/bash

APP_NAME=$1
WIDTH=$2
HEIGHT=$3

osascript \
-e "set theSize to {$WIDTH, $HEIGHT}" \
-e 'tell application "System Events"' \
-e "    tell process \"$APP_NAME\"" \
-e '        set size of every window to theSize' \
-e '    end tell' \
-e 'end tell'

このスクリプトをresize-windowという名前で保存しておけば、ターミナルでこのようにアプリケーション名、幅、高さを指定して実行できる。

$ resize-window cluster 1920 1080

ちなみにこのコマンドを実行するターミナルアプリケーションにはプライバシーとセキュリティ > アクセシビリティの権限を与えておく必要があるので注意。

ショートカットアプリを使う方式

同僚に教えてもらったが、ショートカットアプリを使う方式もあるようだ。たとえば以下のように設定する。

このようにしてショートカットを実行するとclusterアプリケーションのウインドウサイズを16:9に変更できる。高さの指定が1080+28になっているのは、後からOBSでヘッダー領域をクロップしたときに16:9になるように調整したものだ。

まとめ

Macで特定アプリケーションウインドウを指定サイズにするのは意外と難しい。今回のやり方が誰かの役に立てば嬉しい。