【Unity】マルチプレイゲーム制作入門!第5回~同期するスコアUIを作ろう~

講座トップに戻る

はじめに

前回はコインの生成と削除を実装しました。

今回はコインの取得数を表示していきたいと思います。

今回の最終形はこんな感じになります!

コインを取得すると頭上の数値が増えていますね。

この数値もちゃんと同期されているのが分かると思います。

コインカウンター用のプレハブを作成

プレイヤーの上に表示するカウンター用のプレハブを作ります。

画像のようにTextMeshProを付けたUIオブジェクトを作成してください。

そしてCoinCountというスクリプトを作成します。

CoinCount.csは次のようになっています。

public class CoinCount : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI m_text;
    private Transform m_target;

    private void Update()
    {
        //ターゲットがいなくなっていたら削除する
        if(m_target == null)
        {
            Destroy(gameObject);
            return; 
        }

        //ワールド座標をスクリーン座標に変換
        transform.position = RectTransformUtility.WorldToScreenPoint(Camera.main, m_target.position + Vector3.up);
    }

    //数値設定
    public void SetNumber(int count)
    {
        m_text.text = count.ToString();
    }

    
    //表示する対象(プレイヤー)を設定
    public void SetTarget(Transform target)
    {
        m_target = target;
    }
}

ターゲットの少し上の位置をスクリーン座標に変換して数値を表示しています。

このコードに関してはNetcodeの機能を使っていないので特段説明はしません。

スクリプトが作成出来たらアタッチしておきましょう。

プレイヤー生成時にカウンターを生成

プレイヤーがスポーンされた際にカウンターも一緒に生成するようにしましょう。

UIを生成するのでGameシーンにCanvasを配置してください。

そしてプレイヤースクリプトを以下のように変更します。

(Start、Update、SetMoveInputServerRpc、ServerUpdate関数は変更がありません)

public class Player : NetworkBehaviour
{
    [SerializeField] float m_moveSpeed = 1;
    private Rigidbody m_rigidBody;
    private Vector2 m_moveInput = Vector2.zero;

    //コインのプレハブ
    [SerializeField] private GameObject m_coinCountPrefabs;
    private CoinCount m_coinCount;

    //コイン取得数
    private NetworkVariable<int> m_coinNum;

    void Awake()
    {
        m_coinNum = new NetworkVariable<int>(0);
    }

    void Start()
    {
        // Rigidbody を取得
        m_rigidBody = GetComponent<Rigidbody>();
    }

    public override void OnNetworkSpawn()
    {
        //コイン取得数変化通知
        m_coinNum.OnValueChanged += OnCoinNumChanged;

        //コインカウントUI生成
        var canvas = GameObject.Find("Canvas").transform;
        m_coinCount = Instantiate(m_coinCountPrefabs, canvas).GetComponent<CoinCount>();
        m_coinCount.SetTarget(transform);
        m_coinCount.SetNumber(m_coinNum.Value);
    }

    //コインUI更新
    void OnCoinNumChanged(int prevValue, int newValue)
    {
        m_coinCount.SetNumber(newValue);
    }

    private void Update()
    {
        //ownerの場合
        if (IsOwner)
        {
            // 移動入力を設定
            SetMoveInputServerRpc(
                    Input.GetAxisRaw("Horizontal"),
                    Input.GetAxisRaw("Vertical"));
        }

        //サーバー(ホスト)の場合
        if (IsServer)
        {
            ServerUpdate();
        }
    }

    //=================================================================
    //RPC
    //=================================================================
    // 移動入力をセットするRPC
    [ServerRpc]
    private void SetMoveInputServerRpc(float x, float y)
    {
        m_moveInput = new Vector2(x, y);
    }

    //=================================================================
    //サーバー側で行う処理
    //=================================================================
    // サーバー側で呼ばれるUpdate
    private void ServerUpdate()
    {
        //移動
        var velocity = Vector3.zero;
        velocity.x = m_moveSpeed * m_moveInput.normalized.x;
        velocity.z = m_moveSpeed * m_moveInput.normalized.y;
        //移動処理
        m_rigidBody.AddForce(velocity * Time.deltaTime);
    }

