凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity 5.1 から導入された新しいネットワーク機能の UNET について詳しく調べてみた

はじめに

Unity 5.1 よりマルチプレイ用のネットワークシステム(UNET)が追加されました。

古い Network 機能は今後 5.x のどこかのタイミングで廃止される予定です。

UNET は低レイヤのカスタマイズから、抽象化された高レベルな API 群、Unity のエディタ拡張から簡単に利用できるコンポーネント群およびプロファイラとの統合、そしてマッチメイキングといったマルチプレイヤゲーム向けのサービスまでを提供する、Unity 5 を代表する機能の一つです。ロードマップでは Phase.3 まで描かれていて、現在は Phase.1 が提供された段階です。

現状、英語・日本語ともに下回りを含め詳しく解説した文字ベースの資料がドキュメント以外にほとんどなく、ドキュメントはすごい良くまとまっているものの分量が多いので、どこを学習の基点としたら良いか決めるのが難しいと思います。そこで自身の備忘録も兼ねて一通り機能を調べてまとめてみました。機能がとても多いので全てはカバーできないですが、取り敢えず一通り眺めればドキュメントの読み方が分かるようなところを目指して、分かりやすそうな順で解説を書いています。私の理解を元に、ちょっと込み入ったところも書いてみました。間違っている所があればご指摘頂けると助かります。

ただし、UNET は現在も色々と変更が行われているようで、例えばドキュメントのコードから結構変更が行われていてそのままだと動かないものが多いので、今後、下記情報も古くなる可能性があることにご留意下さい。

環境

デモ

試しに 3D チャットを作ってみました。さくらの VPSLinux ビルドしたサーバを動かし、そこへ複数のクライアントからつなぐデモです。サーバ側で動いてる AI もいます。サーバ・クライアント共に同じプロジェクトをビルドしています。

f:id:hecomi:20150814215723p:plain

どれくらいのクライアントの接続に耐えられるか、またいつ止まるかは不明ですが、デモアプリは以下からダウンロードできます。

教材

サンプルプロジェクト

以下のスレッドで HLAPI / LLAPI のサンプルがいくつか配布されています。

以下はインベーダーゲームのサンプルのキャプチャです。

f:id:hecomi:20150811194006p:plain

HLAPI / LLAPI

UNET には大きく分けて 2 種類のユースケースをカバーする機能を提供しています。

f:id:hecomi:20150808150902p:plain

Getting Started

最初から各論に入ると分からなくなるので、まずは UNET の感触をつかむために、椿さんの記事の内容を元にその詳細を解説しようと思います。詳しくは椿さんのエントリを見ていただきたいのですが、箇条書きにすると以下のような手順になります。作業は5分位で出来る内容です。

  • 適当にオブジェクトを配置
  • シーンに NetworkManager および NetworkManagerHUD をアタッチした Game Object を配置
  • Asset > Import Package > Characters をインポートして FPS Controller を配置
  • 配置した FPS ControllerNetworkIdentity および NetworkTransform をアタッチ
    • NetworkIdentityIs Player Authority をチェック
    • NetworkTransformTransform Sync ModeSync Character Controller に変更
  • FPS Controller にカメラなどリモートで不要なコンポーネントを disabled にするスクリプト DisableRemotePlayerBehaviours をアタッチ(自前で用意、後述)して不要にするコンポーネントをインスペクタ上で登録
  • FPS Controller を Prefab 化
  • NetworkManagerSpawn Info > Player PrefabFPS 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;
            }
        }
    }
}

f:id:hecomi:20150807194437p:plain

解説

雰囲気は掴めたと思うので、具体的に上記手順で何をしていたか、各コンポーネントがどういう役割をしているのか、順に見て理解を深めていきましょう。

Network Manager

f:id:hecomi:20150808151226p:plain

Network Manager は HLAPI を使って作られたマルチプレイヤゲームで必要な機能群が一通り揃ったコンポーネントです。

