【Unity】ネットワーク未経験でも作れるマルチプレイヤー入門(Netcode for GameObjects)

2026年6月11日

広告

Unity Netcode for GameObjects を使うと、Host や Client の起動、プレイヤーオブジェクトの生成、位置の同期など、マルチプレイヤーゲームに必要な基本機能を比較的少ないコードで実装できます。

初めてネットワーク機能を触ると、Host、Client、Owner などの言葉が出てきて少し分かりにくく感じるかもしれません。

この記事では、できるだけ小さな2Dサンプルを作りながら、Unity Netcode for GameObjects の基本を確認していきます。H キーで Host、C キーで Client を起動し、自分のプレイヤーだけを動かせるところまで作ります。

まずは、Host、Client、Owner の関係を簡単に整理しておきます。
Host は Server と Client を兼ねた起動方法です。Client は Host に接続し、それぞれ自分が所有する Player だけを操作します。

NetCodeで2Dネットワークアプリのシンプルなサンプルを作る

NetCodeはUnityで利用できるネットワークライブラリの一つで、マルチプレイヤーゲームの開発を容易にするために設計されています。ここでは、Unityで2Dマルチプレイヤーゲームのサンプルプロジェクトを作成する手順を説明します。

新しいプロジェクトを作成する

Unity Hubから2Dのプロジェクト(MultiPlayerSample)を作成します

パッケージマネージャーで Netcode for GameObjects をインストールする

  1. エディタのメニューから、Window > Package Management > Package Manager を選択します。
  1. Package Manager ウィンドウの左サイドバーにある Sources > Unity Registry を選択します。
  2. 中央上部の検索窓(虫眼鏡マーク)に「net」または「Netcode for GameObjects」と入力します。
  3. 検索結果に表示された Netcode for GameObjects を選択し、右側にある Install ボタンをクリックします。

インストールが完了すると…

インストールが正常に完了すると、画面が次のように変化します。

  • パッケージ名の右側に 緑色のチェックマーク(導入済みの印) が表示されます。
  • 先ほどまで「Install」だったボタンが、Locate や Manage という表示に切り替わります。

インストールが完了すると、自動的にプロジェクトへ組み込まれます。左上の「All Packages」に切り替えて確認することもできますが、チェックマークがついたのを確認できたら、そのまま Package Manager ウィンドウを閉じてしまって大丈夫です!

基本的なコンポーネントを作成する

マルチプレイヤーゲームの基本的な構成要素を作成します。

ネットワークマネージャーの作成とトランスポートの選択

ネットワークマネージャーを追加し、プロジェクトにUnityトランスポートを追加します。NetworkManagerは、プロジェクトのネットコード関連の設定をすべて設定するコンポーネントです。Unity Transportは、Netcodeがサーバーとクライアント間の通信に使用するトランスポート層です。詳細については、こちらをご覧ください。

  • [Hierarchy]ウィンドウを右クリックします。
  • [Create Empty] を選択し、空のゲームオブジェクトを作成します
  • 名前をNetworkManagerに変更します
  • [Inspector] [ Add Component](コンポーネントの追加) をクリックし、NetworkManagerコンポーネントを選択します。

検索窓で、一部入力がわかりやすいです

[NetworkManagerゲームオブジェクト]の[NetworkManagerコンポーネント]で、NetworkTransportフィールドを見つけます。
「Select transport…」(トランスポートを選択)をクリックします。
UnityTransportを選択します。

シーンを保存します。

接続されたプレーヤーごとにオブジェクトを生成する

ここではプレイヤーオブジェクトを追加し、接続しているプレイヤーごとに生成(スポーン)させます。

[Hierarchyウィンドウ]で右クリックし、2D Object > Sprite > Circleを作成します。
名前をPlayerに変更します。
Playerを選択した状態で、Inspector Netcode > NetworkObjectコンポーネントを追加します。

ネットワーク転送用のコードを作成し、アタッチします

Playerを選択した状態でClientNetworkTransformスクリプトをPlayerにアタッチします

