PONOS Advent Calendar 2019の17日目の記事です。
はじめに
2019年も僅かとなり、ふとソーシャルゲーム関連のサーバサイド担当してしばらく経ちますが、リアルタイム通信というものは1から設計する経験がなかったことを思い出しました。
令和にもなったことですし、自分さらなるスキルアップを目指して、リアルタイム通信を1から設計しオンラインゲームを作成しスキルアップを目指していくためにPonosAdventCalenderの2回の投稿で、技術選定から実際にクライアント側が情報をリアルタイムに通信させゲームっぽいなにかができるところまでいけたらと考えています。間違っていることも記述してしまうかもしれませんがコメントで訂正していただけると助かります。
リアルタイム通信の実装方針
**リアルタイム通信を実現するには、自分が操作した状況や状態を他のクライアントに即時に伝達する必要があり、通信遅延がゲーム性に影響を与えないように常時接続型の通信であるべきです。**そこに焦点を当て必要な技術を選定し実装方法を決めていきます。また、クライアント側の実装は弊社でも使用しているゲームエンジンのUnityを前提とします。
技術選定
リアルタイム通信を実装するにあたって双方向通信、常時接続型、低遅延というのを考えなくてはいけません。
また伝達方式はネットワーク構造はシンプルで構築することが可能で、サーバでロジックを容易な更新やチェックすることでチートを防ぐこともできるクライアント・サーバ方式を採用していきたいと考えています。
その場合は、サーバを経由する通信オーバーヘッドが存在します。
そこで選定した通信規格はWebSocketで、それを使用して実装するプログラミング言語はRustで実現してみたいと考えました。WebSocketは、双方向の通信セッションを開くことを可能にする高度な通信規格であり実装も容易になっています。
またRustは、C言語の速度を保証するプログラミング言語でなお安全性も確保されており、リアルタイム通信を実装するにあたって良い選択肢になっているかと考えています。
WebSocket
コンピュータネットワーク用の通信規格の1つであり、ウェブアプリケーションにおいて、双方向通信を実現するための技術規格です。
サーバとクライアントが一度コネクションを行った後は、必要な通信を全てそのコネクション上で専用のプロトコルを用いて行います。従来の手法に比べると、新たなコネクションを張ることがなくなります。
HTTPコネクションとは異なる軽量プロトコルを使うなどの理由により通信ロスが減少したり、一つのコネクションで全てのデータ送受信が行えるため同一サーバに接続する他のアプリケーションへの影響が少ないなどのメリットがあります。
Rust
Rustは、Mozillaが支援するオープンソースのシステムプログラミング言語です。速度、並行性、安全性を言語仕様として保証するC言語、C++に代わるシステムプログラミングに適したプログラミング言語を目指しています。また安全性を確保するために言語仕様は複雑になっています。
また、RustでもWebSocketのライブラリが用意されており今回は、WS-RSを使用します。
WS-RSの公式サイト情報ですが、C言語のWebSocketのライブラリであるlibwebsocketsよりパフォーマンスが優れているということでした。
Library | Time(ms) |
---|---|
WS-RS | 1,709 |
libwebsockets | 2,067 |
rust-websocket | 8,950 |
websockets CPython 3.4.3 | 12,638 |
Autobahn CPython 2.7.10 | 48,902 |
NodeJS via ws | 127,635 |
※ 上記の比較表はWS-RSの公式サイトが算出したものです。
※ websockets CPython 3.4.3はいくつかのパイプ破損エラーが発生しました。
※ NodeJS via wsは複数の(229)接続タイムアウトエラーが発生しました。
実装
RustでWebSocketを実現したサーバとUnityで実装したクライアントと通信をする仕組みを構築していきます。実装環境としてはローカル(macOS)で実施することを前提しています。
開発環境 | バージョン |
---|---|
macOS | 10.14.1 |
Rust(rustc、cargo) | 1.39.0 |
WS-RS | 0.9.1 |
Unity | 2019.3.0f1 |
サーバ側の実装
-
Rustのインストール
$ curl https://sh.rustup.rs -sSf | sh
-
環境変数をプロファイルに登録(zshを使用している場合)
$ vi ~/.zshrc export CARGO_HOME="$HOME/.cargo" export PATH="$CARGO_HOME/bin:$PATH”
-
新規プロジェクトを作成
$ cargo new ws_hello_world --bin
-
必要なパッケージを導入する
$ cd ws_hello_world $ vi Cargo.toml [dependencies] env_logger = "0.7.1" ws = "0.9.1"
-
WS-RSのサンプルコードを記述する
$ vi src/main.rs
を実行し編集します。main.rsextern crate ws; extern crate env_logger; use ws::listen; fn main() { // ロガーの初期化 env_logger::init(); // アドレスをlisten(パッシブオープンを開始)し、接続ごとにクロージャーを呼び出す if let Err(error) = listen("127.0.0.1:3012", |out| { // ハンドラーは所有権を取得する必要があるため、move(クロージャに環境の所有権を取得することを強制する)を使用する move |msg| { // この接続で受信したメッセージを処理する println!("Server got message '{}'. ", msg); // 出力チャネルを使用してメッセージを送り返す out.send(msg) } }) { // 障害を通知 println!("Failed to create WebSocket due to {:?}", error); } }
-
実行
$ cargo run
※ 実行した際に不足しているパッケージは自動でビルドされます。
クライアント側の実装
-
C#用のWebSocketライブラリをダウンロード
githubにあるwebsocket-sharpをクローンまたはzipファイルでのダウンロードを実施します。 -
C#用のWebSocketライブラリのビルド
クローンまたはダウンロードしたwebsocket-sharpディレクト配下にあるwebsocket-sharp.slnをVisual Studioで読み込みます。Solutionにある4つのExampleを全て削除します。以下、画像のようにビルド方法をReleaseに変更し、[ビルド] > [全てをビルド]を実行します。 -
Unityの新規プロジェクトにビルドしたWebSocketライブラリを読み込む
Unityで新規プロジェクトを作成し、ビルドしたWebSocketライブラリをAssets配下に設置(画像ではAssets/Plugins配下に設置しています)します。 -
WebSocketに疎通する仕組みの実装
UnityからWebSocketに疎通する仕組みを実装します。新規でGameObjectとそれにアタッチするスクリプトを作成します。そのスクリプトには以下のような実装をします。WebSocketTest.csusing UnityEngine; using WebSocketSharp; public class WebSocketTest: MonoBehaviour { WebSocket ws; void Start() { // 接続先のURLとポート番号を指定する ws = new WebSocket("ws://localhost:3012/"); // WebSocketの接続を開始した時に実行されるイベント ws.OnOpen += (sender, e) => Debug.Log("WebSocket Open"); // メッセージを受信した時に実行されるイベント ws.OnMessage += (sender, e) => Debug.Log("WebSocket Message Data: " + e.Data); // 接続に失敗した時に実行されるイベント ws.OnError += (sender, e) => Debug.Log("WebSocket Error Message: " + e.Message); // 接続を終了した時に実行されるイベント ws.OnClose += (sender, e) => Debug.Log("WebSocket Close"); // WebSocketの接続を開始 ws.Connect(); } void Update() { // sキーを押下したときにメッセージを送信する if (Input.GetKeyUp("s")) { ws.Send("Test Message"); } } void OnDestroy() { // ゲームオブジェクトが削除されたときにWebSocketの接続を閉じる ws.Close(); ws = null; } }
動作確認
上記で貼り付けている動画のように、クライアント側がSキーを押すたびにメッセージを送り、そのメッセージを受信していることが確認できます。
おわりに
今回、RustとWebSocketでリアルタイム通信のHello World的なことを実施しました。今回Rustの言語を扱う当たって言語仕様を調べていましたが結構大変な感じがしています。
ただこれを扱えることによってC言語の速さとC言語とは比べ物にならない安全を得ることができるのでこれからも扱っていきたいと考えています。またリアルタイム通信について様々調べておりまして、今回WebSocketを使用しましたが、いずれは独自プロトコルで実現できたらという願望が芽生えております。
さて、この記事の続きになりますが、PONOS Advent Calendar 2019の22日目に投稿する予定です。続きではUnityを複数台接続してゲームオブジェクトのXYZ座標を共有できるようなサーバ側、クライアント側の実装をしていきたいと思います。
おまけ
今回リアルタイム通信を実施するにあたって以下書籍を購入させていただきました。ネットワークの基本的なとこから解説が入っているので読みやすいです。読み進めていくうちにオンラインゲームのネットワークアーキテクチャを詳しく解説しているので非常にためになる書籍です。
もしみなさんもオンラインゲームのバックエンドを実施したいという方がいらっしゃいましたらぜひ購入してみてはいかがでしょうか。
オンラインゲームを支える技術 --壮大なプレイ空間の舞台裏 (WEB+DB PRESS plus) 中嶋 謙互
https://www.amazon.co.jp/dp/4774145807/ref=cm_sw_r_tw_dp_U_x_iSF8Db3RKYFSJ