以下の様な機能を提供してくれます。

  • ネットワークでの役割の管理
    • サーバかクライアントかホスト(サーバ + ローカルクライアント)のいずれか
    • Network Manager HUD を同時にアタッチするとサンプルの UI が表示
  • オブジェクト生成の管理
    • 開始時に Instantiate する Player Prefab を登録する
    • 動的に生成する Prefab を登録する
    • 登録した Prefab を NetworkServer.Spawn(GameObject) に渡すと全クライアントで生成
  • シーンの管理
    • オフラインシーンとオンラインシーンを管理
    • オンラインになると自動でシーン遷移
    • Build Settings で Scenes In Build にシーンを登録すると Menu ScenePlay Scene にシーンをドラッグ&ドロップできるようになる
  • デバッグ
    • ネットワーク遅延やパケロスのシミュレーション
    • コネクションの状態やローカル・リモートのオブジェクトのリストの表示
  • マッチメイキング
    • Unity Multiplayer サービスとの連携(後述)
  • カスタマイズ
    • いくつかの関数は virtual になっていて継承してカスタム出来る
    • 例) Player Prefab 生成時の OnServerAddPlayer()
    • NetworkLobbyManager はこれを継承してロビーを作れるようにしたもの

先程の例では最初の2つの項目を利用した形になります。

NetworkIdentity

f:id:hecomi:20150808155819p:plain

NetworkIdentity はネットワーク同期でのコアとなるコンポーネントで、同期するオブジェクトには必ず付ける必要があります。ここにはネットワーク内で共通の Scene ID(どのシーンに属するか)や Network ID(ネットワーク内で一意に決まる ID)、Asset ID(どのアセットを利用するか)や他のコンポーネントから利用する様々なフラグ(例えば localPlayerAuthorityNetworkTransform が参照)といった情報が含まれています。

これらは Inspector 下部もしくは Inspector のモードを Debug にすることで確認することが出来ます。

f:id:hecomi:20150808153652p:plain

NetworkTransform

f:id:hecomi:20150808155718p:plain

NetworkTransform はその名の通りネットワーク内で Transform コンポーネントを同期する役割をします。同期のモード(単純に Transform の値を同期するか、RigidBody に追従するか、Character Controller に追従するかなど)や同期の頻度、補間の設定などが可能です。

NetworkTransformVisualizer コンポーネントをアタッチして Visualizer Prefab を設定すると生値と補間の様子を見ることが出来ます。

f:id:hecomi:20150808162716p:plain

後述しますが、Photon の PhotonTransformView の様に細かな補間の設定は出来ないようなので色々と調整したい場合は自前で書く必要があります。

NetworkAnimator

例では出てきませんでしたが、似たものに Animator を同期する NetworkAnimator もあります。

f:id:hecomi:20150812193856p:plain

指定した Animator の変数が自動で Inspector に出てきてチェックしたものを同期してくれます。ただあくまでも定期的な同期なので、すぐにかつ確実に同期が必要な場合、別途対応する必要があります。これは後述します。

サーバ・クライアント・ホスト

NetworkManager のところでサーバ、クライアント、ホストに触れましたが、理解を深めるためにこれらについてちょっと見ておきましょう。

まず大前提として、UNET では同じゲームのコードでクライアント・サーバ共に動かしています。サーバ専用の言語を覚えたりする必要はありません。

UNET では基本的には一つのサーバに複数のクライアントがぶら下がる形になります。ただ専用のサーバがない場合はいずれかのクライアントがサーバの役割も担うことになります。これがホストです。ホストではサーバとクライアントが同じプロセスで動作(同じシーンやオブジェクトを共有)しています。

f:id:hecomi:20150809154610p:plain

こうしてサーバに対してローカルなクライアントとリモートなクライアントが出来るのですが、プログラマはこれらのホストにローカルなクライアントかリモートなクライアントかを意識することなくプログラムできるようになっています。ただし、サーバかクライアントかといったことや、参照しているオブジェクトが各クライアントから見てローカルなのかリモートなのかは強く意識する必要があります。