using Unity.Netcode.Components;

public class ClientNetworkTransform : NetworkTransform
{
    protected override bool OnIsServerAuthoritative()
    {
        return false;
    }
}
インフォメーション

このメソッド OnIsServerAuthoritative は、サーバーがオブジェクトのトランスフォームに対して権限を持つかどうかを判断するためのものです。ここで false を返すことによって、「このクライアントが権限を持つ」と定義しています。つまり、このクラスを使用するオブジェクトのトランスフォームは、クライアントによって制御され、サーバーはそのトランスフォームを受け入れるだけになります。

ワンポイント:なぜこのコードが必要なの?

デフォルトの NetworkTransform は「サーバー側」がオブジェクトの位置を管理する仕組み(Server Authoritative)になっています。
しかし、プレイヤー自身の移動に関しては、クライアント側(操作しているプレイヤーの画面)で即座に動かした方が操作感がスムーズになります。ここで false を返すことで、「サーバーではなくクライアント側に移動の権限を持たせる」という設定にカスタマイズしています。

最新のUnity環境での補足

現在のUnity公式では、これと全く同じ機能を持つコンポーネントが含まれた拡張パッケージ(サンプルパッケージなど)も配布されています。開発規模が大きくなった際はそちらを導入して利用することも可能ですが、本チュートリアルでは手軽に確実に動作させるため、上記のシンプルな自作スクリプトを使用しています。

Player移動スクリプトの作成

次のスクリプトをPlayerにアタッチします

【Unity 6 での注意点】

Unity 6 では新しい「Input System」が標準仕様となっています。従来の Input.GetAxisRaw の代わりに Keyboard.current を使用することで、プロジェクト初期設定の「Active Input Handling」がどちら(新・旧・両方)に設定されていても、エラーなく確実に動作させることができます。
(※あらかじめキーボードがない環境を想定する場合は、Keyboard.current != null のチェックを入れるのが安全です)

using Unity.Netcode;
using UnityEngine;
// 新しいInput Systemを使用するために必要です
using UnityEngine.InputSystem; 

public class PlayerController : NetworkBehaviour
{
    [SerializeField] float moveSpeed = 4f;
    void Update()
    {
        // 自分が操作するオブジェクトでなければ処理をスキップ
        if (!IsOwner) return;

        Move();
    }

    void Move()
    {
        Vector2 inputVector = Vector2.zero;

        // キーボードの入力を取得
        if (Keyboard.current != null)
        {
            // 矢印キーまたはWASDキーの入力をベクトルとして取得
            float moveHorizontal = 0f;
            float moveVertical = 0f;

            if (Keyboard.current.leftArrowKey.isPressed || Keyboard.current.aKey.isPressed) moveHorizontal = -1f;
            if (Keyboard.current.rightArrowKey.isPressed || Keyboard.current.dKey.isPressed) moveHorizontal = 1f;
            if (Keyboard.current.upArrowKey.isPressed || Keyboard.current.wKey.isPressed) moveVertical = 1f;
            if (Keyboard.current.downArrowKey.isPressed || Keyboard.current.sKey.isPressed) moveVertical = -1f;

            inputVector = new Vector2(moveHorizontal, moveVertical).normalized;
        }

        // transform.position で移動
        transform.position += new Vector3(inputVector.x, inputVector.y, 0f) * moveSpeed * Time.deltaTime;
    }
}
  • [Projectウィンドウ][Assetsフォルダ]を選択します
  • [Assetsフォルダ]内を右クリックし、Create > FolderでPrefabsでフォルダを作成します
  • 先ほど作成した[Prefabsフォルダ]にドラッグして、PlayerからPrefabを作成します。
  • 名前をPlayerPrefabに変更します
  • Playerをシーンから削除します。
ヒント