    void OnTriggerEnter(Collider other)
    {
        if (IsServer == false) { return; }
        if (other.gameObject.CompareTag("Coin"))
        {
            //取得処理
            m_coinNum.Value += 1;
            //コイン削除処理(CoinManagerの処理を呼ぶ)
            CoinManager.Instance.DeleteCoin(other.gameObject);
        }
    }
}

Playerのプレハブを開きPlayerScriptのCoinCountPrefabsに先ほど作ったプレハブをアタッチします。

UI生成

プレイヤーがスポーンされたタイミングで呼び出されるOnNetworkSpawnコールバックでUIを生成します。

//コインカウントUI生成
var canvas = GameObject.Find("Canvas").transform;
m_coinCount = Instantiate(m_coinCountPrefabs, canvas).GetComponent<CoinCount>();
m_coinCount.SetTarget(transform);
m_coinCount.SetNumber(m_coinNum.Value);

同期変数を作成

Netcodeの機能にNetworkVariableというものがあります。

これを使うと同期する変数を作ることができます。

今回はコイン数を同期したいためint型の同期変数が必要です。

その場合は以下のように定義します。

private NetworkVariable<int> m_coinNum;

次にOnNetworkSpawn値が変更された時のコールバック関数を設定します。

m_coinNum.OnValueChanged += OnCoinNumChanged;

これでサーバー側でm_coinNumの値が変更されると、登録したOnCoinNumChangedコールバックが呼び出されます。

void OnCoinNumChanged(int prevValue, int newValue)
{
    //コインの値を更新
    m_coinCount.SetNumber(newValue);
}

そしてコインに当たったときにm_coinNumを+1するようしましょう。

void OnTriggerEnter(Collider other)
{
    if (IsServer == false) { return; }
    if (other.gameObject.CompareTag("Coin"))
    {
        //取得処理
        m_coinNum.Value += 1;
        //コイン削除処理(CoinManagerの処理を呼ぶ)
        CoinManager.Instance.DeleteCoin(other.gameObject);
    }
}

これでコインを取得したときに、カウンターが連動して増える仕組みができました。

NetworkVariableの値を変更する際は以下の2点に注意してください。

  • 値を変更は.Valueに対し行う
  • 値の変更はサーバーで行う

プレイヤーの生成方法を変更

ここまででコイン取得数を表示する部分の作成は完了しました。

しかし、現状で実行すると意図したとおりに動きません。

左がホストですが、ホストの画面でだけ自身の操作キャラの頭上にUIが表示されていません。

これはプレイヤーが生成されるタイミングに原因があります。

現状プレイヤーオブジェクトはホストに接続した時に自動的に生成されます。

そのためホスト自身はGameシーンに切り替わる前にオブジェクトが生成されてしまいます。

なのでTitleシーンにコインカウントUIが生成され、シーンが切り変わったときに破棄されています。

逆にクライアントがサーバーに接続する際はすでにサーバー側のシーンがGameに切り替わっている状態なので、問題なく生成されています。

この問題を解決するために、プレイヤーの生成タイミングを変えましょう。

プレイヤーの自動生成を止める

今のままだと自動的にプレイヤーオブジェクトが生成されるので、その設定をやめましょう。

まず、タイトルシーンのNetworkMangerを選択し、PlayerPrefabの設定をNoneに変更します。

そして後で手動で生成するためにNetworkPrefabsの方に登録してください。

次にTitle.csのApprovalCheck関数内の一部を以下のように変更します。

//PlayerObjectを生成するかどうか
response.CreatePlayerObject = false;
//PlayerObjectをスポーンする位置(nullの場合Vector3.zero)
response.Position = Vector3.zero;

これで自動的に生成されなくなりました。

プレイヤーを任意のタイミングで生成する

今度はGameシーンに切り替わったタイミングでプレイヤーを生成するようにしましょう。

方法はGameシーンにネットワークオブジェクトを置き、そのオブジェクトのOnNetworkSpawnコールバック内でプレイヤーを生成します。

これでGameシーンに切り替わったタイミングでプレイヤーを生成することができます。

Game.csという名前のスクリプトを作成します。

