はじめに
Unity 5.1 よりマルチプレイヤ用のネットワークシステム(UNET)が追加されました。
古い Network 機能は今後 5.x のどこかのタイミングで廃止される予定です。
UNET は低レイヤのカスタマイズから、抽象化された高レベルな API 群、Unity のエディタ拡張から簡単に利用できるコンポーネント群およびプロファイラとの統合、そしてマッチメイキングといったマルチプレイヤゲーム向けのサービスまでを提供する、Unity 5 を代表する機能の一つです。ロードマップでは Phase.3 まで描かれていて、現在は Phase.1 が提供された段階です。
現状、英語・日本語ともに下回りを含め詳しく解説した文字ベースの資料がドキュメント以外にほとんどなく、ドキュメントはすごい良くまとまっているものの分量が多いので、どこを学習の基点としたら良いか決めるのが難しいと思います。そこで自身の備忘録も兼ねて一通り機能を調べてまとめてみました。機能がとても多いので全てはカバーできないですが、取り敢えず一通り眺めればドキュメントの読み方が分かるようなところを目指して、分かりやすそうな順で解説を書いています。私の理解を元に、ちょっと込み入ったところも書いてみました。間違っている所があればご指摘頂けると助かります。
ただし、UNET は現在も色々と変更が行われているようで、例えばドキュメントのコードから結構変更が行われていてそのままだと動かないものが多いので、今後、下記情報も古くなる可能性があることにご留意下さい。
環境
- Mac OS X 10.10.4
- Unity 5.1.2f1
デモ
試しに 3D チャットを作ってみました。さくらの VPS で Linux ビルドしたサーバを動かし、そこへ複数のクライアントからつなぐデモです。サーバ側で動いてる AI もいます。サーバ・クライアント共に同じプロジェクトをビルドしています。
どれくらいのクライアントの接続に耐えられるか、またいつ止まるかは不明ですが、デモアプリは以下からダウンロードできます。
教材
- マルチプレイヤーゲームとネットワーク - Unity マニュアル
- ドキュメント(現在未翻訳)。一通り書いてあるけど読むのが大変。
- 【Unit 5】Unityでマルチプレイヤーなゲームを作る入門(1) - テラシュールブログ
- Unity 5 UNET Multiplayer Tutorials - Making a basic survival co-op - Unity Forum
- チュートリアル動画が沢山(英語)。ひとつあたり数十分で 21 本。
- Part 1 の前半部が椿さんの記事とほぼ同じ感じです。
- Unity New Networking System (UNet) - Unite Asia 2015
- Unite Asia 2015 での資料。ドキュメントの図もここから多くを利用している?
- Democratizing Multiplayer Development - Unite Europe 2015 - YouTube
- Unite Europe 2015 での講演動画
- 【Unity9】UNETでマルチプレイヤーなオンラインゲーム開発【UNET1】 - Unity(C#)初心者・入門者向けチュートリアル ひよこのたまご
- チュートリアル動画の内容を図解付きで丁寧に記事にまとめてくださっています、多分今のところ日本語で一番詳しい。
- 手を動かしながら覚えたい人は、まずこちらを実践すると感触が掴めると思います。
サンプルプロジェクト
以下のスレッドで HLAPI / LLAPI のサンプルがいくつか配布されています。
以下はインベーダーゲームのサンプルのキャプチャです。
HLAPI / LLAPI
UNET には大きく分けて 2 種類のユースケースをカバーする機能を提供しています。
Getting Started
最初から各論に入ると分からなくなるので、まずは UNET の感触をつかむために、椿さんの記事の内容を元にその詳細を解説しようと思います。詳しくは椿さんのエントリを見ていただきたいのですが、箇条書きにすると以下のような手順になります。作業は5分位で出来る内容です。
- 適当にオブジェクトを配置
- シーンに
NetworkManager
およびNetworkManagerHUD
をアタッチした Game Object を配置 - Asset > Import Package > Characters をインポートして
FPS Controller
を配置 - 配置した
FPS Controller
にNetworkIdentity
およびNetworkTransform
をアタッチNetworkIdentity
のIs Player Authority
をチェックNetworkTransform
のTransform Sync Mode
をSync Character Controller
に変更
FPS Controller
にカメラなどリモートで不要なコンポーネントを disabled にするスクリプトDisableRemotePlayerBehaviours
をアタッチ(自前で用意、後述)して不要にするコンポーネントをインスペクタ上で登録FPS Controller
を Prefab 化NetworkManager
のSpawn Info
>Player Prefab
にFPS Controller
の Prefab を指定- ビルドして実行 & Editor 上でも実行して、Editor 上では
Lan Host(H)
を、ビルドしたアプリではLan Client(C)
をクリック - 片方を動かすともう一方で動く
using UnityEngine; using UnityEngine.Networking; // 新ネットワーク機能の名前空間 // NetworkBehaviour では isLocalPlayer などネットワーク系の // プロパティやメソッドにアクセスできる public class DisableRemotePlayerBehaviours : NetworkBehaviour { public Behaviour[] behaviours; void Start() { // 登録されたコンポーネントをリモート側で disabled にする if (!isLocalPlayer) { foreach (var behaviour in behaviours) { behaviour.enabled = false; } } } }
解説
雰囲気は掴めたと思うので、具体的に上記手順で何をしていたか、各コンポーネントがどういう役割をしているのか、順に見て理解を深めていきましょう。
Network Manager
- NetworkManager の使用 - Unity マニュアル
- http://docs.unity3d.com/ja/current/ScriptReference/Networking.NetworkManager.html
Network Manager
は HLAPI を使って作られたマルチプレイヤゲームで必要な機能群が一通り揃ったコンポーネントです。
以下の様な機能を提供してくれます。
- ネットワークでの役割の管理
- サーバかクライアントかホスト(サーバ + ローカルクライアント)のいずれか
Network Manager HUD
を同時にアタッチするとサンプルの UI が表示
- オブジェクト生成の管理
- 開始時に
Instantiate
するPlayer Prefab
を登録する - 動的に生成する Prefab を登録する
- 登録した Prefab を
NetworkServer.Spawn(GameObject)
に渡すと全クライアントで生成
- 開始時に
- シーンの管理
- オフラインシーンとオンラインシーンを管理
- オンラインになると自動でシーン遷移
- Build Settings で
Scenes In Build
にシーンを登録するとMenu Scene
とPlay Scene
にシーンをドラッグ&ドロップできるようになる
- デバッグ
- ネットワーク遅延やパケロスのシミュレーション
- コネクションの状態やローカル・リモートのオブジェクトのリストの表示
- マッチメイキング
- Unity Multiplayer サービスとの連携(後述)
- カスタマイズ
- いくつかの関数は virtual になっていて継承してカスタム出来る
- 例)
Player Prefab
生成時のOnServerAddPlayer()
NetworkLobbyManager
はこれを継承してロビーを作れるようにしたもの
先程の例では最初の2つの項目を利用した形になります。
NetworkIdentity
NetworkIdentity
はネットワーク同期でのコアとなるコンポーネントで、同期するオブジェクトには必ず付ける必要があります。ここにはネットワーク内で共通の Scene ID
(どのシーンに属するか)や Network ID
(ネットワーク内で一意に決まる ID)、Asset ID
(どのアセットを利用するか)や他のコンポーネントから利用する様々なフラグ(例えば localPlayerAuthority
は NetworkTransform
が参照)といった情報が含まれています。
これらは Inspector 下部もしくは Inspector のモードを Debug にすることで確認することが出来ます。
NetworkTransform
NetworkTransform
はその名の通りネットワーク内で Transform
コンポーネントを同期する役割をします。同期のモード(単純に Transform
の値を同期するか、RigidBody
に追従するか、Character Controller
に追従するかなど)や同期の頻度、補間の設定などが可能です。
NetworkTransformVisualizer
コンポーネントをアタッチして Visualizer Prefab
を設定すると生値と補間の様子を見ることが出来ます。
後述しますが、Photon の PhotonTransformView
の様に細かな補間の設定は出来ないようなので色々と調整したい場合は自前で書く必要があります。
NetworkAnimator
例では出てきませんでしたが、似たものに Animator
を同期する NetworkAnimator
もあります。
指定した Animator
の変数が自動で Inspector に出てきてチェックしたものを同期してくれます。ただあくまでも定期的な同期なので、すぐにかつ確実に同期が必要な場合、別途対応する必要があります。これは後述します。
サーバ・クライアント・ホスト
NetworkManager
のところでサーバ、クライアント、ホストに触れましたが、理解を深めるためにこれらについてちょっと見ておきましょう。
まず大前提として、UNET では同じゲームのコードでクライアント・サーバ共に動かしています。サーバ専用の言語を覚えたりする必要はありません。
UNET では基本的には一つのサーバに複数のクライアントがぶら下がる形になります。ただ専用のサーバがない場合はいずれかのクライアントがサーバの役割も担うことになります。これがホストです。ホストではサーバとクライアントが同じプロセスで動作(同じシーンやオブジェクトを共有)しています。
こうしてサーバに対してローカルなクライアントとリモートなクライアントが出来るのですが、プログラマはこれらのホストにローカルなクライアントかリモートなクライアントかを意識することなくプログラムできるようになっています。ただし、サーバかクライアントかといったことや、参照しているオブジェクトが各クライアントから見てローカルなのかリモートなのかは強く意識する必要があります。
例えば、先ほど操作するプレイヤにアタッチした NetworkIdentity
コンポーネントの Is Player Authority
にチェックを入れましたが、これによって各クライアント毎に自身のプレイヤの所有権が与えられ、isLocalPlayer
フラグが true になります。
ネットワーク間で動作するコンポーネント
さて、今度はコードからの利用を見て行きましょう。先ほどの NetworkTransform
の代わりになるようなものを書いてみます。簡単のために位置だけ同期するコードを書いてみます。
自前でネットワーク関連のコンポーネントを作成するには、MonoBehaviour
の代わりに、これを継承してネットワーク機能を付加した NetworkBehaviour
を継承します。NetworkBehaviour
は NetworkIdentity
と共に動作します。いくつか特殊な記法が存在します。
- Unity - Manual: NetworkBehaviour
- http://docs.unity3d.com/ja/current/ScriptReference/Networking.NetworkBehaviour.html
using UnityEngine; using UnityEngine.Networking; public class Player_SyncPosition : NetworkBehaviour { // SyncVar Attribute をつけたプロパティはネットワーク越しで共有される [SyncVar] private Vector3 syncPos; public float easing = 0.25f; // Unity Engine から呼ばれる関数(e.g. Start / OnCollisionEnter) に // ClientCallback Attribute をつけるとクライアント側だけで実行される(サーバ側は空実装) // 同様に ServerCallback Attribute もある [ClientCallback] void Update() { // サーバ側に現在位置を送信 if (isLocalPlayer) { TransmitPosition(); } else { LerpPosition(); } } // Client Attribute をつけると Client のみ実行される(サーバでは空実装になる) // 同様に Server Attribute もある [Client] void TransmitPosition() { CmdProvidePositionToServer(transform.position); } // サーバ側で実行されるコマンド // クライアント側からサーバ側へコマンドを送る時はこれが必要 // Command Attribute と Cmd-prefix な関数をセットで定義 [Command] void CmdProvidePositionToServer(Vector3 pos) { syncPos = pos; } void LerpPosition() { transform.position = Vector3.Lerp(transform.position, syncPos, easing); } }
これを NetworkTransfom
の代わりにアタッチすると、滑らか(目的位置に徐々に近づけるコードなのでキビキビでなくヌルッと動く)に位置が同期されます。
コメントにも解説を入れましたが、NetworkBehaviour
には特別ないくつかの Attribute やルールが存在し、これらを利用してネットワーク越しで情報をやり取りするコードを簡潔に書けるような仕組みが用意されています。一通り機能を見てみましょう。
- 変数の同期
SyncVar
アトリビュートをつけると値が同期される- ただし Primitive 型のみ(Vector3 等の Unity の Primitve 型も OK)
- 同期はサーバ → クライアントのみなのでサンプルコードのようにサーバに同期したい値を送る必要がある
- もしくは
hook
を使うとより簡単に記述できる
- もしくは
- 仕組みの詳細: UNET SyncVar | Unity Blog
- ネットワーク機能関連のコールバック
- いくつかの virtual 関数群が用意されている(override して利用、詳細はマニュアル参照)
OnStartServer
OnStartClient
OnSerialize
OnDeSerialize
OnNetworkDestroy
OnStartLocalPlayer
OnRebuildObservers
OnSetLocalVisibility
OnCheckObserver
- いくつかのコールバックは後で解説します
- いくつかの virtual 関数群が用意されている(override して利用、詳細はマニュアル参照)
- サーバ / クライアントでの関数の切り分け
- クライアントからサーバへのコマンドの送信
Cmd
から始まる関数にCommand
アトリビュートをつけるとサーバで実行されるクライアントから呼び出せる関数になる
- サーバからクライアントの RPC(リモートプロシージャコール)
Rpc
から始まる関数にClientRpc
アトリビュートをつけるとクライアントで実行されるサーバから呼び出せる関数になる
- ネットワーク越しのイベントの登録
Event
から始まるイベントにSyncEvent
アトリビュートをつけるとクライアントで呼び出されるイベントをサーバから発火出来るClientRPC
が単純な呼び出しに対しイベントを用意して他のスクリプトから利用することが可能- http://docs.unity3d.com/ScriptReference/Networking.SyncEventAttribute.html
これらをうまいこと利用してロジックを組んであげればゲームが出来るのが何となくイメージできるのではないでしょうか。チュートリアル動画ではこれらをうまく利用してゲームを作成しているのでサンプルとして見ると参考になると思います。
オブジェクトの "Spawn" を理解する
次に動的なオブジェクトの生成について見て行きましょう。
ドキュメントでは頻繁に "Spawn" という単語が出てきます。これは "Instantiate" とは区別して使われていて、"Instantiate" が Object.Instantiate()
によってオブジェクトを生成するのに対し、"Spawn" はネットワークに接続されたクライアント全てにおいてオブジェクトを生成することを意味しています。
UNET では、オブジェクトがサーバ上で変更されたり破棄されるとその通知が各クライアントへ伝わります。また、生成後に新しいクライアントがサーバに接続した際も、その新しいクライアント上で既に生成済みのオブジェクトが生成されます。
オブジェクトを "Spawn" するためには、対象のオブジェクトを NetworkServer.Spawn()
に渡す必要があります。NetworkServer
クラスはサーバの状態や機能をまとめたクラスです。
もちろん直接オブジェクトの参照をネットワーク越しに渡すことは出来ません。そこでこれが上手く動くためには、各クライアントで何のオブジェクトを生成するか各クライアントが把握している必要があります。この役割を果たすのが NetworkIdentity
の時に見た Asset ID
です。そして Asset ID
は事前に登録しておく必要があります。
登録する方法は NetworkManager
を利用している際はインスペクタから、それ以外はコードから行う必要があります。インスペクタから行う場合は NetworkManager
の Spawn Info
> Registered Spawnable Prefabs
に対象の Prefab を登録します。
コードで書く場合は ClientScene.RegisterPrefab()
で登録します。
例えばキャラクタから弾を発射してみます。NetworkManager
に弾の Prefab を登録し、以下の様なコードを書いてプレイヤにアタッチします。
using UnityEngine; using UnityEngine.Networking; using System.Collections; public class ShootBullet : NetworkBehaviour { public GameObject bulletPrefab; public KeyCode shootKey = KeyCode.Space; public float forwardSpeed = 10f; public float upSpeed = 5f; public float duration = 3f; [ClientCallback] void Update() { if (isLocalPlayer && Input.GetKeyDown(shootKey)) { var forward = Camera.main.transform.forward; var up = Camera.main.transform.up; var velocity = forward * forwardSpeed + up * upSpeed; CmdShoot(velocity); } } [Command] void CmdShoot(Vector3 velocity) { var bullet = Instantiate(bulletPrefab); bullet.transform.position = transform.position + velocity.normalized * 0.5f; var rigidbody = bullet.GetComponent<Rigidbody>(); if (rigidbody) { rigidbody.velocity = velocity; } NetworkServer.Spawn(bullet); StartCoroutine(DestroyBullet(bullet)); } [Server] IEnumerator DestroyBullet(GameObject bullet) { yield return new WaitForSeconds(duration); NetworkServer.Destroy(bullet); Destroy(bullet); // 要るか要らないかまだ不明... } }
生成・破棄をサーバ側で行うようにしています。サーバで Instantiate
して、NetworkServer.Spawn()
で全てのクライアントでも生成、時間が経ったら NetworkServer.Destroy()
で全てのクライアントから破棄しています。UNET ではこういった、クライアントからサーバへの命令なのか、サーバからクライアントへの命令なのかのルールを守る必要があります。
シリアライズ・デシリアライズ
SyncVars
SyncVar は前述の通りです。知らなくても全く問題ない内容(そうなるように頑張ってくれている)ですが、公式ブログにどうやって実装しているかの解説が書いてあります。
内部的にはパフォーマンスや帯域節約のために SyncVar
アトリビュートを適用した変数のうち、変更されたものに Dirty フラグをセットするようになっているのですが、この Dirty フラグを自動でセットするように変数をプロパティに置き換えるコードジェネレーションが内部で走っています。ユーザコードを大量に置換すると問題が起きやすいので、ここでは Mono.Cecil という IL レベルでコードをあれこれするライブラリを利用しています。
WebGL のコード変換プロセスもアレでしたが、Unity の中には黒魔術屋さんが沢山いそうですね。
SyncLists
話題が逸れたので戻していきます。SyncVar
は単一の変数にしか効きませんでしたが、リストで使いたい場合に組み込みの同期用リストがいくつか用意されています。
SyncListString
SyncListFloat
SyncListInt
SyncListUInt
SyncListBool
またユーザ定義型の構造体をリスト化出来る SyncListStruct<T>
も用意されています。
独自シリアライズ・デシリアライズ
NetworkBehaviour
のちらっと見ましたが、OnSerialize()
と OnDeSerialize()
というコールバックが NetworkBehaviour
の virtual 関数として用意されています。ここでは複雑なシリアライズ・デシリアライズの記述が可能です。が、自前で Dirty フラグを意識したりと結構大変そうです。以下のマニュアルの最下部にサンプルコードが載っています。
Network Message
Send()
系の関数が NetworkServer
、NetworkClient
、NetworkConnection
に実装されています(NetworkConnection
は NetworkClient
なら一つ、NetworkServer
なら複数持っている各接続をまとめたクラス)。引数に MessageBase
を継承したクラスのインスタンスを指定して使う形になります。
- Unity - Manual: Network Messages
- NetworkServer | Unity Scripting API
- NetworkClient | Unity Scripting API
- NetworkConnection | Unity Scripting API
- MessageBase | Unity Scripting API
コードとしては、 Serialize(NetworkWriter)
と Deserialize(NetworkReader)
を継承するクラスを作成することで複数のパラメタをパックすることが出来ます。メッセージを受けとってゴニョゴニョ処理するような場合や SyncVar
が対応していない型(Byte Array 等)を送りたいときなど、SyncVar
ではカバーできないケースに使うと良いと思います。
また、EmptyMessage
、IntegerMessage
、StringMessage
は予め用意されています。
- EmptyMessage | Unity Scripting API
- IntegerMessage | Unity Scripting API
- StringMessage | Unity Scripting API
余談ですが、ものによって DeSerialize だったり Deserialize だったりするのは修正されるのかな...。
Channel / QoS
概要
これまで色々と見てきましたが、SyncVar
や Send
はどういった通信路を経由して送るのかお任せの状態でした。例えば位置や姿勢は 100 回に 1 回メッセージが届かなかったとしても特に影響はありませんが、ダメージやステートなどが同期されないと、あるクライアントでは敵が生きていて別のクライアントでは死んでる、みたいな状態が起こってしまいます。
UNET ではどういった通信路を利用して同期したりメッセージを送りあったりするかを指定する Channel を複数本用意することができ、それぞれの Channel に QoS を指定できるようになっています。QoS は Quality of Service のことで、一般的には送信するデータの扱い・品質を意味します。
UNET では以下の QoS が用意されています(参考: All about the Unity networking transport layer | Unity Blog)。
- Unreliable
- パケロスの可能性がある
- 速い
- UnreliableFragmented
- Unreliable + 一回のデータ量上限が決まっている
- 長いログなど
- UnreliableSequenced
- Unreliable + 順序が保証されている
- 映像や音声など
- Reliable
- パケロスしない
- 遅い
- ダメージやステートなど
- ReliableFragmented
- Reliable + データ量上限
- グループ化されたメッセージなど
- ReliableSequenced
- Reliable + 順序保証
- ファイルの転送など
- StateUpdate
- Unreliable + 古いデータは破棄
- 位置の同期など
- AllCostDelivery
- Reliable が RTT に応じて再送するのに対し、一定間隔で再送
- ショットの発射など
これらは NetworkManager
を利用している場合は、Advanced Configuration
の QoS Channel
から設定できます。
利用方法
コードからはアトリビュートの引数として Channel を指定できます。スクリプト単位で指定する場合は NetworkSettings
アトリビュートを使用します。
using UnityEngine.Networking; [NetworkSettings(channel=1,sendInterval=0.2f)] class MyScript : NetworkBehaviour { [SyncVar] int value; }
ここでは同時に同期の間隔(sendInterval
)も指定できます。これらの設定は Inspector に表示されます。
関数ごとには Command
や ClientRPC
といったアトリビュートの引数で指定できます。
public class Player : NetworkBehaviour { // ... [Command(channel=1)] public void CmdMove(int x, int y) { moveX = x; moveY = y; isDirty = true; } }
こういった細かなチューニングがより良いゲーム体験には必要になってきます。
プロファイラ
UNET では段階的にですがプロファイラとの統合が図られています。現在は以下の 2 つの機能がプロファイラと統合されています。詳細は未だドキュメント化されていないですが、どれだけパケットが流れているか、どのパケットが支配的になっているかといったことが確認可能です。グラフをクリックするとクリックした箇所の詳細が下部に表示されます。
Network Messaging
入出するパケットの流れを見ることが出来ます。
Network Operations
どのタイミングでオブジェクトの生成・破棄が起きているか、Command
や ClientRPC
がどれだけコールされているか、またそのコールされた関数は何か、といったことが確認できます。
オブジェクトの Visibility 制御
パフォーマンスの話が続きます。ゲームが広いエリアで沢山のネットワーク関連のオブジェクトが接続されている場合、全てのクライアントに対して全てのオブジェクトを Spawn していると、レンダリングコストもかかりますし、沢山の帯域を消費してしまいますし、新しくユーザが参加した場合も全てのオブジェクトを Spawn するのに時間がかかってログイン時間が長くなってしまったりと色々と悪影響が出てきます。
NetworkProximityChecker
そこで、UNET では NetworkProximityChecker
というコンポーネントが用意されていて、これを利用すると設定した距離以上離れると以下のように動作します。
- ホストと同じローカルクライアントの場合
Renderer
が disabled になる
- リモートクライアントの場合
Destroy
される- 新しく接続した場合、範囲外なら Spawn しない
仕組みとしては Physics を利用しているので、Check Method
で Physics3D
か Physics2D
どちらでチェックするか選択する必要があります。その上で Vis Range
よりも離れると hidden になるという感じです。Force Hidden
はプレイヤオブジェクトだと通常は hidden にならないので、無理やり hidden にしたい場合にチェックします。
ただ、現在のところ用意されたコンポーネントはバグが有る(ArgumentOutOfRangeException
が発生する)ようで、ワークアラウンドで以下のように継承して利用する必要があるようです。
using UnityEngine; using UnityEngine.Networking; public class ProximityChecker : NetworkProximityChecker { public override bool OnCheckObserver(NetworkConnection newObserver) { return false; } }
ホストでの Visibility の取り扱い
上述したように、ホストでは全てのオブジェクトを管理する必要が有るため、Renderer
が disabled になるだけです。ただ、ものによっては同期する必要のない重いスクリプトが付いている場合があります。これらは NetworkBehaviour
の OnSetLocalVisibility(bool)
を利用することで制御することが可能です。
カスタマイズ
NetworkProximityChecker
が内部的に何をしていてどうすればカスタマイズ出来るか見て行きましょう。まず、オブジェクトの Visibility の管理は全てサーバ側で行われることを覚えておいて下さい。
NetworkProximityChecker
は定期的に NetworkIdentity.RebuildObservers()
という関数を呼び、この結果 NetworkBehaviour.OnRebuildObservers(HashMap<NetworkConnection> observers, bool initial)
というコールバックが呼ばれます。
この中で近接判定を行います。ここで言う Observer とは各プレイヤのことです。つまり RebuildObservers
とは誰が見えているかという情報を更新してくれ、という命令になります。この情報というのが HashMap<NetworkConnection>
で、ここに自分が誰から見えているか詰める、というのが近接判定の作業になります。
Observer はプレイヤと言いましたが、具体的にクラスで言うと NetworkConnection
のことです。NetworkServer.connections
に全てのクライアントに対してのコネクションが入っているのですが、これとは別にサーバ側では各プレイヤのオブジェクトにアタッチされた NetworkIdentity
の connectionToClient
に、各クライアントへのコネクションが格納されています。これを利用してプレイヤを判定するというのが具体的なコードになります。
以下、NetworkProximityChecker
を模倣して書いたスクリプトになります。
using UnityEngine; using UnityEngine.Networking; using System.Collections; using System.Collections.Generic; public class CustomProximityChecker : NetworkBehaviour { private NetworkIdentity netId_; public float interval = 1f; public float range = 2f; [Server] public override void OnStartServer() { netId_ = GetComponent<NetworkIdentity>(); StartCoroutine(CheckProximityPeriodically()); } [Server] IEnumerator CheckProximityPeriodically() { bool isInitial = true; for (;;) { yield return new WaitForSeconds(interval); // 全ての OnRebuildObservers を呼ぶ netId_.RebuildObservers(isInitial); isInitial = false; } } [Server] public override bool OnCheckObserver(NetworkConnection newObserver) { // 新しくユーザが接続した時に呼ばれる。 // true を返すとそのユーザのシーンに Spawn し、false だと何もしない return false; } [Server] public override bool OnRebuildObservers(HashSet<NetworkConnection> observers, bool initial) { // このスクリプトがアタッチされているオブジェクトが各プレイヤから見えていたら // observers にそのプレイヤに該当する NetworkConnection を格納する // その結果に応じて Spawn や Destroy が各プレイヤのシーンに対して行われる var hits = Physics.OverlapSphere(transform.position, range); foreach (var hit in hits) { var netId = hit.GetComponent<NetworkIdentity>(); bool isPlayer = (netId != null) && (netId.connectionToClient != null); if (isPlayer) { observers.Add(netId.connectionToClient); } } return true; } }
ちなみに OnRebuildObservers()
や OnCheckObserver()
は全ての関連した NetworkBehaviour
に対して呼ばれるので、別のクラスに分離しても構いません。サンプルコードは Sphere で見ていましたが、オクルージョンによって判定したりエリアによって判定したり、自分なりのルールをここに付け加えれば、各クライアント毎の処理も減り、その結果やりとりするメッセージも減って帯域も節約できます。
マッチメイキング
概要
いよいよマッチメイキングについて見て行きましょう。
UNET ではマッチメイキングとリレーサーバをサービスとして提供してくれています。
プレイヤはルームを作成して、別のプレイヤはそのルームを検索、参加する、ということが可能になり、お互いに IP を知らなくとも一緒にゲームをプレイできるようになります。現在はプレビュー版で 100 CCU(Concurrent User)までテストできます。
NetworkManager
を利用してマッチメイキングを行うと、自動的に UNET のリレーサーバをパケットが経由するようになるため、これによって Firewall や NAT 越えの心配をする必要がなくなります。
マッチメイキングしてみる
まずは登録して試してみましょう。手順は以下のエントリが詳しいです。
登録後、Player Settings の Cloud Project Id
に作成したプロジェクトの ID を登録すれば OK です。NetworkManager
にマッチメイキングの機能が備わっているので、これを利用すると異なるネットワークからお互いの IP を知ることなく Unity のリレーサーバ経由でマッチングすることが可能です。
コードから制御する
マッチメイキングを制御するには NetworkMatch
を利用します。
マニュアルのように自分でコールバック含め作成しても良いのですが、NetworkManager
にも機能が備わっているのでそちらを参考に書いてみます。
using UnityEngine; using UnityEngine.Networking; using UnityEngine.Networking.Match; using System.Collections; public class NetworkManagerTest : MonoBehaviour { private NetworkManager manager_; private NetworkMatch match_; public string matchName = "hogehoge"; public uint matchSize = 4U; void Start() { manager_ = GetComponent<NetworkManager>(); } void Update() { if (Input.GetKeyDown(KeyCode.S)) { Debug.Log("Start Match Maker"); manager_.StartMatchMaker(); manager_.matchName = matchName; manager_.matchSize = matchSize; match_ = manager_.matchMaker; } if (match_ != null && Input.GetKeyDown(KeyCode.C)) { match_.CreateMatch(manager_.matchName, manager_.matchSize, true, "", manager_.OnMatchCreate); } if (match_ != null && Input.GetKeyDown(KeyCode.L)) { Debug.Log("List Matches"); match_.ListMatches(0, 20, "", manager_.OnMatchList); } if (match_ != null && Input.GetKeyDown(KeyCode.J)) { Debug.Log("Join Match"); var desc = manager_.matches[0]; // join first room match_.JoinMatch(desc.networkId, "", manager_.OnMatchJoined); } } }
大変雑ですが、これを NetworkManager
と同じ Game Object に取り付け、ホスト側ではサーバからのレスポンスがあったら「S > C」の順でキーを押下、クライアント側では「S > L > J」すると作成したルームに参加することが出来ます。さすがにこのままだとあれなので、この機能を呼ぶように適当に UI を作れば OK です。
Network Transport Layer (LLAPI)
最後に LLAPI です。LLAPI はシステムのソケットの上に乗る薄いレイヤです。
- Unity - Manual: Using the Transport Layer API
- All about the Unity networking transport layer | Unity Blog
- NetworkTransport | Unity Scripting API
サンプルプロジェクトも上がっているので、気になる方は見てみると面白いと思います。
まだ余り情報がないのと、私がネットワーク周りに疎いので、どなたか詳しく解説してくださると嬉しいです。。
その他
その他気になりそうな点です。
WebGL への対応
LLAPI で対応してますが、HLAPI では今のところ対応していません。
- http://forum.unity3d.com/threads/unet-and-webgl-authoritative-server.329972/:tit;e
- UNet and WebGL? - Unity Forum
現在対応中とのことです。WebRTC data channel も対応してくれないかな...。
ベータ機能
5.2b では、いくつかのバグ修正と、NetworkTransform
の改善、ローカルでの Discovery、ノンプレイヤなオブジェクトの Authority の設定などが含まれているようです。
専用サーバ
ホスト前提のゲームでなく、MMO の様にどこかでサーバが動いていて、みんながそこにアクセスするようなことをしたい場合は、Linux ビルドしたアプリをバッチモードでどこかのサーバでヘッドレスに起動して運用するのが良いのではないかと思います。
コンソールから -batchmode
引数をつけるか、Linux のビルドオプションに Headless Mode
があるので、それを指定してサーバ側で起動するのでも良いと思います。
冒頭のデモはこの運用でサーバがさくらの VPS 上で動いています。同じプロジェクトのビルドでサーバ・クライアント両方動くのはスゴイですね。多分 Android や iOS ビルドしても動くのではないでしょうか。
ただちょっとコードに気を使わないと色々と破綻してしまう(例えばホストでテストしているとクライアントの動作も含むため、サーバ単体で動かして初めてミスに気づくなど)ので注意が必要です。
設定
Project Settings に Network の項目が追加されています。現状は Debug Level
と Sendrate
のみ指定できます。
Animator の即時同期
NetworkAnimator
では素早いアクションなどの同期に向いていないので、そういう場合は自前でやり取りする必要があります。そこで、SyncVar
とそのアトリビュートの hook
がとても役立ちます。
public class AnimationTriggerSync : NetworkBehaviour { // ... // この SyncVar を通じて各クライアントに変更を通知する [SyncVar(hook = "OnIsJumpChangedForRemoteClient")] private bool isJump_ = false; // 1. ここが起点 // 各クライアントは自分のアニメーションのフラグをすぐにセット // その他のクライアントも同期するためにまずはサーバのフラグをセット [Client] public void SetIsJumpForLocalClient(bool isJump) { if (isLocalPlayer) { animator_.SetBool("isJump", isJump); CmdProvideIsJumpToServer(isJump); } } // 2. サーバでフラグをセットする // これにより hook で設定された関数が各クライアントで呼ばれる // channel の QoS は Reliable State Update がおすすめ [Command(channel = 2)] private void CmdProvideIsJumpToServer(bool isJump) { isJump_ = isJump; } // 3. 各クライアントでフラグをセットする // ローカルなクライアントにも通知されるが、すでにセット済みなので、 // リモートなクライアントだけ Animator にフラグをセットする [Client] private void OnIsJumpChangedForRemoteClient(bool isJump) { if (!isLocalPlayer) { animator_.SetBool("isJump", isJump); } } // ... }
これでローカルなプレイヤはネットワークに繋がっていない時と同じように動き、リモートなクライアントは最小限の時間で同期されます。省略しますが、トリガの場合は適当に int をインクリメントしてフックするとか、Command
して ClientRPC
するか、キー判定をサーバ側でやって ClientRPC
するかのいずれかになると思います。
開発
クライアントのテストをするためにいちいちビルドをしていると大変なので、エディタを複数立ち上げる方式が便利そうです。
おわりに
一通り機能を見てみましたがいかがだったでしょうか。概要さえ掴めてしまえば予備知識無しでは難解だったドキュメントも読めるようになるのではないかと思います。
現状の UNET はホストを前提とした設計になっているのですが、デモの様にサーバを立ててしまえば今後様々なユースケースに対応してくれると考えています(サービス側で dedicated server を動かすとか...)。いまのところ Phase.2 の情報が少なくてなかなか読めないところはありますが、今後のアップデートも注目していきます。