UNET⑭ NetworkManagerをカスタマイズして、プレイヤー生成処理などを独自のものにする

通常、NetworkManagerに登録可能なプレイヤーのプレハブは、1つだけです。つまり、全員同じキャラクターしか使えません。

今回は、NetworkManagerを継承した独自クラスを作り、プレイヤー生成処理をカスタマイズすることによって、プレイヤーによって別々のキャラクターを使えるようにします。

NetworkManagerを継承した独自クラスを作る

NetworkManagerのソースコードを見ると、いくつかのメソッドにvirtualが付いていることがわかります。つまり、これらのメソッドはoverrideして、処理を書き換えることが想定されているということです。

overrideするためだけに用意されたメソッドもあります。下記、一部抜粋です。

        public virtual void OnStartHost()
        {
        }

        public virtual void OnStartServer()
        {
        }

        public virtual void OnStartClient(NetworkClient client)
        {
        }

        public virtual void OnStopServer()
        {
        }

        public virtual void OnStopClient()
        {
        }

        public virtual void OnStopHost()
        {
        }

これらのメソッドをoverrideすることによって、特定のタイミングで処理を行うことが出来るようになります。

では、NetworkManagerを継承した独自クラスを作ってみましょう。

空のオブジェクトを生成し、名前を「NetworkManager」とします。

そのオブジェクトに新規スクリプトを作成・アタッチします。スクリプト名は「MyNetworkManager」とします。

MyNetworkManagerスクリプトは、NetworkManagerを継承したものにします。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking; // ←NetworkManagerなどのために必要

public class MyNetworkManager : NetworkManager // ←NetworkManagerを継承
{

}

これだけで、とりあえず、NetworkManagerとしてちゃんと動作します。

f:id:motoyamablog:20171127101430p:plain

Cubeなどで適当にプレイヤーオブジェクトのプレハブを作り、MyNetworkManager>SpawnInfo>PlayerPrefab欄に登録します。

MyNetworkManagerオブジェクトにNetworkManagerHUDを追加し、動作確認してみてください。普通のNetworkManagerと同じように動作するはずです。

各種メソッドをoverrideする

NetworkManagerのoverride可能なメソッド一覧です。

public virtual NetworkClient StartHost(ConnectionConfig config, int maxConnections)
public virtual NetworkClient StartHost(MatchInfo info)
public virtual NetworkClient StartHost()
public virtual void ServerChangeScene(string newSceneName)
public virtual void OnServerConnect(NetworkConnection conn)
public virtual void OnServerDisconnect(NetworkConnection conn)
public virtual void OnServerReady(NetworkConnection conn)
public virtual void OnServerAddPlayer(NetworkConnection conn, short playerControllerId, NetworkReader extraMessageReader)
public virtual void OnServerAddPlayer(NetworkConnection conn, short playerControllerId)
public virtual void OnServerRemovePlayer(NetworkConnection conn, PlayerController player)
public virtual void OnServerError(NetworkConnection conn, int errorCode)
public virtual void OnServerSceneChanged(string sceneName)
public virtual void OnClientConnect(NetworkConnection conn)
public virtual void OnClientDisconnect(NetworkConnection conn)
public virtual void OnClientError(NetworkConnection conn, int errorCode)
public virtual void OnClientNotReady(NetworkConnection conn)
public virtual void OnClientSceneChanged(NetworkConnection conn)
public virtual void OnStartHost()
public virtual void OnStartServer()
public virtual void OnStartClient(NetworkClient client)
public virtual void OnStopServer()
public virtual void OnStopClient()
public virtual void OnStopHost()
public virtual void OnMatchCreate(CreateMatchResponse matchInfo)
public virtual void OnMatchList(ListMatchResponse matchList)

 上記の中でも、overrideする可能性が高そうなメソッドを、実際にoverrideしてみましょう。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking; // ←NetworkManagerなどのために必要

public class MyNetworkManager : NetworkManager // ←NetworkManagerを継承
{
    public override void OnServerConnect(NetworkConnection conn)
    {
        print("OnServerConnect");
        base.OnServerConnect(conn);
    }

    public override void OnServerDisconnect(NetworkConnection conn)
    {
        print("OnServerDisconnect");
        base.OnServerDisconnect(conn);
    }

    public override void OnServerReady(NetworkConnection conn)
    {
        print("OnServerReady");
        base.OnServerReady(conn);
    }