Playerオブジェクトをシーンから削除するのは、このネットワークPrefabをNetworkManagerコンポーネントのPlayer Prefabプロパティに割り当てるためです。ライブラリは、Playerオブジェクトをシーン内に配置されたNetworkObjectとして定義することをサポートしていません。

  • [Hierarchyウィンドウ]の[NetworkManagerゲームオブジェクト] を選択します。
  • [NetworkManager コンポーネント]で、Player Prefab フィールドを探します。
  • 作成したPlayerPrefabをこのフィールドにドラッグします。

補足: 生成されるプレイヤーオブジェクトについて

ここで設定した PlayerPrefab は、接続したプレイヤーごとに自動で生成されます。
ただし、プレイヤーごとに別々の Prefab が選ばれるわけではありません。Host も Client も、同じ PlayerPrefab からそれぞれのプレイヤーオブジェクトが作られます。
同じ Prefab から作られていても、生成されたオブジェクトには「誰が所有しているか」という情報があります。その情報を表すのが OwnerClientId です。
簡単な例として、Host のプレイヤーを青、Client のプレイヤーを赤にしてみます。

using Unity.Netcode;
using UnityEngine;
// 新しいInput Systemを使用するために必要です
using UnityEngine.InputSystem;

public class PlayerController : NetworkBehaviour
{
    [SerializeField]
    float moveSpeed = 4;

    SpriteRenderer spriteRenderer;

    public override void OnNetworkSpawn()
    {
        spriteRenderer = GetComponent<SpriteRenderer>();

        if (OwnerClientId == NetworkManager.ServerClientId)
        {
            spriteRenderer.color = Color.blue;
        }
        else
        {
            spriteRenderer.color = Color.red;
        }
    }

    void Update()
    {
        if (!IsOwner) return;

        Move();
    }

    void Move()
    {
        // キーボードの入力を取得(新しいInput System)
        if (Keyboard.current == null) return;

        float moveHorizontal = 0f;
        float moveVertical = 0f;

        if (Keyboard.current.leftArrowKey.isPressed || Keyboard.current.aKey.isPressed) moveHorizontal = -1f;
        if (Keyboard.current.rightArrowKey.isPressed || Keyboard.current.dKey.isPressed) moveHorizontal = 1f;
        if (Keyboard.current.upArrowKey.isPressed || Keyboard.current.wKey.isPressed) moveVertical = 1f;
        if (Keyboard.current.downArrowKey.isPressed || Keyboard.current.sKey.isPressed) moveVertical = -1f;

        Vector2 movement = new Vector2(moveHorizontal, moveVertical).normalized;
        transform.position += new Vector3(movement.x, movement.y, 0f) * moveSpeed * Time.deltaTime;
    }
}

NetworkManager.ServerClientId はサーバー側の ClientId で、値は 0 です。Host はサーバーとクライアントを兼ねているため、Host のプレイヤーオブジェクトは基本的に OwnerClientId == 0 になります。
一方、Client として接続したプレイヤーには、別の OwnerClientId が割り当てられます。

Host の PlayerObject   OwnerClientId = 0
Client の PlayerObject OwnerClientId = 1 以降

このように、Prefab 自体は同じでも、OwnerClientId を使うことでプレイヤーごとに見た目や処理を変えることができます。

より本格的には: playerNumber を別に管理する

OwnerClientId は Netcode が接続ごとに割り当てる ID です。簡単な色分けや、「Host か Client か」を見分ける程度であれば OwnerClientId で十分です。
ただし、ゲーム内で使う「1P」「2P」のような番号として、そのまま使うとは限りません。
たとえば、画面に 1P2P と表示したい場合や、チーム分け、席順、キャラクター選択と紐づけたい場合は、ゲーム側で playerNumber を別に管理すると扱いやすくなります。
考え方は次のように分けると分かりやすいです。

OwnerClientId = Netcode が接続ごとに割り当てる ID
playerNumber  = ゲーム側で決めるプレイヤー番号

入門段階ではまず OwnerClientId を使って確認し、ゲームとしてルールを増やしていく段階で playerNumber を用意するとよいでしょう。
一点だけ注意です。以前出した PlayerNumberManager のような別クラス方式は、記事のこの段階では少し重いです。ここでは「考え方だけ紹介」くらいに留める方が、初心者向け記事の流れを壊しにくいです。
確認根拠として、Netcode の実装では ServerClientId0NetworkVariable の初期書き込み権限は Server になっています: NetworkManager.cs (line 585)、NetworkVariableBase.cs (line 225)。