public class Game : NetworkBehaviour
{
    //プレイヤーのプレハブ
    [SerializeField] private NetworkObject m_playerPrefab;

    public override void OnNetworkSpawn()
    {
        //ホスト以外の場合
        if (IsHost == false){ return; }

        //クライアント接続時
        NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;

        //すでに存在するクライアント用に関数呼び出す
        foreach (var client in NetworkManager.Singleton.ConnectedClientsList)
        {
            OnClientConnected(client.ClientId);
        }
    }

    public void OnClientConnected(ulong clientId)
    {
        //プレイヤーオブジェクト生成
        var generatePos = new Vector3(0, 1, -8);
        generatePos.x = -5 + 5 * (NetworkManager.Singleton.ConnectedClients.Count % 3);
        NetworkObject playerObject = Instantiate(m_playerPrefab, generatePos, Quaternion.identity);
        //接続クライアントをOwnerにしてPlayerObjectとしてスポーン
        playerObject.SpawnAsPlayerObject(clientId);
    }
}

そしてGameシーンにこのスクリプトをアタッチするオブジェクトを作ります。

今回はInstanceという名前のオブジェクトにNetworkObjectとGame.csを取り付けました。

PlayerPrefabの部分にプレハブをアタッチし忘れないように注意しましょう。

プレイヤーオブジェクトのスポーン

ネットワークオブジェクトのスポーン方法はコインを生成する時に学びました。

NetworkObject型のSpawn関数をサーバーで呼び出せばいいのでしたね。

今回は少しだけ違っていて、プレイヤーオブジェクトを生成します。

プレイヤーオブジェクトとはNetcodeにおいて特別な存在で、NetworkManger経由で直接取得することができます。

//クライアントが自分自身のレイヤーオブジェクトを取得する場合
NetworkManager.Singleton.LocalClient.PlayerObject;
//サーバーが特定のクライアントのプレイヤーオブジェクトを取得する場合
NetworkManager.Singleton.ConnectedClients[clientId].PlayerObject;

自動でプレイヤーを生成していた時は、勝手にPlayerObjectとして生成してくれていました。

これを手動で生成する場合はNetworkObject型のSpawnAsPlayerObjectという関数を使います。

//接続クライアントをOwnerにしてPlayerObjectとしてスポーン
playerObject.SpawnAsPlayerObject(clientId);

引数のclientIdはそのプレイヤーを操作するクライアントのIDになります。

クライアント接続コールバック

OnClientConnectedCallbackはクライアントが接続した際に呼び出されるコールバックです。

これでクライアントが接続するたびにOnClientConnected関数が呼び出されます。

//クライアント接続時
NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;

OnClientConnectedの引数はクライアントのIDです。

この関数内で先ほど説明したプレイヤーオブジェクトの生成を行います。

public void OnClientConnected(ulong clientId)
{
    //プレイヤーオブジェクト生成
    var generatePos = new Vector3(0, 1, -8);
    generatePos.x = -5 + 5 * (NetworkManager.Singleton.ConnectedClients.Count % 3);
    NetworkObject playerObject = Instantiate(m_playerPrefab, generatePos, Quaternion.identity);

    //接続クライアントをOwnerにしてPlayerObjectとしてスポーン
    playerObject.SpawnAsPlayerObject(clientId);
}

しかしこれだけでは問題があり、OnClientConnectedCallbackはすでに接続されているクライアント(ホスト含め)に対してはコールバックが呼び出されません。

なので、すでに存在するクライアントようにはOnClientConnected関数を手動で呼び出します。

//すでに存在するクライアント用に関数呼び出す
foreach (var client in NetworkManager.Singleton.ConnectedClientsList)
{
    OnClientConnected(client.ClientId);
}

動作確認してみよう

ここまで出来たらビルドして動作確認してみましょう。

全てのプレイヤーの上にコイン数が表示されるようになったと思います。

まとめ

今回は変数の同期とプレイヤーオブジェクトの生成について学びました。

Netcodeの学習部分は今回で終わりです。お疲れさまでした。

あとはSteamの機能を使ってインターネット越しで通信ができるようにするだけです。

あと少しですので頑張りましょう。

👇あなたにお勧めの書籍

次回↓

コメント