    public override void OnServerAddPlayer(NetworkConnection conn, short playerControllerId)
    {
        print("OnServerAddPlayer(NetworkConnection conn, short playerControllerId)");
        base.OnServerAddPlayer(conn, playerControllerId);
    }

    public override void OnServerAddPlayer(NetworkConnection conn, short playerControllerId, NetworkReader extraMessageReader)
    {
        print("OnServerAddPlayer(NetworkConnection conn, short playerControllerId, NetworkReader extraMessageReader)");
        base.OnServerAddPlayer(conn, playerControllerId, extraMessageReader);
    }

    public override void OnServerRemovePlayer(NetworkConnection conn, PlayerController player)
    {
        print("OnServerRemovePlayer");
        base.OnServerRemovePlayer(conn, player);
    }

    public override void OnServerError(NetworkConnection conn, int errorCode)
    {
        print("OnServerError");
        base.OnServerError(conn, errorCode);
    }

    public override void OnServerSceneChanged(string sceneName)
    {
        print("OnServerSceneChanged");
        base.OnServerSceneChanged(sceneName);
    }

    public override void OnClientConnect(NetworkConnection conn)
    {
        print("OnClientConnect");
        base.OnClientConnect(conn);
    }

    public override void OnClientDisconnect(NetworkConnection conn)
    {
        print("OnClientDisconnect");
        base.OnClientDisconnect(conn);
    }

    public override void OnClientError(NetworkConnection conn, int errorCode)
    {
        print("OnClientError");
        base.OnClientError(conn, errorCode);
    }

    public override void OnClientNotReady(NetworkConnection conn)
    {
        print("OnClientNotReady");
        base.OnClientNotReady(conn);
    }

    public override void OnClientSceneChanged(NetworkConnection conn)
    {
        print("OnClientSceneChanged");
        base.OnClientSceneChanged(conn);
    }

    public override void OnStartHost()
    {
        print("OnStartHost");
        base.OnStartHost();
    }

    public override void OnStartServer()
    {
        print("OnStartServer");
        base.OnStartServer();
    }

    public override void OnStartClient(NetworkClient client)
    {
        print("OnStartClient");
        base.OnStartClient(client);
    }

    public override void OnStopServer()
    {
        print("OnStopServer");
        base.OnStopServer();
    }

    public override void OnStopClient()
    {
        print("OnStopClient");
        base.OnStopClient();
    }

    public override void OnStopHost()
    {
        print("OnStopHost");
        base.OnStopHost();
    }
}

メソッドが呼ばれたことをログに出して確認しています。以前作成したログを画面に出す機能を使うと確認しやすいです。持っていない人はこちらから入手

ゲームを複数起動し、サーバー、ホスト、クライアントでどのようにメソッドが呼ばれるか、確認してみてください。

OnServerSceneChanged は、接続後にシーンを切り替えないと呼ばれません。前回行ったように、シーンを切り替えて、確認してみてください。

プレイヤーごとに別々のプレハブから生成する

NetworkManagerの処理を書き換えて、プレイヤーごとに別々のプレハブからプレイヤーオブジェクトを生成できるようにしてみましょう。

プレイヤープレハブの作成

今回は、3種類のプレイヤーから好きなものを選べるようにしたいと思います。適当にCubeとSphereCapsuleでプレイヤープレハブを作ってください。

f:id:motoyamablog:20171127110749p:plain
※NetworkIdentityをアタッチするのをお忘れなく

NetworkManagerのRegisterdSpawnablePrefabsに3つのプレイヤープレハブを登録してください。

f:id:motoyamablog:20171127160129p:plain
PlayerPrefab欄は、もはや使われなくなるため、空で大丈夫です。AutoCreatePlayer欄はオンにしておいてください。

 

とりあえずランダムで生成されるようにNetworkManagerを改造する

最終的には、きちんと選べるようにしますが、とりあえず、ランダムでいずれかのプレハブから生成されるようにしてみましょう。