インフォメーション

このPrefabをPlayer Prefabスロットにドロップすると、クライアントがゲームに接続した時に、自動的にこのPrefabを接続クライアントのキャラクターとしてスポーンするようにライブラリに指示することになります。もしPlayer PrefabとしてPrefabが設定されていなければ、プレイヤーオブジェクトは生成されません。

ホストの役割とクライアントの役割を選択できるようにします

次のスクリプトを作成し、[NetworkManagerゲームオブジェクト]にアタッチします

using Unity.Netcode;
using UnityEngine;
using UnityEngine.InputSystem;

public class Selector : MonoBehaviour
{
    private string mode = "";

    private void Update()
    {
        var nm = NetworkManager.Singleton;
        if (nm == null) return;
        if (nm.IsClient || nm.IsServer) return;

        var kb = Keyboard.current;
        if (kb == null) return;

        if (kb.hKey.wasPressedThisFrame)
        {
            nm.StartHost();
            mode = "ホスト";
        }
        else if (kb.cKey.wasPressedThisFrame)
        {
            nm.StartClient();
            mode = "クライアント";
        }
    }

    private void OnGUI()
    {
        GUI.skin.label.fontSize = 22;
        GUI.skin.label.normal.textColor = Color.white;

        if (mode == "")
        {
            GUI.Label(new Rect(10, 10, 600, 80),
                "H キー : ホストとして開始\nC キー : クライアントとして開始");
        }
        else
        {
            GUI.Label(new Rect(10, 10, 600, 40), "モード : " + mode);
        }
    }
}

シーンを保存します。

ビルドにシーンを追加する

  • エディター左上のFile > Build Profilesをクリックします。(Unity 6 では Build Settings から名称が変わっています)
  • シーン一覧の右にあるボタンをクリックして、現在のシーンを追加します。

Windowモードでゲームを起動できるようにする(全画面ではないモード)

テストしやすいように、全画面表示ではなく、ウィンドウで実行・ウィンドウの移動ができるようにします

  • エディター左上のEdit > Project Settings…をクリックします。
  • [Playerタブ]を選択します。
  • [Resolution and Presentation]を展開します。
  • Resolution > Fullscreen Modeから、Fullscreen WindowをWindowedに変更します。
  • Resolution > Run In Backgroundにチェックを入れます。
    1台のPCでHostとClientを同時にテストすると、どちらか一方のウィンドウは必ず非アクティブになります。このチェックがオフ(デフォルト)のままだと、非アクティブになった側はネットワーク処理を含めてゲームが停止してしまい、「同期が壊れている」ように見えたり、停止が長く続くとタイムアウトで切断されたりすることがあります。
  • Unityエディターのメインウィンドウに戻り、シーンを保存します。

テストで実行してみる

  • エディターを実行して、Hキーを押下して、Circleのゲームオブジェクトが作成されるのを確認します
  • 上下左右キーで移動できることを確認します
インフォメーション

ホスト側を先に生成する必要がありますので、Cキーではゲームオブジェクトは作成されません

ビルドして、ホスト側、クライアント側としてテストしてみる

今回のサンプルでは、ホスト側を、ビルドして実行している方、クライアント側をUnityエディターで実行している方として構成します

  • エディター左上のFile > Build And Runをクリックします。
  • [Projectフォルダー]内に[Builds]という新しいフォルダーを作成します。
  • 実行ファイル作成。NewworkSampleとして保存します。
  • プロジェクトがビルドされ、新しいウィンドウで起動します。
  • Unityエディター側も実行します
  • 新しいウィンドウの方で、「H」キーを押下します
  • Unityエディターの方で、「C」キーを押下します
  • それぞれのウィンドウをクリックしてフォーカスを変更し、上下左右キーで移動できることを確認します

