OpenSiv3Dでオンラインのシミュレータを作る話

#概要
OpenSiv3DのTCP機能を使ってオンラインのシミュレータを作りました。
今回はその解説です。
普段OpenSiv3Dでゲーム開発をやっていてサーバーサイドさっぱりな人がオンライン開発のきっかけになればと思って書いています。
ただし、自分自身サーバー初心者なので間違いがあったらコメントお願い致します。
![SyElectro Client 2019_12_17 0_03_59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213924/b16b52c1-5402-81ac-c8b8-57119244e690.png)
#方針
オンラインでアプリを作るためにはサーバーとクライアントが必要です。
一番簡単なのは同一ネットワーク(簡単に言えば、同じWiFiに接続しているときです)内での通信で、それなら同じアプリでサーバーとクライアントを作れます。
しかし、違うネットワーク間での通信をやろうと思ったら大変です。ネットワーク通信をやるときは最初にクライアントがサーバーに接続するのですが、その時にサーバー側のファイヤーウォールに弾かれます。あと、普通のネット契約だとIPアドレスが時間が経つと変わるので、サーバーのIPアドレスがわからなくなります。
では、どうすればいいか？簡単です。サーバーを借りればいいんです。
ということで、VPSという仮想サーバーを借ります。大体月に1000円かかりますが、一日二日の利用なら100円もしません。
サーバー側のOSはLinuxなのですが、OpenSiv3Dのビルドが正直辛いです。
まあ一度成功すれば大丈夫なので頑張りましょう。

クライアントはWindowsとOpenSiv3Dで作ります。
MacやLinuxで普段使っているならそれでも大丈夫だと思います。

通信についてはOpenSiv3DにはTCPClientとTCPServerがあるのでそれを使います。
ただし、たまにデータの欠落が発生するので注意です。（今回はあまり気にせずに行きます）