例えば、先ほど操作するプレイヤにアタッチした NetworkIdentity コンポーネントIs Player Authority にチェックを入れましたが、これによって各クライアント毎に自身のプレイヤの所有権が与えられ、isLocalPlayer フラグが true になります。

f:id:hecomi:20150809164453p:plain

ネットワーク間で動作するコンポーネント

さて、今度はコードからの利用を見て行きましょう。先ほどの NetworkTransform の代わりになるようなものを書いてみます。簡単のために位置だけ同期するコードを書いてみます。

自前でネットワーク関連のコンポーネントを作成するには、MonoBehaviour の代わりに、これを継承してネットワーク機能を付加した NetworkBehaviour を継承します。NetworkBehaviourNetworkIdentity と共に動作します。いくつか特殊な記法が存在します。

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 やルールが存在し、これらを利用してネットワーク越しで情報をやり取りするコードを簡潔に書けるような仕組みが用意されています。一通り機能を見てみましょう。

  • 変数の同期
  • ネットワーク機能関連のコールバック
    • いくつかの virtual 関数群が用意されている(override して利用、詳細はマニュアル参照)
      • OnStartServer
      • OnStartClient
      • OnSerialize
      • OnDeSerialize
      • OnNetworkDestroy
      • OnStartLocalPlayer
      • OnRebuildObservers
      • OnSetLocalVisibility
      • OnCheckObserver
    • いくつかのコールバックは後で解説します
  • サーバ / クライアントでの関数の切り分け
    • Client アトリビュートをつけるとクライアントのみで実行される関数になる(サーバだと直ぐに return される)
    • Server アトリビュートをつけるとサーバのみ
    • Unity のコールバックにつける ClientCallbackServerCallback もある
      • 基本的には ClientServer と同じ、Warning を発生しない(?)
  • クライアントからサーバへのコマンドの送信
    • Cmd から始まる関数に Command アトリビュートをつけるとサーバで実行されるクライアントから呼び出せる関数になる
  • サーバからクライアントの RPC(リモートプロシージャコール)
    • Rpc から始まる関数に ClientRpc アトリビュートをつけるとクライアントで実行されるサーバから呼び出せる関数になる
  • ネットワーク越しのイベントの登録

これらをうまいこと利用してロジックを組んであげればゲームが出来るのが何となくイメージできるのではないでしょうか。チュートリアル動画ではこれらをうまく利用してゲームを作成しているのでサンプルとして見ると参考になると思います。

オブジェクトの "Spawn" を理解する

次に動的なオブジェクトの生成について見て行きましょう。

ドキュメントでは頻繁に "Spawn" という単語が出てきます。これは "Instantiate" とは区別して使われていて、"Instantiate" が Object.Instantiate() によってオブジェクトを生成するのに対し、"Spawn" はネットワークに接続されたクライアント全てにおいてオブジェクトを生成することを意味しています。

UNET では、オブジェクトがサーバ上で変更されたり破棄されるとその通知が各クライアントへ伝わります。また、生成後に新しいクライアントがサーバに接続した際も、その新しいクライアント上で既に生成済みのオブジェクトが生成されます。

オブジェクトを "Spawn" するためには、対象のオブジェクトを NetworkServer.Spawn() に渡す必要があります。NetworkServer クラスはサーバの状態や機能をまとめたクラスです。

もちろん直接オブジェクトの参照をネットワーク越しに渡すことは出来ません。そこでこれが上手く動くためには、各クライアントで何のオブジェクトを生成するか各クライアントが把握している必要があります。この役割を果たすのが NetworkIdentity の時に見た Asset ID です。そして Asset ID は事前に登録しておく必要があります。

登録する方法は NetworkManager を利用している際はインスペクタから、それ以外はコードから行う必要があります。インスペクタから行う場合は NetworkManagerSpawn Info > Registered Spawnable Prefabs に対象の Prefab を登録します。