H キー・C キーを押してから両方の画面にプレイヤーが表示されるまでの流れをシーケンス図にすると、次のようになります。

ホストとクライアントの接続シーケンス図。Hキー・Cキーを押してから両画面にプレイヤーが表示されるまでの流れ

実行中の様子

画面上半分が、ビルドして実行しているウィンドウ、下半分がUnityエディターで実行中のウィンドウになります

参考

NetCodeのホスト、サーバー、クライアント

UnityのNetCodeでは、ネットワーク接続の設定において「ホスト」、「サーバー」、「クライアント」という異なる役割があります。それぞれの役割について詳しく説明します。

ホスト

ホストは、サーバーとクライアントの機能を兼ね備えています。つまり、ホストは自身のゲーム状態を管理しつつ、他のクライアントとのデータの同期も行います。ホストモデルは、特に小規模なマルチプレイヤーゲームや、プレイヤーがサーバーとしても機能する必要がある場合に適しています。

サーバー

サーバーはゲームの状態を中央で管理し、ゲームのロジックを処理します。クライアントからの入力を受け取り、それに基づいてゲームの状態を更新し、その結果をすべてのクライアントに送信します。サーバーは通常、クライアントからは独立しており、ゲームの公正性を保つために中立的な役割を果たします。

クライアント

クライアントはゲームのユーザーインターフェースを担当し、プレイヤーの入力をサーバーに送信します。サーバーからのデータを受け取り、それに基づいて画面上の表示を更新します。クライアントはサーバーの決定に従い、自身のゲームの状態を更新します。

違いと役割

  • ホスト: サーバーとしての役割も担いながら、自らもクライアントとしてゲームに参加します。これにより、ホストがゲーム状態を直接管理し、他のプレイヤーと対話することができます。
  • サーバー: ゲームの全体的な論理と状態を管理し、クライアント間のデータの同期を担当します。サーバーはプレイヤーの一方的な入力に基づく処理を行い、その結果をクライアントに配布します。
  • クライアント: サーバーから送られてくるデータをもとに、プレイヤーの画面にゲーム状態を反映します。クライアントは基本的に受動的な役割を担い、サーバーからの指示に従います。

NetCodeは、これらの役割を効率的に管理し、大規模なマルチプレイヤーゲームでも高いパフォーマンスを提供するよう設計されています。それぞれの役割が明確に分離されていることで、ゲームのスケーラビリティと管理が容易になります。

補足: 生成されるプレイヤーPrefabについて

今回の構成では、Host も Client も NetworkManager に設定した同じ Player Prefab からプレイヤーオブジェクトが生成されます。

つまり、接続したプレイヤーごとに別々のPrefabが自動で選ばれるわけではありません。NetworkManagerPlayer Prefab に登録したPrefabが、各プレイヤー用にインスタンス化されます。

ただし、生成元のPrefabが同じでも、OwnerClientIdIsOwner を使えば、プレイヤーごとに見た目や挙動を変えることができます。例えば、Host と Client で色を変える場合は、OnNetworkSpawn() の中で OwnerClientId を見て Material の色を変更します。

public override void OnNetworkSpawn()
{
    var spriteRenderer = GetComponent<SpriteRenderer>();

    if (OwnerClientId == 0)
    {
        spriteRenderer.color = Color.blue;
    }
    else
    {
        spriteRenderer.color = Color.red;
    }
}

OwnerClientId == 0 は基本的に Host 側のプレイヤーです。Client として接続したプレイヤーには 1 以降の ID が割り当てられます。

このように、Prefab自体は1つでも、生成後に所有者情報を見て初期化することで、プレイヤーごとの差分を作れます。

より本格的には: playerNumber を別に管理する

先ほどの例では、OwnerClientId を使って Host と Client の色を分けました。

ただし、OwnerClientId は Netcode が接続ごとに割り当てる ID です。簡単な判定には便利ですが、ゲーム内の「1P」「2P」のような番号としてそのまま使うとは限りません。

