Siv3DでWebSocketを使ってみる

これは「[Siv3D Advent Calendar 2019](https://qiita.com/advent-calendar/2019/siv3d)」の14日目の記事です。

## 1. はじめに

こんばんは、親子丼と申します。
某高専の学生で、部活にSiv3Dを布教するなどしています。

さて、皆さん「WebSocket」をご存知でしょうか。
WebSocketとは、Webで双方向通信を行うための軽量なプロトコルで、
低遅延な通信が必要なときによく用いられます。
有名な使用例だと、[slack](https://slack.com)や[Discord](https://discordapp.com/)などのコミュニケーション系ですかね。
いろいろリアルタイムにサーバと通信してインタラクティブな操作を実現しているようなWebアプリだと、
縁の下の力持ち的に裏側で頑張ってくれている技術だったりします。

個人的に、某[高専プログラミングコンテスト](http://www.procon.gr.jp/)で
リアルタイムな通信を行いたいときにWebSocketさんには大変お世話になりました。
と、ここで
「**WebSocketをSiv3Dでも使えたら素敵なのでは？**」
と思い立ち、いろいろ試行錯誤して実装したので、今回はそれについて書こうと思います。

## 2. 前準備

全てのソースコードは以下のリポジトリにあります。
<https://github.com/oyakodon/siv3dws>

### 開発環境

- Windows 10 (64-bit)
- Microsoft Visual Studio Community 2019
- OpenSiv3D SDK v0.4.2

### websocketpp

<https://github.com/zaphoyd/websocketpp>

WebSocketを実装したヘッダオンリなC++ライブラリです。
１から書くとかなり辛くなるだろうと思ったので、こちらのライブラリを使っています。
手順としては、

1. githubから**developブランチ**をzipでダウンロード (<https://github.com/zaphoyd/websocketpp/tree/develop>)
  - masterのものだと何故かうまく動作しませんでした。
  - ファイルを配置する場所はどこでも良いと思います。
2. Visual Studioのプロジェクトのプロパティからインクルードパスを通す

といった具合です。

### Boostの導入

websocketppはAsioに基づいているとのことなので、
Boostを導入しました。

- Boost v1.71.0

(未検証ですが、おそらくstandaloneでも可能かと思われます。)

今回は、既にビルドされたBinary版を使いました。
<https://sourceforge.net/projects/boost/files/boost-binaries/1.71.0/>

VS2019だと、`boost_1_71_0-msvc-14.2-32.exe`をインストールすれば良いらしいです。

### Broadcastサーバの用意

さて、ここまででビルドする準備は整いましたが、
今回は、下図のように送ったものを他の人にそのまま送るサーバを作り、
それらに各アプリを接続する形にするので、サーバが必要です。
![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87362/d2bb8d42-812a-0ecd-38af-af302b93b9c5.png)
そこで、ローカルで建てられるNode.js製のWebSocketサーバを作成しました。
<https://github.com/oyakodon/siv3dws/tree/master/ws_broadcast_server>
ダウンロード後、Node.jsがインストールされている環境で、`npm start`コマンドを実行すれば、
8080ポートにサーバが建ちます。
Webサーバにもなっていて、`http://localhost:8080`にブラウザでアクセスすると、
publicフォルダにあるindex.htmlの内容が表示されるはずです。

## 3. Siv3D + WebSocket

Siv3DでWebSocketを扱うためにClientを実装しました。
<https://github.com/oyakodon/siv3dws>

プロジェクトにcpp, hppを追加すると使えるようになります。
(事前にBoostとwebsocketppの設定が必要)

websocketppのexamples/echo_clientや
Siv3DのTCPClientの実装を参考に実装しています。

- <https://github.com/zaphoyd/websocketpp/tree/develop/examples/echo_client>
- <https://github.com/Siv3D/OpenSiv3D/blob/master/Siv3D/include/Siv3D/TCPClient.hpp>

### 使い方

WebSocketで文字列を送受信するようなプログラムを下記に示します。

```cpp
#include <Siv3D.hpp> // OpenSiv3D v0.4.2
#include "SivWSClient.hpp"

void Main()
{
	const Font font(60);

	oykdn::WebSocketClient client;
	
	client.onMessage([&](const String& msg)
		{
			Print << msg;
		}
	);

	client.onOpen([]() { Print(U"👍 connected!"); });
	client.onClose([]() { Print(U"👼 disconnected"); });
	client.onError([]() { Print(U"🙅‍♀ ERROR"); });

	client.open(U"ws://localhost:8080/");

	while (System::Update())
	{
		font(U"Hello, Siv3D!🐣").drawAt(Scene::Center());
		Circle(Cursor::Pos(), 20).draw(ColorF(1, 0, 0, 0.5));

		if (SimpleGUI::Button(U"Send", Vec2(600, 20)))
		{
			client.send(U"いろはにほへと");
		}
	}
}
```

- Point
  - `oykdn::WebSocketClient`でClientを作ることができます！
  - `client.onMessage(~)`で受信時に実行する内容を登録できます！ (コールバック関数)
  - `onOpen`, `onClose`, `onError`でそれぞれ接続時、切断時、エラー時のイベントを登録できます！
  - `client.send(U"文字列")`で文字を送信することができます！

このアプリを2つ起動して、交互にSendボタンを押すと互いに「いろはにほへと」が
表示されるはずです！
また、先程のNode.js製サーバを使っている場合は、`http://localhost:8080/test.html`で、
下図のようにWebとSiv3D間でメッセージのやり取りができます！

![test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87362/9417ed51-5f05-b0c1-41f5-8b6518b606b0.gif)

## 4. Circleをスマホで操作してみる

では、スマートフォンのブラウザからSiv3D上のCircleを操作してみます。

```cpp
#include <Siv3D.hpp> // OpenSiv3D v0.4.2
#include "SivWSClient.hpp"
#include "JSONParser.hpp"

void Main()
{
	Scene::SetBackground(ColorF(0.8, 0.9, 1.0));

	const Font fontSm(24);
	const Font font(60);

	Circle me(Scene::Center(), 30);
	Vec2 diff = Vec2::Zero();

	oykdn::WebSocketClient client;
	oykdn::JSONParser parser;

	client.onMessage([&](const String& msg)
		{
			if (!parser.parse(msg))
			{
				return;
			}

			if (parser.getOptDouble(U"x") && parser.getOptDouble(U"y"))
			{
				diff.x = parser.getOptDouble(U"x").value();
				diff.y = parser.getOptDouble(U"y").value();
				me.moveBy(diff.x / 10.0, diff.y / 10.0);
			}
		}
	);

	client.open(U"ws://localhost:8080/");

	while (System::Update())
	{
		fontSm(U"diff: ", diff).draw(Vec2(10, 10), Palette::Black);

		font(U"Hello, Siv3D!🐣").drawAt(Scene::Center(), Palette::Black);

		if (KeyR.down())
		{
			me.setPos(Scene::Center());
		}

		me.draw(ColorF(1, 0, 0, 0.5));
	}

}
```

先程のサンプルと構造はほぼ同じですが、`onMessage`の処理が少し異なります。
ここでは、JSONから変位diffを読み取って、そのx, yの分だけCircleを動かしています。

ちなみに、`oykdn::JSONParser`は自作のJSONからデータを抜き取るためのものです。
JSON文字列を渡すと`,`や`:`でsplitしてkey-valueに分解し、HashTableに登録します。
HashTableから指定したkeyのデータを任意の型に変換して取り出せます。
オブジェクトがネストされていたり、リスト形式のもののパースには対応していませんが、
テストなのでと割り切って使っています。

Webの方はコードは割愛しますが、
touchmove(mousemove)イベントが発火した際に、変位をJSONにして
WebSocketで送っているだけです。

こんな感じに赤い円が動きます(例は、スマホではなくChromeの開発者モード)。

![circlecontrol.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87362/6cc405a9-57f7-b694-4e69-4bada151b9d1.gif)


ヌメヌメ動いてますね！

## (番外編) ピクト○ャットのようなもの

Siv3Dの[スケッチのサンプル](https://siv3d.github.io/ja-jp/sample/app/#_2)を見ていたら、
「ピク○チャットのようなお絵かきチャットをこれで作れるのでは？」
と思ったので、実装してみました。

![chat.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87362/a7a28d23-9d08-084e-ddd1-8fa8ff53167b.gif)

メッセージのやり取りはほぼ上記のものと同じ仕組みを使っています。
画像はBase64でエンコード/デコードして、JSONに載せて送っています。
なので、実行中はサーバのログが大変なことになりますw

## 5. まとめ

今回は、WebSocketをSiv3Dでも使えるようにすることを目的として、
`WebSocketClient`を実装し、その例をいくつか示しました。

WebSocketはWeb(JavaScript)の世界で幅広く使われているプロトコルで、
インターネットを介して離れたところと低遅延で通信できるため、
Siv3Dでも色々使い所はあると思います。
(ゲームの通信で用いたり、APIを叩いてみたりなどなど...。)

機会があれば是非WebSocketに触ってみてください！

## あとがき

最近、[M5StickC](https://www.switch-science.com/catalog/5517/)という、
加速度センサやジャイロセンサ、小さなTFT画面、Wi-FiやBluetoothが使えて
Arduino IDEが開発できるESP32マイコンを搭載したIoT開発基盤を買ったのですが、
これをSiv3Dでコントローラ代わりにしたいという野望もあり、
Siv3DでWebSocket通信したいなぁと思ったところもあります。
時間があるときに実装して、どこかで共有できたらと思っております。