f:id:hecomi:20150809185313p:plain

コードで書く場合は 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); // 要るか要らないかまだ不明...
    }
}

f:id:hecomi:20150810181213p:plain

生成・破棄をサーバ側で行うようにしています。サーバで Instantiate して、NetworkServer.Spawn() で全てのクライアントでも生成、時間が経ったら NetworkServer.Destroy() で全てのクライアントから破棄しています。UNET ではこういった、クライアントからサーバへの命令なのか、サーバからクライアントへの命令なのかのルールを守る必要があります。

f:id:hecomi:20150810191632j:plain

シリアライズ・デシリアライズ

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() 系の関数が NetworkServerNetworkClientNetworkConnection に実装されています(NetworkConnectionNetworkClient なら一つ、NetworkServer なら複数持っている各接続をまとめたクラス)。引数に MessageBase を継承したクラスのインスタンスを指定して使う形になります。

コードとしては、 Serialize(NetworkWriter)Deserialize(NetworkReader) を継承するクラスを作成することで複数のパラメタをパックすることが出来ます。メッセージを受けとってゴニョゴニョ処理するような場合や SyncVar が対応していない型(Byte Array 等)を送りたいときなど、SyncVar ではカバーできないケースに使うと良いと思います。

また、EmptyMessageIntegerMessageStringMessage は予め用意されています。

余談ですが、ものによって DeSerialize だったり Deserialize だったりするのは修正されるのかな...。

Channel / QoS

概要

これまで色々と見てきましたが、SyncVarSend はどういった通信路を経由して送るのかお任せの状態でした。例えば位置や姿勢は 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 ConfigurationQoS Channel から設定できます。

f:id:hecomi:20150810213545p:plain

利用方法

コードからはアトリビュートの引数として Channel を指定できます。スクリプト単位で指定する場合は NetworkSettings アトリビュートを使用します。

using UnityEngine.Networking;

[NetworkSettings(channel=1,sendInterval=0.2f)]
class MyScript : NetworkBehaviour
{
    [SyncVar]
    int value;
}

ここでは同時に同期の間隔(sendInterval)も指定できます。これらの設定は Inspector に表示されます。

f:id:hecomi:20150810214422p:plain

関数ごとには CommandClientRPC といったアトリビュートの引数で指定できます。

public class Player : NetworkBehaviour {
    // ...

    [Command(channel=1)]
    public void CmdMove(int x, int y) {
        moveX = x;
        moveY = y;
        isDirty = true;
    }
}

こういった細かなチューニングがより良いゲーム体験には必要になってきます。

プロファイラ

UNET では段階的にですがプロファイラとの統合が図られています。現在は以下の 2 つの機能がプロファイラと統合されています。詳細は未だドキュメント化されていないですが、どれだけパケットが流れているか、どのパケットが支配的になっているかといったことが確認可能です。グラフをクリックするとクリックした箇所の詳細が下部に表示されます。

Network Messaging

入出するパケットの流れを見ることが出来ます。

f:id:hecomi:20150811154823p:plain

Network Operations

どのタイミングでオブジェクトの生成・破棄が起きているか、CommandClientRPC がどれだけコールされているか、またそのコールされた関数は何か、といったことが確認できます。

f:id:hecomi:20150811155007p:plain

オブジェクトの Visibility 制御

パフォーマンスの話が続きます。ゲームが広いエリアで沢山のネットワーク関連のオブジェクトが接続されている場合、全てのクライアントに対して全てのオブジェクトを Spawn していると、レンダリングコストもかかりますし、沢山の帯域を消費してしまいますし、新しくユーザが参加した場合も全てのオブジェクトを Spawn するのに時間がかかってログイン時間が長くなってしまったりと色々と悪影響が出てきます。

NetworkProximityChecker

