スポンサーリンク
ゲーム制作チュートリアル

Unity Netcode for GameObjects入門|オンライン対戦・協力プレイの作り方

ゲーム制作チュートリアル

「オンライン対戦や協力プレイをUnityで作ってみたいけど、何から手をつけたらいいか分からない」——そんな風に感じていませんか。

プレイヤー同士の位置や体力を同期させる仕組みは、一人用のゲーム制作とは違う難しさがありますよね。

この記事では、Unity公式のネットワークライブラリ「Netcode for GameObjects」を使って、導入からプレイヤーの動きを同期させ、ローカル環境でホストとクライアントの接続を確認するところまでを順番に解説していきます。

Netcode for GameObjectsとは?導入前に知っておきたい前提

Netcode for GameObjectsは、Unityが公式に提供しているマルチプレイヤー向けのネットワークライブラリです。プレイヤーの接続や位置の同期といった、オンラインゲームに欠かせない機能を、比較的少ないコード量で実装できるのが特徴です。

まず押さえておきたいのが、「Host」「Server」「Client」という3つの言葉の違いです。この3つ、なんとなく同じような響きがあって混同しやすいんですよね。

Hostは、サーバーとクライアントの役割を1台のマシンで兼ねる起動モードのことです。友達と2人でテストプレイするときに、片方が「サーバーだけの専用マシン」を用意する必要はなく、片方がHostとして立ち上がれば、そのままプレイヤーとして参加しながらゲーム全体の管理も行えます。

一方でServerは、プレイヤーとしては参加せず、ゲームの進行管理だけを行う存在を指します。個人開発や小規模なマルチプレイでは、まずHostから始めるのが取り組みやすい方法です。

今回のチュートリアルも、このHostとClientという2つの役割を切り替えながら進めていきます。実装を始める前に、この2つの言葉の意味だけ頭に入れておくと、この後の手順がぐっと理解しやすくなりますよ。

なお、この記事で紹介する手順はUnity 2021以降、またはUnity Editor 6000.0.3f1以降での動作を想定しています。バージョンによってメニューの名称や画面の見え方が多少異なる場合があるので、その点だけ心に留めておいてくださいね。

また、この先のスクリプト実装ではC#の基礎的な書き方が前提になります。変数やクラスの扱いに不安がある場合は、先にこちらで基本を確認しておくとスムーズに読み進められます。




Netcode for GameObjectsのインストール方法

インストール自体はとてもシンプルで、Package Managerから検索してボタンを押すだけで完了します。実際の手順を見ていきましょう。

  1. Unity Hubからプロジェクトを開き、「3D」または「2D」の新しいプロジェクトを作成します。
  2. Unityエディターのメニューから Window > Package Manager(またはWindow > Package Management > Package Manager)を選択し、Package Managerウィンドウを開きます。
  3. ウィンドウ左上にある「Packages」ドロップダウンから「Unity Registry」を選択します。
  4. 検索窓に「Netcode for GameObjects」と入力します。
  5. 検索結果に表示された「Netcode for GameObjects」(com.unity.netcode.gameobjects)を選択し、右側の「Install」ボタンをクリックします。

インストールが完了すると、パッケージ名の横に緑色のチェックマークが表示されます。これが表示されていれば、準備は整った状態です。

ワンポイント

インストールに時間がかかっているように見えても、パッケージのダウンロードとコンパイルが裏側で進んでいるだけのことがほとんどです。エディター下部のプログレスバーが消えるまで、少し待ってみてください。

これでNetcode for GameObjectsを使う土台ができました。次は、ネットワーク全体の管理役となる「NetworkManager」を設定していきます。


NetworkManagerを設定する方法

NetworkManagerは、接続や同期といったネットコード関連の設定をすべて管理する、いわば司令塔にあたるコンポーネントです。まずはこれをシーンに配置していきましょう。

  1. Hierarchyウィンドウを右クリックし、「Create Empty」を選択して空のゲームオブジェクトを作成します。
  2. 作成したオブジェクトの名前を「NetworkManager」に変更します。
  3. 「NetworkManager」オブジェクトを選択した状態で、Inspectorウィンドウの「Add Component」をクリックし、「Netcode」>「NetworkManager」を選択してアタッチします。
  4. NetworkManagerコンポーネントにある「Select transport…」(またはNetworkTransportフィールド)のドロップダウンから「UnityTransport」を選択します。
  5. Ctrl+S(macOSの場合はCmd+S)を押して、シーンを保存します。

この「UnityTransport」の設定は、サーバーとクライアントが実際に通信するための通り道を決めるものです。ここが未設定のままだと、この先の手順で接続がうまくいかない原因になるので、忘れずに設定しておいてくださいね。

ここまでで、ネットワークの管理役となる存在がシーンに用意できました。次は、実際に接続してきたプレイヤーごとに生成される「プレイヤーPrefab」を作っていきます。