# 環境構築
ここで全部書いたら心折れそうなのでめっちゃ端折ってます。
需要がありそうなら追記していきます。
まずは、サーバー側から
VPSは[ConoHa](https://www.conoha.jp)のVPSを使います。
あとは、[こちらのチュートリアル](https://support.conoha.jp/vps/guide/vpsstartup/?btn_id=vps-guide_guide-vpsstartup)を参考に以下の手順で進めてください。

## VPSの追加
OSは色々ありますが、迷ったらUbuntuを使ってみるといいと思います。
OpenSiv3Dのビルドには、最新のライブラリが必要なものが多いのでArch Linuxの方がいい気が最近していますが、ビルド成功していないのでまだわからないです。

## サーバー接続
クライアントはPowershell、もしくはコマンドプロンプトをお勧めします。標準でWindowsに入っているので。
サーバー接続には、以下のコマンドで入れます。
ssh root@xxx.xx.xx.xx
(xの部分はIPアドレスです。ConoHa VPSのサーバー管理メニューに書いてあります)

## アカウント追加
rootアカウント以外で入れるように、サーバー内に新しいアカウントを作ります。
以下アカウント名をsknjpnとします。
IPアドレスはsylife.jpとします。（ドメイン取得はまたいつか解説します)

## 公開鍵、秘密鍵の設定
*Windows内で*ssh-keygenしてサーバーに
`scp .pubファイルのパス sknjpn@sylife.com:/home/`
でファイル送信をします。
あとは、上げたファイルを
`/home/sknjpn/.ssh/authorizedKeys`
に配置してください。

## アカウントの管理
### ユーザーアカウントがsudoできるようにする
wheelグループに追加したり、visudoで権限を変えたり、調べてやってください。
### パスワードログイン禁止、rootログイン禁止
sshdの設定を行ってください。
ちなみに、sshはクライアントの名前で、sshで接続されるサーバーのサービスがsshdです。

## 最新のOpenSiv3Dのダウンロードとビルド
[こちらのOpenSiv3Dのビルド](https://github.com/Siv3D/OpenSiv3D/blob/master/Linux/README_JP.md)の記述通りにやってください。
ライブラリが必要とかは無視してまずやってみます。
3のビルドでnot foundのエラーが出ると思います。

## 必要なライブラリ導入
基本的な方針は動かなかったらnot foundのライブラリを
sudo apt install パッケージ名
で入れることです。
パッケージ名はライブラリの先頭にlibとつけたり、最後に-devと付けたらあることが多いです。(適当で申し訳ない)
ただ、注意すべきライブラリがあります。
まず、OpenCV。ubuntuのパッケージは古いのでv4.0.1をビルドします。
次にboost。boostも同様にv1.70をビルドします。
どのバージョンでビルドするべきかは[こちらを参考に](https://github.com/Siv3D/OpenSiv3D/blob/master/Siv3D/ThirdPartyLibraries.md)。
boostは最新のビルドだとダメだったりするので要注意。
そして、一番やっかいなのがgccのバージョンです。多分、sudo apt updateでは、7.4になると思いますが、OpenSiv3Dでは、9.0が必要なので、自分でビルドして入れる必要があります。(こうすると異なるバージョンのgccが混在するので、使用するgccを明示してビルドする必要があり、なかなかにハードです)
もし、NavMeshDetail.cppでエラーが発生した場合は低いバージョンのgccでビルドしている可能性が高いです。

## VSCode Remote Developmentを使う
Windows普段使いの人がコマンドプロンプトでエディタ使うのは険しい人も多いと思います。
そういうときはVSCodeのRemote Development使いましょう。
sshと同じ要領で接続するだけで簡単にサーバー上の編集が可能です。
コピペとか、ファイル転送が楽です。

## VPS特有の記述
VPSとパソコンの大きな違いとして、ディスプレイがないことがあります。
そうすると、VPSでOpenSiv3Dを動かそうとすれば十中八九落ちます。
そこで、描画系の初期化処理を実行しないように修正を行います。
ただし、この対応を行うと.draw()命令などは使えなくなるので、そのことに注意してコードを書く必要があります。
### before

 ```C++:OpenSiv3d/Siv3D/src/Siv3D-Platform/Linux/Siv3DMain.cpp
       
        Siv3DEngine engine;

	try
	{
		Siv3DEngine::Get<ISiv3DSystem>()->init();
	}
	catch (const EngineError& error)
	{
		const String text = U"EngineError: {}"_fmt(error.what());
		LOG_ERROR(text);
		EngineMessageBox::Show(text);
		return -1;
	}
```  
 
### after

```C++:OpenSiv3d/Siv3D/src/Siv3D-Platform/Linux/Siv3DMain.cpp

	Siv3DEngine engine;

	Main();
	return 0;
	
	try
	{
		Siv3DEngine::Get<ISiv3DSystem>()->init();
	}
	catch (const EngineError& error)
	{
		const String text = U"EngineError: {}"_fmt(error.what());
		LOG_ERROR(text);
		EngineMessageBox::Show(text);
		return -1;
	}
```

## angelscriptの導入
[こちら](https://www.angelcode.com/angelscript/downloads.html)から先ほどのOpenSiv3Dの必要ライブラリバージョン一覧を参考にダウンロードします。コマンドはwgetなど使って下さい。unzipで解凍できます。
解凍したら
angelscript/projects/gnuc
内にcdで移動して、
make
sudo make install
でインストールできます。

## 再ビルド
Mainの処理の適用がされていないと思うので、最後にもう一度ビルドしてください。

## アプリのビルド
ビルドしたときに、undefinedというエラーが出てきて、そこにライブラリの名前が出てきたら(たとえば、boostとかopencvとか)、大体そのライブラリがないかバージョンが違います。インストールしなおして下さい。
あと、gccのバージョンは気を付けてください。
libstdc++のバージョンがありませんのメッセージが来たらgccの適用が出来てないことが原因の可能性が高いです。

## 実行する
` ./Siv3D_App `
で実行できたらOKです。
この時にこの時にグラフィック系の問題が起こるなら、Mainの修正が出来ていないか、実行したコードにdraw系の関数やPrintfがある可能性があります。（特に`System::Update()`関数も使ってはいけません。unistd.hのuleepなどで対応しましょう）

## クライアント側の開発環境
OpenSiv3Dのプロジェクトを作成してビルドできるようになっているならどの環境についても大丈夫だと思います。

# コードを書く
今回は完成したプロジェクトを公開します。
https://github.com/sknjpn/SyElectro-Example
注意すべき点は以下の通りです。

## ユーザーは1人ではない
ということで、サーバー側は複数接続をカバーできるようにします。

## データの長さが分からない
データはバイナリで送っているのですが、どこで終わるか分からないので、最初にサイズ情報を送っています。
最初に話した通り通信データは欠落する可能性があるので、現状ではサイズが間違って送られて落ちる可能性があります。
（これまで稼働した感じでは、サーバーからユーザーへの通信が特に危なそうです）

## ユーザーに現状の状態をすべて送るのは通信量的に厳しい
VPSで一般的に通信出来る最大速度は10MB/sもありません。
ということは、100人同時通信する場合１人当たりの通信速度は最大100KB/sであり、60FPSで毎フレーム通信するとしたら1フレームに通信出来るのは最大1KB/sということになります。
シミュレータの場合、100体や1000体の物体の同時シミュレーションをしようと考えると、最悪１物体に割り当てらる通信データは1Bということになりかねません。
というか、あまりユーザーとの通信を多くするとそれだけリスクが高まる上、処理コストも増大するので、なるべく通信量は削減したほうがいいです。
ということで、今回はサーバーからユーザーの通信のうち、フィールドデータの共有は最初の１回のみとし、あとはユーザーコマンドの共有と、現在実行しているタイミングの共有のみを行い、サーバーと全てのクライアント間で全く同じ条件でシミュレーションすることで、同期を実現しました。
基本的に、同じプログラムを用いているので、ユーザーのコマンドを適用するタイミングさえ一致すれば理論上はシミュレーション結果は一致するので、コマンドデータの共有だけで同期を実現しようということです。
そのうえで重要なのは、コマンドの適用するタイミングです。サーバーからコマンドを受け取って即フィールドに適用してしまうと各クライアント間で適用タイミングがずれる可能性が高いです。もし、1フレームでも適用タイミングがずれるとバタフライ効果と同様にどんどんずれていきます。こうなると全てが破綻します。
そこで、コマンドデータにコマンド適用タイミングを保存するようにしました。
そして、コマンドを適用するタイミングはなるべく遅延するべきではないので、サーバーがコマンドを受け取り次第即適用します。
しかし、そうするとクライアントの更新速度が重要です。もし、クライアントの更新したフレームがサーバー側より遅いと、コマンドが適用されるのも遅くなるので、ユーザーの感じる遅延は長くなります。逆にクライアントが早いと最悪の場合サーバーよりも早く進んでしまい、コマンドを適用できなくなってしまいます。
そこで、サーバー側から、何フレームまで更新して大丈夫なのかを送ることにしました。
こうすることで、多少の遅延はありますが、サーバーとクライアントで同期することが可能になりました。


# 課題
まず、前提としてTCP通信は第三者の攻撃によってロスと改ざん、盗聴される可能性がありますので、そのことを留意してシステムを構築する必要があります。そのため、今回のプログラムのままではアカウントのパスワードなどを送信するのは大変危険です。
もしやるなら、メール認証など別の方法を取った方がいいかもしれません。
あと、現状のプログラムでもヘッダのサイズデータに改ざんが入ったり、データロスでヘッダの読み取り位置がずれると、例外吐いて落ちます。
ここは要改善ですね。


# 最後に
いかがだったでしょうか？
今回の記事が皆さんの開発の手助けになれば幸いです。