そこで、UNET では NetworkProximityChecker というコンポーネントが用意されていて、これを利用すると設定した距離以上離れると以下のように動作します。

  • ホストと同じローカルクライアントの場合
    • Renderer が disabled になる
  • リモートクライアントの場合
    • Destroy される
    • 新しく接続した場合、範囲外なら Spawn しない

f:id:hecomi:20150811162133p:plain

仕組みとしては Physics を利用しているので、Check MethodPhysics3DPhysics2D どちらでチェックするか選択する必要があります。その上で 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 になるだけです。ただ、ものによっては同期する必要のない重いスクリプトが付いている場合があります。これらは NetworkBehaviourOnSetLocalVisibility(bool) を利用することで制御することが可能です。

カスタマイズ

NetworkProximityChecker が内部的に何をしていてどうすればカスタマイズ出来るか見て行きましょう。まず、オブジェクトの Visibility の管理は全てサーバ側で行われることを覚えておいて下さい。

NetworkProximityChecker は定期的に NetworkIdentity.RebuildObservers() という関数を呼び、この結果 NetworkBehaviour.OnRebuildObservers(HashMap<NetworkConnection> observers, bool initial) というコールバックが呼ばれます。

この中で近接判定を行います。ここで言う Observer とは各プレイヤのことです。つまり RebuildObservers とは誰が見えているかという情報を更新してくれ、という命令になります。この情報というのが HashMap<NetworkConnection> で、ここに自分が誰から見えているか詰める、というのが近接判定の作業になります。

Observer はプレイヤと言いましたが、具体的にクラスで言うと NetworkConnection のことです。NetworkServer.connections に全てのクライアントに対してのコネクションが入っているのですが、これとは別にサーバ側では各プレイヤのオブジェクトにアタッチされた NetworkIdentityconnectionToClient に、各クライアントへのコネクションが格納されています。これを利用してプレイヤを判定するというのが具体的なコードになります。

以下、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 のリレーサーバ経由でマッチングすることが可能です。

f:id:hecomi:20150811194932p:plain

f:id:hecomi:20150811195107p:plain

f:id:hecomi:20150811195646p:plain

コードから制御する

マッチメイキングを制御するには 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 はシステムのソケットの上に乗る薄いレイヤです。

サンプルプロジェクトも上がっているので、気になる方は見てみると面白いと思います。

まだ余り情報がないのと、私がネットワーク周りに疎いので、どなたか詳しく解説してくださると嬉しいです。。

その他

その他気になりそうな点です。

WebGL への対応

LLAPI で対応してますが、HLAPI では今のところ対応していません。

現在対応中とのことです。WebRTC data channel も対応してくれないかな...。

ベータ機能

5.2b では、いくつかのバグ修正と、NetworkTransform の改善、ローカルでの Discovery、ノンプレイヤなオブジェクトの Authority の設定などが含まれているようです。

専用サーバ

ホスト前提のゲームでなく、MMO の様にどこかでサーバが動いていて、みんながそこにアクセスするようなことをしたい場合は、Linux ビルドしたアプリをバッチモードでどこかのサーバでヘッドレスに起動して運用するのが良いのではないかと思います。

コンソールから -batchmode 引数をつけるか、Linux のビルドオプションに Headless Mode があるので、それを指定してサーバ側で起動するのでも良いと思います。

f:id:hecomi:20150812194328p:plain

冒頭のデモはこの運用でサーバがさくらの VPS 上で動いています。同じプロジェクトのビルドでサーバ・クライアント両方動くのはスゴイですね。多分 AndroidiOS ビルドしても動くのではないでしょうか。

ただちょっとコードに気を使わないと色々と破綻してしまう(例えばホストでテストしているとクライアントの動作も含むため、サーバ単体で動かして初めてミスに気づくなど)ので注意が必要です。

設定

Project Settings に Network の項目が追加されています。現状は Debug LevelSendrate のみ指定できます。

f:id:hecomi:20150812194612p:plain

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 の情報が少なくてなかなか読めないところはありますが、今後のアップデートも注目していきます。