プレイヤーPrefabを作成し、動きを同期させる方法

ここからは、接続してきたプレイヤーごとに自動で生成されるキャラクターを作っていきます。「NetworkObject」「ClientNetworkTransform」「PlayerController」の3点セットを用意すれば、自分の操作だけが自分のキャラクターを動かす状態が作れます。

  1. Hierarchyウィンドウを右クリックし、「3D Object > Capsule」(2Dの場合は「2D Object > Sprite > Circle」)を作成し、名前を「Player」に変更します。
  2. 「Player」オブジェクトを選択し、「Add Component」>「Netcode」>「NetworkObject」を選択してアタッチします。

NetworkObjectとClientNetworkTransformの役割

NetworkObjectは、NetworkManagerが接続時にこのオブジェクトをスポーンして管理できるようにするための目印のようなコンポーネントです。これがないと、Netcodeはそのオブジェクトをプレイヤーとして扱ってくれません。

次に用意するのが、位置を同期させるためのスクリプトです。プレイヤー自身の移動は、クライアント側で即座に動かしたほうが操作にもたつきが出にくくなります。そのため、クライアント側に移動の権限を持たせる「Client Authoritative」という考え方でスクリプトを作成します。

  1. 新しいC#スクリプトを「ClientNetworkTransform」という名前で作成します。
  2. 以下のコードを記述します。
using Unity.Netcode.Components;

public class ClientNetworkTransform : NetworkTransform
{
    protected override bool OnIsServerAuthoritative()
    {
        return false; // クライアント側に移動権限を与える
    }
}
  1. 作成した「ClientNetworkTransform」スクリプトを、Playerオブジェクトにアタッチします。

IsOwnerで自分のキャラだけを動かす仕組み

続いて、実際にキーボードでキャラクターを動かすスクリプトを作ります。ここで重要になるのがIsOwnerという判定です。

IsOwnerは「自分が操作権を持つオブジェクトかどうか」を表すもので、「自分がサーバー側かどうか」を表すIsServerとは別の軸の判定です。この2つを混同してしまうと、意図しないタイミングでキャラクターが動いてしまったり、逆に動かせなかったりする原因になります。

今回のスクリプトでは、新しいInput SystemのKeyboard.currentを使って入力を取得します。従来の書き方に慣れている場合は少し勝手が違うので、先にこちらで基本の使い方を確認しておくのもおすすめです。

  1. C#スクリプトを「PlayerController」という名前で作成します。
  2. 以下のコードを記述し、Playerオブジェクトにアタッチします。
using Unity.Netcode;
using UnityEngine;
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)
        {
            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 += new Vector3(inputVector.x, inputVector.y, 0f) * moveSpeed * Time.deltaTime;
    }
}

最後に、このPlayerオブジェクトをPrefab化して、NetworkManagerに登録します。

  1. ProjectウィンドウのAssetsフォルダ内に「Prefabs」という名前のフォルダを作成します。
  2. Hierarchyウィンドウから「Player」オブジェクトを「Prefabs」フォルダにドラッグ&ドロップしてPrefab化します。
  3. Hierarchyウィンドウの「Player」オブジェクトを選択し、Deleteキーでシーンから削除します。シーン内に配置したままのプレイヤーオブジェクトはサポートされないため、自動生成に任せる形にします。
  4. Hierarchyウィンドウで「NetworkManager」オブジェクトを選択し、NetworkManagerコンポーネントの「Default Player Prefab」プロパティに、作成した「PlayerPrefab」をドラッグ&ドロップして登録します。

これで、接続してきたプレイヤーごとにキャラクターが生成され、自分の操作だけが自分のキャラクターに反映される状態が整いました。次は、ホストとクライアントを切り替えて起動するための仕組みを作っていきます。




ホスト/クライアントを切り替える接続UIの作り方

プレイヤーの準備ができたら、次は「誰がホストで、誰がクライアントとして参加するか」を選べる仕組みを作ります。1本のスクリプトを用意するだけで、キー入力によってStartHostとStartClientを呼び分けられます。

  1. C#スクリプトを「Selector」という名前で作成します。
  2. 以下のコードを記述します。Hキーを押すとホストとして、Cキーを押すとクライアントとして起動する仕組みです。
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);
        }
    }
}
  1. 作成した「Selector」スクリプトを、Hierarchyウィンドウの「NetworkManager」オブジェクトにアタッチします。

このスクリプトの中で使われているNetworkManager.Singletonは、シーン内のどこからでも同じNetworkManagerを参照できるようにする書き方です。この「Singleton」という考え方自体は、Netcode特有のものではなくUnity開発全般でよく使われる設計パターンなので、気になる場合はこちらも参考にしてみてください。