例えば、1P / 2P / 3P のように画面表示したい場合や、再接続しても同じ番号に戻したい場合、チームや席順、キャラクター選択と紐づけたい場合は、ゲーム側で playerNumber を別に管理した方が扱いやすくなります。

考え方としては、サーバー側で接続したプレイヤーに番号を割り当て、その番号を NetworkVariable で各クライアントに同期します。

using Unity.Netcode;
using UnityEngine;

public class PlayerController : NetworkBehaviour
{
    public NetworkVariable<int> PlayerNumber = new NetworkVariable<int>();

    public override void OnNetworkSpawn()
    {
        PlayerNumber.OnValueChanged += OnPlayerNumberChanged;

        ApplyPlayerNumber(PlayerNumber.Value);
    }

    public override void OnNetworkDespawn()
    {
        PlayerNumber.OnValueChanged -= OnPlayerNumberChanged;
    }

    void OnPlayerNumberChanged(int previousValue, int newValue)
    {
        ApplyPlayerNumber(newValue);
    }

    void ApplyPlayerNumber(int playerNumber)
    {
        Debug.Log($"PlayerNumber: {playerNumber}");
    }
}

そして、サーバー側でプレイヤー生成後に番号を設定します。

using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;

public class PlayerNumberManager : MonoBehaviour
{
    int nextPlayerNumber = 1;
    Dictionary<ulong, int> playerNumbers = new Dictionary<ulong, int>();

    void Start()
    {
        NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
    }

    void OnDestroy()
    {
        if (NetworkManager.Singleton == null) return;

        NetworkManager.Singleton.OnClientConnectedCallback -= OnClientConnected;
    }

    void OnClientConnected(ulong clientId)
    {
        if (!NetworkManager.Singleton.IsServer) return;

        int playerNumber = nextPlayerNumber;
        nextPlayerNumber++;

        playerNumbers[clientId] = playerNumber;

        NetworkObject playerObject = NetworkManager.Singleton.ConnectedClients[clientId].PlayerObject;

        PlayerController playerController = playerObject.GetComponent<PlayerController>();
        playerController.PlayerNumber.Value = playerNumber;
    }
}

この例では、接続順に 123 … と playerNumber を割り当てています。

Host:   playerNumber = 1
Client: playerNumber = 2

OwnerClientId は Netcode 側の接続 ID、playerNumber はゲーム側で決めるプレイヤー番号、というように役割を分けて考えると分かりやすいです。

今回のようなシンプルなサンプルでは OwnerClientId だけでも十分ですが、ゲームとしてルールを増やしていく場合は playerNumber を別に持たせると管理しやすくなります。

NetworkVariable<T> は他の変数でも使える

NetworkVariable<T> はジェネリック型なので、playerNumber に限らず、サーバーが決めてクライアントに配る値であれば何にでも応用できます。

// T の部分を変えるだけで使える
public NetworkVariable<float> Health      = new NetworkVariable<float>(100f);
public NetworkVariable<bool>  IsReady     = new NetworkVariable<bool>(false);
public NetworkVariable<Color> PlayerColor = new NetworkVariable<Color>();

値が変わったときの処理は OnValueChanged に登録します。記事内の playerNumber と同じパターンです。

public override void OnNetworkSpawn()
{
    Health.OnValueChanged += (prev, next) =>
    {
        healthBar.value = next;  // UI を更新するなど
    };
}

使える型・使えない型をまとめると次のようになります。

使える型使えない型
intfloatbool などの値型string(直接は不可)
シリアライズ可能な struct参照型(クラス)
Vector2Vector3Color などList<T>、配列
FixedString32Bytes(文字列の代替)任意のクラスオブジェクト

このパターンの本質は「サーバーが値を書き込む → 全クライアントに自動で同期 → OnValueChanged で変化を検知」の 3 ステップです。HP・スコア・準備完了フラグ・チームIDなど、サーバーが決めてクライアントに伝える値であれば何にでも使えます。

リンク

訪問数 178 回, 今日の訪問数 1回

広告