【Unity】マルチプレイゲーム制作入門!第3回~キャラクターを動かそう~

講座トップに戻る

はじめに

前回はホストとクライアントの接続を行いました。

今回は次の内容を実装していきます。

  • ステージ用オブジェクトの配置
  • プレイヤーオブジェクトの作成
  • 接続承認を作成

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

プレイヤー(ボール)が同期され両方の画面で動くようになっています。

ステージを作成

プレイヤーを動かすためのステージを作っていきます。

このステージは動くことがないので同期させる必要がありません。

そのため最初からシーンに置いておくだけで問題ありません。

下の画面になるようにオブジェクトを配置してください。

地面のオブジェクト(Plane)はY軸を-0.5だけ下げています。

配置後のシーンはこんな感じです。

ちなみにカメラのトランスフォームはこんな感じです

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

適当なフォルダにプレイヤー用のプレハブを作成してください。

まずは普通に球体を作るので以下のコンポーネントを追加します。

プレイヤーは同期させる必要があるので、そのためのコンポーネントを追加します。

  • NetworkObject
  • NetworkTransform
  • NetworkRigidbody

設定は初期値のままで問題ありません。

同期させるために追加した3つのコンポーネントについて解説します。

NetworkObject

同期させたいオブジェクトはNetworkObjectコンポーネントをつける必要があります。

このコンポーネントを付けたオブジェクトはネットワークオブジェクトになります。

ネットワークオブジェクトは特定のクライアントまたはサーバーが所有者(Owner)に設定されます。

所有者が切断すると、そのネットワークオブジェクトは削除されます。

NetworkTransform

NetworkTransformコンポーネントを付けたネットワークオブジェクトはTransformが同期されます。

NetworkRigidbody

NetworkRigidbodyコンポーネントを付けたネットワークオブジェクトは物理挙動が同期されます。

その他のコンポーネント

そのほかにもAnimationを同期するコンポーネントなどがありますが、今回は割愛します。

プレイヤー用のスクリプトを作成

次にプレイヤー用のスクリプトを作成します。

プロジェクトの適当な場所にPlayer.csを作成してください。

以下がPlayer.csの中身になります。

using Unity.Netcode;
using UnityEngine;
public class Player : NetworkBehaviour
{
    [SerializeField] float m_moveSpeed = 1;

    private Rigidbody m_rigidBody;
    private Vector2 m_moveInput = Vector2.zero;

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

    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);
    }
}

このスクリプトを先ほど作成したオブジェクトにアタッチしてください。

スクリプトの重要な部分を解説していきます。

NetworkBehaviourを継承する

ネットワークオブジェクトにアタッチするスクリプトは、NetworkBehaviourを継承します。

これを継承すると以下のようなことができます。

  • ネットワーク関連のイベントが発生する(OnNetworkSpawnなど)
  • RPCが定義できる(ネットワーク越しに命令する特殊な関数のこと)
  • 同期変数が作成できる
  • このスクリプトが実行されている端末が「クライアント」「ホスト」「サーバー」のどれなのかを判別できる
  • このスクリプトが実行されている端末が、このオブジェクトの所有者かどうかを判別できる

これらの機能は使う時に説明していくので今は「へーそんな機能があるんだ」くらいの気持ちで見ておいてください。

移動入力をサーバーに反映するServerRPCを作成

同期オブジェクトの処理はサーバー側で行い、その結果がクライアントに反映されます。

(クライアントで処理を行っても他のクライアントに同期されません。)

例えば同期しているキャラクターの移動などはサーバー側で行う処理です。

しかし、入力の検知はクライアントごとに行うので結果をサーバーに伝える必要があります。

このように、クライアントからサーバーに何かアクションを行いたい場合はRPC(リモートプロシージャコール)という機能を使います。

ServerRPCはクライアントから呼び出すとサーバーで実行される関数です。

今回はクライアントが検知した移動入力をセットするServerRPCを作成しています。

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

次の項目を満たす関数がServerRPCになります。

  • 定義するスクリプトがNetworkBehaviourを継承している
  • [ServerRpc]というアトリビュートを関数につける
  • 関数名の最後は必ず「ServerRpc」にする

Update時にServerRpcを呼び出す

クライアントは入力をサーバーに伝えるために、先ほど作ったServerRpcを呼び出す必要があります。

NetoworkObjectはすべてのクライアント上で生成されているので、自分がオーナーの場合のみ入力の値をサーバーに渡します。