画面左上に操作案内のテキストが表示され、HキーまたはCキーを押すと「モード:ホスト」または「モード:クライアント」という表示に切り替われば、正しく実装できています。これで接続の役割分担ができるようになったので、次はビルドに向けた設定を進めていきましょう。




ビルド前に必ず設定すべき項目とその理由

いよいよビルドの準備段階ですが、その前に確認しておきたい設定が3つあります。「シーンの登録」「ウィンドウモード」「Run In Background」のいずれかを見落とすと、せっかくビルドしても正しく動作確認ができません。それぞれ何のための設定なのか、理由も含めて見ていきましょう。

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

作成したシーンは、明示的に「ビルド対象」として登録しないと、ビルドしたアプリに含まれません。手順は次の通りです。

  1. メニューの File > Build Profiles(従来のBuild Settingsにあたる画面)を開きます。
  2. シーン一覧の右にある「+」ボタンをクリックするか、「Add Open Scenes」を選択して、現在編集中のシーンを追加します。

この登録を忘れると、ビルドしたアプリを起動しても真っ暗な画面のまま何も表示されない、といった状態になりやすいので注意してください。

ウィンドウモードとRun In Backgroundの設定

1台のPCでホストとクライアントの両方を同時に起動して動作確認するには、画面をフルスクリーンにしないことが前提になります。両方のウィンドウを同時に表示・操作できる状態にしておく必要があるからです。

  1. メニューの Edit > Project Settings… > Player > Resolution and Presentation を開きます。
  2. 「Resolution > Fullscreen Mode」の設定を「Fullscreen Window」から「Windowed」に変更します。
  3. 同じ項目内にある「Run In Background」のチェックを有効にします。

この中でも、特に見落としやすいのが「Run In Background」です。

注意

Run In Backgroundがオフのままだと、フォーカスが外れた(クリックしていない側の)ウィンドウの処理が止まってしまいます。これが原因で「同期がズレる」「急に接続が切れる」といった現象が起きると、スクリプトのバグを疑って時間を使ってしまいがちですが、実際は単純な設定漏れであることが多いので、まずこの項目を確認してみてください。

この3つの設定を済ませておけば、ビルド後のテストで「動かない原因が設定なのかコードなのか分からない」という迷いを減らせます。準備が整ったところで、実際にビルドしてローカルでの接続テストに進んでいきましょう。


ローカル環境でホスト/クライアント接続をテストする方法

設定がすべて整ったら、いよいよ実際に動かして確認する段階です。ビルド版とUnityエディター版を同時に起動し、それぞれをHキー・Cキーで役割分けすれば、1台のPCだけで同期の動作確認ができます。

  1. メニューの File > Build And Run を選択します。
  2. プロジェクトフォルダ内に「Builds」という新しいフォルダを作成し、実行ファイル(例:「NetworkSample」)を保存してビルドを行います。
  3. ビルドが完了すると、アプリケーションのウィンドウが自動で1つ立ち上がります。
  4. Unityエディター側でも「Play」ボタンを押して、シーンを実行します。
  5. 片方のウィンドウでHキーを押してホストとして起動し、もう片方でCキーを押してクライアントとして起動します。
  6. それぞれのウィンドウをクリックしてフォーカスを切り替えながら、キーボード操作でキャラクターを動かしてみます。

両方のウィンドウで、動かした側のキャラクターだけが動き、それがもう一方の画面にもリアルタイムで反映されていれば、同期は正しく機能しています。

もし片方のウィンドウでキャラクターが生成されない、あるいはクリックしても何も反応しないといった場合は、PlayerPrefabの登録漏れやコンポーネントの参照先が空になっていることが原因になっているケースが多いです。そういったエラーへの対処法は、こちらでまとめて確認できます。

ここまで確認できれば、ローカル環境での基本的な接続とプレイヤー同期は完成です。次は、体力やスコアといった「状態」をどう同期させるかという、もう一段踏み込んだ内容を見ていきましょう。




NetworkVariableとRPC、どちらを使うべきか

体力やスコアなどの「状態」を同期させたいとき、多くの人が迷うのがNetworkVariableRPCのどちらを使うべきか、という点です。判断基準はシンプルで、「新しく参加したプレイヤーが、その情報を必要とするかどうか」で決まります。

NetworkVariableは、持続的な「状態」を共有するのに向いています。たとえばプレイヤーの体力のように、ゲームの途中からでも常に最新の値が必要になる情報は、NetworkVariableで管理するのが自然です。途中参加したプレイヤーにも、最新の値が自動で同期される仕組みになっています。

一方でRPCは、爆発エフェクトの発生やダメージ時のSE再生のように、その瞬間にだけ起きる「イベント」の送信に向いています。RPCで送られるメッセージは、その瞬間に接続しているプレイヤーにしか届きません。そのため、ドアの開閉状況のような「状態」をRPCだけで表現しようとすると、途中から参加したプレイヤーにはその結果が伝わらず、クライアントごとに見た目がズレてしまいます。