本家NetworkManagerにて、プレイヤーの生成を行っているのは以下の部分です。

        public virtual void OnServerAddPlayer(NetworkConnection conn, short playerControllerId, NetworkReader extraMessageReader)
        {
            OnServerAddPlayerInternal(conn, playerControllerId);
        }

        public virtual void OnServerAddPlayer(NetworkConnection conn, short playerControllerId)
        {
            OnServerAddPlayerInternal(conn, playerControllerId);
        }

        void OnServerAddPlayerInternal(NetworkConnection conn, short playerControllerId)
        {
            if (m_PlayerPrefab == null)
            {
                if (LogFilter.logError) { Debug.LogError("The PlayerPrefab is empty on the NetworkManager. Please setup a PlayerPrefab object."); }
                return;
            }

            if (m_PlayerPrefab.GetComponent<NetworkIdentity>() == null)
            {
                if (LogFilter.logError) { Debug.LogError("The PlayerPrefab does not have a NetworkIdentity. Please add a NetworkIdentity to the player prefab."); }
                return;
            }

            if (playerControllerId < conn.playerControllers.Count  && conn.playerControllers[playerControllerId].IsValid && conn.playerControllers[playerControllerId].gameObject != null)
            {
                if (LogFilter.logError) { Debug.LogError("There is already a player at that playerControllerId for this connections."); }
                return;
            }

            GameObject player;
            Transform startPos = GetStartPosition();
            if (startPos != null)
            {
                player = (GameObject)Instantiate(m_PlayerPrefab, startPos.position, startPos.rotation);
            }
            else
            {
                player = (GameObject)Instantiate(m_PlayerPrefab, Vector3.zero, Quaternion.identity);
            }

            NetworkServer.AddPlayerForConnection(conn, player, playerControllerId);
        }

見ての通り、OnServerAddPlayerInternalメソッド内でプレイヤー生成処理を行っています。しかし、OnServerAddPlayerInternalメソッドはvirtualではないため、上書きできません。そこで、OnServerAddPlayerInternalを呼び出しているOnServerAddPlayerを上書きします。

using UnityEngine;
using UnityEngine.Networking; // ←NetworkManagerなどのために必要

public class MyNetworkManager : NetworkManager // ←NetworkManagerを継承
{
    [SerializeField] GameObject m_CubePlayerPrefab;
    [SerializeField] GameObject m_SpherePlayerPrefab;
    [SerializeField] GameObject m_CapsulePlayerPrefab;
    
    public override void OnServerAddPlayer(NetworkConnection conn, short playerControllerId)
    {
        GameObject playerPrefab;

        int playerType = Random.Range(0, 3);

        if (playerType == 0) playerPrefab = m_CubePlayerPrefab;
        else if (playerType == 1) playerPrefab = m_SpherePlayerPrefab;
        else playerPrefab = m_CapsulePlayerPrefab;

        GameObject player;
        Transform startPos = GetStartPosition();
        if (startPos != null)
        {
            player = (GameObject)Instantiate(playerPrefab, startPos.position, startPos.rotation);
        }
        else
        {
            player = (GameObject)Instantiate(playerPrefab, Vector3.zero, Quaternion.identity);
        }

        NetworkServer.AddPlayerForConnection(conn, player, playerControllerId);
    }
}

基本的に本家NetworkManagerの処理をそのまま真似していますが、不要なエラー処理を削除し、プレイヤーのプレハブを3種類からランダムで選ぶように変更しています。

MyNetworkManagerに3種類のプレハブを登録します。

f:id:motoyamablog:20171127160302p:plain

 

実行する度に、3つのうちのいずれかのプレイヤープレハブが生成されるはずです。

f:id:motoyamablog:20171127123449g:plain

※上図ではPlayerStartPositionを使っています

 

このように、プレイヤー生成を上書きするだけなら比較的簡単です。上記スクリプトでGetStartPosition()とかやってる部分を改造すれば、生成場所も自由にできます。

 

さて、次は、Cube/Sphere/Capsuleの中から、任意のものを選べるようにしたいと思います。これは結構面倒です。まず、当然、選ぶUIを作る必要があります。そして、選んだ結果をクライアントからサーバーへ通知する必要があるので、そこが結構面倒です。

 

UIの作成

プレイヤーを3種類から選ぶためのUIを作成します。

UI>Canvasを作成し、UI Scale ModeをScale With Screen Sizeにします。

f:id:motoyamablog:20171127111315p:plain

Canvas配下に空のオブジェクトを追加し、名前をPlayerToggleGroupとします。
それにToggleGroupコンポーネントをアタッチします。

PlayerToggleGroupの中にToggleを3つ追加し、それぞれのオブジェクト名とTextを次のようにします。

f:id:motoyamablog:20171127111603p:plain


CubeToggleのIsOnはオンにし、それ以外のToggleのIsOnはオフにします。

3つのToggleのGroup欄にPlayerToggleGroupを登録します。

新規オブジェクトを作成し、PlayerInfoという名前にします。そのオブジェクトに新規スクリプトを作成・アタッチします。スクリプト名は「PlayerInfo」としてください。

using UnityEngine;
using UnityEngine.Networking;