NetworkBehaviourを継承しているオブジェクトは、そのオブジェクトのオーナーかどうかをチェックすることができるIsOwnerというプロパティが用意されています。

Update関数内でIsOwnerがTreuの場合のみ先ほど作ったServerRpcを呼び出しています。

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

サーバーで移動処理を実行

今度はサーバー側でクライアントから受け取った入力をもとに移動処理を実行します。

先ほどのIsOwnerと似たものにサーバーかどうかをチェックするIsServerというプロパティがあります。

UpdateでIsServerをチェックしてTrueの場合のみServeUpdate関数を呼び出しています。

private void Update()
{
    //サーバー(ホスト)の場合
    if (IsServer)
    {
        ServerUpdate();
    }
}
// サーバーだけで呼び出す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);
}

プレハブをNetworkManagerに登録

NetworkObjectのプレハブはNetworkMangaerに登録する必要があります。

Titleシーンに戻りNetworkMangerを選択します。

PlayerPrefabという項目に先ほど作ったPlayerのプレハブをセットします。

この状態で一度プレイしてみます。

こんな感じでボールが出て動くようになりました!

接続承認を作成

現在はクライアントが無条件で接続できるようになっているので、クライアントが接続する前にホストが接続を許可するかチェックするようにしましょう。

StartHost関数を呼び出す前に、NetworkManager.Singleton.ConnectionApprovalCallbackにコールバックを登録することで、クライアント接続時にこのコールバックが呼び出されるようになります。

またのこコールバックを有効にするにはNetworkManagerの「ConnectionApproval」という項目をチェックする必要があります。

画像の赤枠の部分をクリックしてください。

次にコールバックを設定しましょう。

Titleクラスを以下のように変更します。

public class Title : MonoBehaviour
{
    public void StartHost()
    {
        //接続承認コールバック
        NetworkManager.Singleton.ConnectionApprovalCallback = ApprovalCheck;
        //ホスト開始
        NetworkManager.Singleton.StartHost();
        //シーンを切り替え
        NetworkManager.Singleton.SceneManager.LoadScene("Game", LoadSceneMode.Single);
    }

    public void StartClient()
    {
        //ホストに接続
        bool result = NetworkManager.Singleton.StartClient();
    }

    /// <summary>
    /// 接続承認関数
    /// </summary>
    private void ApprovalCheck(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
    {
        // 追加の承認手順が必要な場合は、追加の手順が完了するまでこれを true に設定します
        // true から false に遷移すると、接続承認応答が処理されます。
        response.Pending = true;

        //最大人数をチェック(この場合は4人まで)
        if (NetworkManager.Singleton.ConnectedClients.Count >= 4)
        {
            response.Approved = false;//接続を許可しない
            response.Pending = false;
            return;
        }

        //ここからは接続成功クライアントに向けた処理
        response.Approved = true;//接続を許可

        //PlayerObjectを生成するかどうか
        response.CreatePlayerObject = true;

        //生成するPrefabハッシュ値。nullの場合NetworkManagerに登録したプレハブが使用される
        response.PlayerPrefabHash = null;

        //PlayerObjectをスポーンする位置(nullの場合Vector3.zero)
        var position = new Vector3(0, 1, -8);
        position.x = -5 + 5 * (NetworkManager.Singleton.ConnectedClients.Count % 3);
        response.Position = position;

        //PlayerObjectをスポーン時の回転 (nullの場合Quaternion.identity)
        response.Rotation = Quaternion.identity;

        response.Pending = false;
    }
}

今回は4人まで接続できるようにしました。

以下の部分でスポーンする位置を調整しています。

//PlayerObjectをスポーンする位置(nullの場合Vector3.zero)
var position = new Vector3(0, 1, -8);
position.x = -5 + 5 * (NetworkManager.Singleton.ConnectedClients.Count % 3);
response.Position = position;

NetworkManager.Singleton.ConnectedClients.Countで現在接続されているクライアントの数が分かるので、少しずつ位置をずらすようにしています。

ビルドして確認しよう

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

ボールが出てそれぞれの画面で移動ができるようになりました。

まとめ

今回はステージを作成して、プレイヤーを動かすところまで作成しました。

今回は長かったですね、学習お疲れさまでした!

次回はコインを出して当たると取る処理を作っていきたいと思います。

👇あなたにお勧めの書籍

次回↓

コメント