NetworkVariableを実際に使うときは、書き込み権限の設定も一緒に確認しておく必要があります。デフォルトでは書き込み権限がサーバー側に限定されているため、クライアント自身が値を変更できるようにしたい場合は、次のように権限を指定します。

private NetworkVariable networkData = new NetworkVariable(
    0,                                          // 初期値
    NetworkVariableReadPermission.Everyone,     // 読み取り権限
    NetworkVariableWritePermission.Owner        // 書き込み権限
);

また、値が変わるたびに毎フレーム監視するのは処理として非効率なので、値が変化した瞬間だけ発火するOnValueChangedイベントを使うのが基本の書き方です。

networkData.OnValueChanged += (int oldParam, int newParam) => {
    // 値が変わった際に行う処理(UI更新やエフェクト生成など)
};

NetworkVariableで使える型・使えない型

NetworkVariableは、どんな型でも自由に扱えるわけではありません。使える型と使えない型をあらかじめ知っておくと、実装の途中でつまずきにくくなります。

  • 使える型intfloatboolなどの値型、およびシリアライズ可能な構造体(Vector2Vector3Colorなど)
  • 使えない型:クラスオブジェクトなどの参照型、List<T>、配列、通常のstring
  • 文字列を扱いたい場合Unity.Collections名前空間のFixedString32BytesFixedString64Bytesなど、固定バイト数の文字列型を使います

通常のstringListをそのまま使おうとしてエラーになるのは、初めてNetcodeに触れる人がよくつまずくポイントです。文字列を扱いたいときは固定長の型に置き換える、独自の構造体を使いたいときはINetworkSerializableインターフェースを実装してシリアライズ処理(NetworkSerializeメソッド)を用意する、という2点を覚えておくとスムーズです。

プレイヤー識別子の使い分け

プレイヤーを区別するための識別子にも、目的に応じて2つの選択肢があります。

OwnerClientIdは、Netcodeが接続ごとに自動で割り当てるシステム内部のIDです。Hostは常に「0」(NetworkManager.ServerClientId)となり、Clientには接続した順に「1」以降の番号が割り当てられます。Hostを青、Clientを赤にするといった簡易的な見た目の分岐には、このIDをそのまま使っても問題ありません。

一方で、チーム分けやキャラクター選択、「1P」「2P」といったUI表示のように、ゲーム側のルールとして固有の番号を割り当てたい場合は、OwnerClientIdとは別にplayerNumberのような独自の変数を用意して管理するのがおすすめです。システムが自動で割り振るIDと、ゲームのルールとして意味を持つ番号は、役割が異なるものとして分けて考えると設計に迷いにくくなります。


よくある質問

Q
Netcode for GameObjectsは無料で使えますか?
A
Netcode for GameObjectsはUnity公式が無料で提供しているパッケージで、Package Managerからインストールする際に追加費用は発生しません。ただし、オンライン越しの接続にRelayやLobbyといった別サービスを利用する場合は、Unityの料金プランや通信量によって費用が発生することがあります。まずはこの記事のようにローカル環境で試すところから始めると、費用を気にせず動作を確認できます。
Q
Photon PUNなど他のマルチプレイヤーアセットとの違いは何ですか?
A
Netcode for GameObjectsはUnity純正のライブラリで、Unityのコンポーネントやワークフローに近い形で使える点が特徴です。一方でPhoton PUNのようなサードパーティ製アセットは、マッチメイキングなどオンライン接続まわりの機能があらかじめ充実している場合が多く、目的や規模によって選ぶべき選択肢は変わってきます。個人開発でまず基本的な同期の仕組みを学びたい場合は、公式ライブラリであるNetcode for GameObjectsから触れてみるのが分かりやすい入り口になります。
Q
スマートフォン(Android/iOS)でも動作しますか?
A
Netcode for GameObjects自体はAndroidやiOS向けのビルドにも対応しています。ただし、この記事で紹介した手順は1台のPC上でホストとクライアントを同時に起動して確認するローカルテストが前提になっているため、スマートフォン向けにビルドする場合は入力方法(タッチ操作への対応)や通信環境の準備など、別途調整が必要な項目が出てきます。まずはPC環境で同期の仕組みを理解してから、実機向けの調整に進むのがおすすめです。

※当サイトはアフィリエイト広告を利用しています。リンクを経由して商品を購入された場合、当サイトに報酬が発生することがあります。

※本記事に記載しているAmazon商品情報(価格、在庫状況、割引、配送条件など)は、執筆時点のAmazon.co.jp上の情報に基づいています。
最新の価格・在庫・配送条件などの詳細は、Amazonの商品ページをご確認ください。

スポンサーリンク