Siv3D + Ruby で通信ゲームを作る #2 入力の同期
Siv3D + Ruby で通信ゲームを作ろうの第2回です。
今回からいよいよ実際にプログラミングをしていきます。
難しいことはしませんが、Siv3DやRubyの細かい文法などは説明しませんので、適宜調べてください。
(RubyをC++風に書いています。Rubyの文法が好きな方は申し訳ありません。)
雛形となるゲーム
今回はこのゲームを通信対戦ゲームにしようと思います。
起動すればわかりますが、シーンを2つ遷移した後、2人で打ち合うブロック崩しのようなゲームが始まったと思います。今は一人で2つとも操作していますが、これを2人で操作しするように改造します。
その前に、こんなにも簡単なゲームですが、考えられる状態が3つあります。
- サーバに接続しようとしている状態
- サーバと接続できたが、対戦相手がいない状態
- 対戦相手とゲームをしている状態
なので、これらをそれぞれConnectingクラス、Waitingクラス、Gameクラスに分けて実装していきます。
サーバとの接続
まずはサーバと接続します。
その前に、Siv3DにはTCP通信を行うモジュールが用意されていますが、これだとRubyと通信する上で使いづらいので、このTCPStringを使用します。
では、いよいよサーバと接続します。
Main.cppがSiv3D、main.rbがRubyのプログラムになります。rubyコマンドでmain.rbを動かしたら、ウィンドウを閉じずにSiv3D側を起動してみてください。
ConnectingクラスでTCPStringClient::connect関数を呼び出し、サーバと接続しています。接続はいつ切れるかわからないので、各シーンで必ず接続が切れた時の挙動を記述してください。
この「127.0.0.1」というのが特別なIPアドレスで自身のパソコンを指します。もし、複数台のPCで試したいときは、ここをサーバが動いているPCのIPアドレスに変えてください。
もう一つ指定されている「50000」という数字はポート番号と言って、どのアプリの通信なのかを識別するIDのようなものになります。何でもいいですが、ウェルノウンポートだけは避けてください。
Ruby側ではTCPServer::openメソッドを使用し、他のアプリケーションからの接続を待ち受けます。
acceptメソッドのところで実際に接続が来るまで止まり、接続が来るとそのソケットを返して再び動き出します。
接続が確立すると、Ruby側から「welcome\n」という文字列を送り、getsメソッドで文字列が1行来るまで待機します。受け取ると、それをそのままwriteメソッドで返すプログラムとなっています。
まだ送受信はしていませんが、サーバとの接続が完了しました。
入力の送受信
同期の方法にもいろいろありますが、今回は入力を同期する方法でいきます。
状態そのものを同期している訳ではないので、各クライアントでずれる可能性がありますが、今後複雑になって来ても同期するのは入力だけでいいというメリットがあります。
いきなり2人は大変なので、まずは1人の入力をサーバに送信、返ってきたものを使ってバーを動かしてみます。
Main.cppの他の部分とmain.rbには変更はないです。
取り扱いやすいように、文字列として送信します。getsメソッドは改行までを読み込むので、最後に改行を付加するのを忘れないでください。
受信する時はTCPStringClient::readLine関数を使用します。末尾に改行がついているのでString::pop_back関数で末尾の改行を削除した後、Parse関数でint型に変換します。
ちなみに、whileで回しているのは1つのフレームで複数個受信する可能性があるためです。即時性が大切なため、受信したものは全てそのフレーム中で処理してしまいます。
2人対戦への対応
いよいよ2人対戦へ対応させます。
対戦するとなると、各クライアントを識別する必要が出てきます。今回は2人しかいないので、先に来た方を親、後から来た方を子とし、それぞれ左のバー、右のバーを操作させます。
今回からクライアントを2台起動する必要があります。手っ取り早く2台起動するには、Visual Studioのデバッグで起動されるやつではなく、配布する時に使用するexeファイルを2回起動させてください。
一気に増えましたが、やっていることは単純です。
まず、サーバから見ていきます。
今まではacceptメソッドを1回だけ呼び出していましたが、今回は2人分呼び出し、それぞれにparent、childと名前を付けます。2人揃った時に「start\n」を送信し、さらにparentにだけ「isParent\n」を送信します。
その後は2つのクライアントを監視するので、スレッドを2つに分けます。
それぞれのスレッドで1行受信したら頭にparentから来たなら「parent,」、childから来たなら「child,」という文字列を付加してparent、child両方に送ります。
最後にRubyではスレッドを作ってもメインスレッドが終了すると作ったスレッドも終了してしまうので、全部がjoinするまで待ちます。
次にクライアントです。
サーバと接続出来ても、対戦相手がいなければ意味がないので、「start\n」という文字列が来るまでWaitingクラスで待ちます。
Gameクラスでは読み込んだ一行を「,」で区切り、最初の要素が「isParent」なら自身を親とし、「parent」なら右(親)のバーのスピードを変更し、「child」なら左(子)のバーのスピードを変更します。
これで、親のクライアントからの入力はサーバによって「parent,」が付加されているので、親と子の左のバーが親の入力によって動きます。子についても同様です。
なぜ、一度サーバに送信するか?
ここでよくよく考えると、自分の入力にも関わらず、そのまま動かさずに一度サーバに送信しているのがわかると思います。
この理由は下の図を見るとわかります。
これがMain.cppとは違い、自身への入力をそのまま反映し、相手にだけ送信したパターンです。
どれだけ短くても送受信には時間がかかります。仮にサーバ ⇔ 各クライアント間にt秒かかるとすると、相手だけこちらから2t秒ずれて動くようになってしまいます。
これが、Main.cppが行っている一度サーバに送信したパターンです。
サーバからは同時に送って来るので、どちらもt秒遅れて反映されます。そもそもサーバにはt秒遅れて送信されているので、実際の入力から「互いに」2t秒遅れたことになります。
自分の入力も2t秒遅れたら気持ち悪いじゃないかと思うかもしれませんが、意外とこれはバレません。
このように通信関係では、いかにプレイヤーを「だます」かが重要になってきます。
まとめ
文字が多くなってしまいましたが、理解していただけたでしょうか?
ここで、入力は同期できましたが、ゲームの開始がずれていたり、ボールが飛んでっても再配置されなかったりとまだゲームとしては不十分なのがわかると思います。
次回はこの入力以外の同期方法についてやろうと思います。