tanaka's Programming Memo

プログラミングについてのメモ。

Mirrorでプレイヤーごとに違うプレハブからプレイヤーオブジェクトを生成する

f:id:am1tanaka:20200717212431p:plain

Mirrorは、UNetをベースに設計された高評価のMMOスケールにも対応できるというネットワークAPIライブラリです。

mirror-networking.com

MirrorでデフォルトのNetworkManagerを使った場合、Player Prefab欄に設定されているプレハブをプレイヤー用のゲームオブジェクトとして自動的に生成します。NetworkManagerを継承すればプレイヤーごとに違うプレハブからプレイヤーを生成することができるという自分用のメモです。

目次

実行環境

  • Unity2019.3.15f1
  • Mirror16.1.1

ざっくり手順

プレイヤー用のプレハブを用意する

違いが分かるようにメッシュを変えたり動きを変えたプレイヤー用のプレハブをいくつか用意します。NetworkIdentityをアタッチしてあれば一先ず動きます。

プレイヤー定義用のScriptableObjectを作成

クライアントからどのプレハブを使うかを送る手段が考えつかなかったので、ちょっと乱暴ですがScriptableObjectにプレイヤー用のプレハブを配列で持たせて、そのインデックスで生成するオブジェクトを指定するようにします(ResourcesやAssetBundleを使えば、プレハブ名などでいけると思います)。

PlayerPrefabList.cs

配列の参照用のenumと、GameObjectの配列を持つScriptableObjectを宣言しています。PlayerTypeの内容は、用意するプレイヤー用プレハブに応じて書き換えてください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum PlayerType
{
    Green,
    Red
}

[CreateAssetMenu(menuName ="AM1Mirror/PlayerPrefabList")]
public class PlayerPrefabList : ScriptableObject
{
    public GameObject [] playerPrefs = null;
}

これができたら、ProjectウィンドウのCreateから AM1Mirror > PlayerPrefabList を選んでスクリプタブルオブジェクトを作成します。作成したら、enumの定義順に応じてプレイヤー用のプレハブをInspectorウィンドウから設定しておきます。

NetworkManagerのサブクラスを作成する

公式ガイドの以下をもとに、今回の要であるカスタムのNetworkManagerを作ります。

mirror-networking.com

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Mirror;

public class NetworkManagerCP : NetworkManager
{
    PlayerType spawnPlayerType;

    [Tooltip("プレイヤープレハブリスト"), SerializeField]
    PlayerPrefabList playerPrefabList = null;

    public void SetPlayerType(PlayerType pt)
    {
        spawnPlayerType = pt;
    }

    /// <summary>
    /// サーバー開始時、プレイヤーキャラクターのメッセージを登録
    /// </summary>
    public override void OnStartServer()
    {
        base.OnStartServer();

        NetworkServer.RegisterHandler<CreateCharacterMessage>(OnCreateCharacter);
    }

    /// <summary>
    /// クライアント側で接続した時に、選択してあるプレイヤーのプレハブをメッセージで送信
    /// </summary>
    /// <param name="conn"></param>
    public override void OnClientConnect(NetworkConnection conn)
    {
        base.OnClientConnect(conn);

        CreateCharacterMessage ccm = new CreateCharacterMessage
        {
            playerType = spawnPlayerType
        };

        conn.Send(ccm);
    }

    /// <summary>
    /// メッセージがクライアントからサーバーに到着したら、届いたプレハブでプレイヤー生成
    /// </summary>
    /// <param name="conn"></param>
    /// <param name="messages"></param>
    void OnCreateCharacter(NetworkConnection conn, CreateCharacterMessage messages)
    {
        Transform tr = GetStartPosition();
        GameObject go = Instantiate(playerPrefabList.playerPrefs[(int)messages.playerType], tr.position, tr.rotation);
        NetworkServer.AddPlayerForConnection(conn, go);
    }

}

通信を開始する

通信を開始するためのクラスSimpleNetManを作成します。これはNetworkManagerCPに統合できるのですが、設定がNetworkManagerの設定に埋もれるのが見辛いので分けました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Mirror;

public class CreateCharacterMessage : MessageBase
{
    public PlayerType playerType;
}

public class SimpleNetMan : MonoBehaviour
{
    public enum ConnectType
    {
        Host,
        Client
    }

    [Tooltip("プレイヤーの種類"), SerializeField]
    PlayerType playerType = PlayerType.Green;
    [Tooltip("接続の種類"), SerializeField]
    ConnectType connectType = ConnectType.Host;
    [Tooltip("ホストのIPアドレス"), SerializeField]
    string ipAddress = "localhost";

    NetworkManagerCP networkManager = null;

    private void Awake()
    {
        networkManager = GetComponent<NetworkManagerCP>();
        networkManager.SetPlayerType(playerType);
        StartCoroutine(Online());
    }

    IEnumerator Online()
    {
        yield return null;  // 1フレーム待つ

        if (connectType == ConnectType.Host)
        {
            networkManager.StartHost();
        }
        else
        {
            networkManager.networkAddress = ipAddress;
            networkManager.StartClient();
        }
    }
}

IEnumeratorでやってるのは、なんとなく1フレーム待った方が初期化が終ってそうでいいかな、という雰囲気でやったことです。Awake()内でやってもいいかも。

仕上げ

作成したSimpleNetManNetworkManagerCPNetworkManagerのような名前のゲームオブジェクトに一緒にアタッチします。

SimpleNetManでは、生成したいプレイヤータイプを設定して、ホストかクライアントか選択して、ホストのIPアドレスを設定します。これらのパラメーターを設定するメニューを用意すれば実行時に変更できます。

NetworkManagerCPでは、下の方にあるPlayer Prefab List欄に、最初に作成したScriptableObjectをアタッチして、ScriptableObjectに設定したプレイヤー用のプレハブをRegistered Spawnable Prefabs欄に全て追加します。これをしないとプレイヤーオブジェクトがネットワーク上に生成できず、エラーになります。

まとめ

ざっくりですがこんな感じでできました。ScriptableObjectを利用するなど何らかの方法で、クライアントからホストへ生成したいプレイヤープレハブをNetworkMessageで送信して、それをもとにStartHost()やStartClient()が実行されたら、NetworkMessageを受け取って記録して、クライアントの生成の段階で指定されたプレハブからプレイヤーオブジェクトをInstantiateして、ネットワークに追加します。

この記事では、スクリプトが分かれていたり、Inspectorウィンドウでプレイヤーや接続の種類を設定する不自然な状態になっていますが、これは体験入学用に全員がUnityエディターで作業をすることを前提としているからです。通常の利用であれば、プレイヤーを選択するルームなどを作って、そこで選ばれたものをメッセージで送ってゲーム開始、という感じになると思います。

とりあえず、こんな流れで、こんな感じのコードで異なるプレハブからプレイヤーを生成できるという記事でした。

おまけ:Mirrorのイベントの発生順

重要なドキュメントですが、なんか下の方にあった...

mirror-networking.com

これを見ると、まずはStart()が呼ばれて、OnStartServer()OnStartHost()より後に呼ばれています。ということで、NetworkManagerのStartHost()やStartClient()で通信を開始します。

参考URL