// プレイヤーの種別を表す定数のクラス
public static class PlayerType
{
    public const int Cube = 0;
    public const int Sphere = 1;
    public const int Capsule = 2;
}

// プレイヤー生成時に必要な情報をクライアントからサーバーへ送るための入れ物
class PlayerInfoMessage : MessageBase
{
    public int type;
}

// プレイヤー情報クラス。
// 今何が選択されているかを管理する。
public class PlayerInfo : MonoBehaviour
{
    public static int type = PlayerType.Cube;

    public void OnCubeValueChanged(bool value)
    {
        if (value)
            type = PlayerType.Cube;
    }

    public void OnSphereValueChanged(bool value)
    {
        if (value)
            type = PlayerType.Sphere;
    }

    public void OnCapsuleValueChanged(bool value)
    {
        if (value)
            type = PlayerType.Capsule;
    }
}

各Toggleが押された時の処理とメソッドを結びつけます。

CubeToggle

f:id:motoyamablog:20171127121200p:plain

SphereToggle

f:id:motoyamablog:20171127121229p:plain

CapsuleToggle

f:id:motoyamablog:20171127121249p:plain

※メソッドを選ぶ際、Dyanamic boolのほうを選んでください。Static Parametersではないです。

f:id:motoyamablog:20171127121331p:plain

  

NetworkManagerを改造する 

基本的に、プレイヤー生成の切っ掛けとなるのは、シーンの切り替えです。シーンが接続画面(OfflineScene)からゲーム画面(OnlineScene)になったタイミングで、プレイヤーが生成されます。
そのため、NetworkManagerでは、OnClientSceneChanged内でプレイヤーの生成処理(ClientScene.AddPlayer)を呼び出しています。本家NetworkManagerの該当箇所を抜粋します。

public virtual void OnClientSceneChanged(NetworkConnection conn)
{
    // always become ready.
    ClientScene.Ready(conn);

    if (!m_AutoCreatePlayer)
    {
        return;
    }

    bool addPlayer = (ClientScene.localPlayers.Count == 0);
    bool foundPlayer = false;
    foreach (var playerController in ClientScene.localPlayers)
    {
        if (playerController.gameObject != null)
        {
            foundPlayer = true;
            break;
        }
    }
    if (!foundPlayer)
    {
        // there are players, but their game objects have all been deleted
        addPlayer = true;
    }
    if (addPlayer)
    {
        ClientScene.AddPlayer(0);
    }
}

ローカルプレイヤーオブジェクトが存在するか調べ、存在しなければClientScene.AddPlayerを呼んでいます。ClientScene.AddPlayerは、サーバーにプレイヤーの追加を依頼します。サーバーがこれを受け取ると、OnServerAddPlayerが呼び出され、その中でプレイヤーの生成が行われます。

任意のプレハブからプレイヤーオブジェクトを生成するためには、まず、どのプレハブから生成したいかを、クライアントからサーバーへ伝える必要があります。そのために、OnClientSceneChangedをoverrideして、ClientScene.AddPlayer実行時に、同時に付加情報を送るようにします。

次に、サーバー側のOnServerAddPlayerをoverrideして、付加情報を読み取って、プレハブを選択するようにします。

MyNetworkManagerを次のように改修します。

using UnityEngine;
using UnityEngine.Networking; // ←NetworkManagerなどのために必要

public class MyNetworkManager : NetworkManager // ←NetworkManagerを継承
{
    [SerializeField] GameObject m_CubePlayerPrefab;
    [SerializeField] GameObject m_SpherePlayerPrefab;
    [SerializeField] GameObject m_CapsulePlayerPrefab;

    public override void OnServerAddPlayer(NetworkConnection conn, short playerControllerId, NetworkReader extraMessageReader) // ←第3引数があるバージョンに変更します
    {
        PlayerInfoMessage msg = extraMessageReader.ReadMessage<PlayerInfoMessage>();
        int playerType = msg.type;

        GameObject playerPrefab;

        if (playerType == PlayerType.Cube) playerPrefab = m_CubePlayerPrefab;
        else if (playerType == PlayerType.Sphere) playerPrefab = m_SpherePlayerPrefab;
        else playerPrefab = m_CapsulePlayerPrefab;

        GameObject player;
        Transform startPos = GetStartPosition();
        if (startPos != null)
        {
            player = (GameObject)Instantiate(playerPrefab, startPos.position, startPos.rotation);
        }
        else
        {
            player = (GameObject)Instantiate(playerPrefab, Vector3.zero, Quaternion.identity);
        }

        NetworkServer.AddPlayerForConnection(conn, player, playerControllerId);
    }

    public override void OnClientSceneChanged(NetworkConnection conn)
    {
        // クライアント上でシーンの準備が完了したらこれを呼ぶ必要がある
        ClientScene.Ready(conn);

        // PlayerInfoMessageオブジェクトを作成し、プレイヤーの情報を格納する
        PlayerInfoMessage msg = new PlayerInfoMessage();
        msg.type = PlayerInfo.type;

        // サーバーにAddPlayerメッセージを送信する。
        // その際、第3引数に追加情報(PlayerInfoMessage)を付与する。
        ClientScene.AddPlayer(conn, 0, msg);
    }
}

 

OfflineSceneとOnlineSceneの作成

通常は、シーンの切り替えがプレイヤーの生成タイミングであるため、OfflineSceneとOnlineSceneを作って登録する必要があります。

今まで作ってきたシーンを「Offline」という名前で保存してください。

次に、新しいシーンを作成し、何かわかりやすい見た目にしたうえで、「Online」という名前で保存してください。

 

メニュー>File>BuildSettingsを開き、OfflineシーンとOnlineシーンを登録します。

f:id:motoyamablog:20171127162105p:plain

 

OfflineシーンのMyNetworkManagerにOfflineシーンとOnlineシーンを登録します。

f:id:motoyamablog:20171127162314p:plain

 

 

以上で完成です。選択したプレハブからプレイヤーが生成されることを確認してください。

f:id:motoyamablog:20171127125022g:plain

 

情報をもっと追加しよう

今回は「どのプレハブから作るか」しか指定していませんが、他にもプレイヤーの情報を追加してみましょう。

例えば、プレイヤー名を入力してからゲームに接続すると、プレイヤーオブジェクトの頭上にプレイヤー名が表示される・・・とか。

f:id:motoyamablog:20171127131452g:plain

このようなことがしたい場合は、上記で言えばPlayerInfoMessageクラスにプレイヤー名の変数を追加します。そして、OnServerAddPlayerで読み取って、プレイヤーオブジェクト(にアタッチされたNetworkBehaviourのスクリプトの変数)に設定します。SyncVar変数に持たせると自動で同期されるので楽です。

 

OfflineSceneとOnlineSceneが同一の場合

今回は、接続画面(OfflineScene)とゲーム画面(OnlineScene)が異なるシーンであるという前提で説明を行いましたが、接続画面とゲーム画面が同一である場合もあるかもしれません(本格的なゲームではあまり無いと思いますが)。その場合は、OnClientSceneChangedが呼ばれないため、上記の方法では正しく動きません。その場合は、OnClientConnectをoverrideし、上記スクリプトのOnClientSceneChangedで行っていることをOnClientConnectで行うようにしてください。

 

プレイヤーオブジェクトを複数シーンにまたがって使いまわす場合

ゲームのシーンが複数あり、シーンを切り替えてもプレイヤーオブジェクトを消さずに使いまわしたい(DontDestroyOnLoadを使う)場合があるかもしれません。その場合、上記のプログラムだと問題があります。上記のプログラムは、シーンが切り替わると、プレイヤーオブジェクトは古いシーンと共に一度削除されるということを前提としたプログラムであり、シーンが切り替わるたびにプレイヤーオブジェクト生成処理を行っています。プレイヤーオブジェクトがDontDestroyOnLoadで削除されない場合は、プレイヤーオブジェクトが存在するか調べて、存在しないときのみAddPlayerするようにしてください(そうしないと、まあ動くんですが、エラーログが出てしまうので)。

OnClientSceneChanged内の処理に、次の判定を追加します。

    public override void OnClientSceneChanged(NetworkConnection conn)
    {
        ClientScene.Ready(conn);

        // ローカルプレイヤーオブジェクトが存在するか調べ、
        // 既に存在する場合は、何もしない
        foreach (var playerController in ClientScene.localPlayers)
        {
            if (playerController.gameObject != null)
            {
                // 既にプレイヤーオブジェクトが存在するので、AddPlayerせず終了
                return;
            }
        }

        // プレイヤーオブジェクトが存在しないので、追加する
        PlayerInfoMessage msg = new PlayerInfoMessage();
        msg.type = PlayerInfo.type;

        ClientScene.AddPlayer(conn, 0, msg);
    }

 これで、プレイヤーオブジェクトがまだ無いときのみ、生成するようになります。

 

 

次へ進む

目次へ