概要
他のプレイヤーと一緒に遊ぶマルチプレイヤーゲームは、非常に魅力的ですが、その開発はシングルプレイヤーゲームとは全く異なる複雑さを伴います。各プレイヤーのPCやスマートフォン(クライアント)は、ネットワークを介して互いの状態を同期し続けなければなりません。誰かが動けば、その位置情報を他の全員に送信し、全員の画面でそのキャラクターが同じように動いているように見せる必要があります。
このネットワーク通信とデータ同期の複雑な処理を助けるためのフレームワークが、ネットワーキングソリューションです。Unityには、かつてUNetという古いソリューションがありましたが、現在は公式の新しいソリューションとしてNetcode for GameObjectsが提供されています(他にもPhotonなどサードパーティ製の有名なソリューションも多数存在します)。
Netcode for GameObjectsは、既存のGameObjectとコンポーネントのワークフローに統合される形で設計されており、比較的スムーズにマルチプレイヤー開発を始めることができます。
この記事では、Netcode for GameObjectsの最も基本的な概念である「サーバーとクライアント」「オブジェクトの同期」「RPC」について解説します。
サーバーとクライアント (Server & Client)
マルチプレイヤーゲームでは、ゲームの状態を管理し、誰が何をしたかを決定する「権威」を持つ存在が必要です。この役割を担うのがサーバー (Server) です。各プレイヤーはクライアント (Client) としてサーバーに接続し、サーバーの決定に従います。このモデルをサーバーオーソリティ (Server Authority) と呼び、チートを防ぎ、全プレイヤーのゲーム状態の一貫性を保つための標準的なアーキテクチャです。
Netcode for GameObjectsでは、以下の3つのモードで動作させることができます。
- Server: サーバーとしてのみ動作します。通常、プレイヤーは操作できず、ゲームの管理に専念します。
- Client: サーバーに接続するクライアントとして動作します。
- Host: サーバーとクライアントの両方の役割を兼ね備えます。プレイヤーの一人がサーバー役を兼任する、小規模なP2P(ピアツーピア)ゲームでよく使われます。
NetworkManagerとトランスポート層
Netcode for GameObjectsを動かす中心的なコンポーネントがNetworkManagerです。これは、ネットワークセッションの管理、接続、オブジェクトの生成などを行うシングルトンです。
NetworkManagerは、実際のデータの送受信を行うトランスポート (Transport) 層コンポーネントを必要とします。標準では、ローカル開発やP2Pに適したUnity Transportが使われます。専用サーバーを立てる場合は、TCPやUDPベースの他のトランスポートを選択することもできます。
NetworkObjectとNetworkTransform
ネットワーク上で同期したいオブジェクト(プレイヤーキャラクター、動く床、敵など)には、NetworkObjectコンポーネントをアタッチする必要があります。これにより、各オブジェクトはネットワーク上で一意なIDを割り当てられ、管理対象となります。
さらに、そのオブジェクトの位置、回転、スケールを自動的に同期したい場合は、NetworkTransformコンポーネントを追加します。これだけで、サーバー側で動かしたオブジェクトの位置が、自動的にすべてのクライアントに送信され、同期されるようになります。
NetworkVariable:変数の同期
プレイヤーの体力、スコア、所持弾薬数といった変数を同期したい場合は、NetworkVariable<T>を使います。
using Unity.Netcode;
public class PlayerHealth : NetworkBehaviour
{
// 体力変数をNetworkVariableで定義
// 第3引数で、誰が書き込み可能かを設定(デフォルトはサーバーのみ)
public NetworkVariable<int> CurrentHealth = new NetworkVariable<int>(100, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
// 値が変更された時にクライアント側で呼ばれるイベント
public override void OnNetworkSpawn()
{
CurrentHealth.OnValueChanged += OnHealthChanged;
}
private void OnHealthChanged(int previousValue, int newValue)
{
Debug.Log($"Health changed from {previousValue} to {newValue}");
// ここで体力バーUIを更新するなどの処理を行う
}
// サーバー側で体力を減らす処理
public void TakeDamage(int amount)
{
if (!IsServer) return; // サーバーでなければ何もしない
CurrentHealth.Value -= amount;
}
}
NetworkBehaviourを継承したクラス内でNetworkVariable<T>を定義すると、サーバー側で.Valueプロパティを変更するだけで、その値が自動的に全クライアントに同期されます。クライアント側では、OnValueChangedイベントを購読することで、値の変更を検知してUIの更新などを行うことができます。
RPC (Remote Procedure Call) - リモートプロシージャコール
変数の同期だけでなく、「クライアントからサーバーへ攻撃の意思を伝える」「サーバーから全クライアントへエフェクト再生を命令する」といった、特定の処理を特定の相手に実行させたい場合があります。これがRPC (Remote Procedure Call) の役割です。
Netcode for GameObjectsでは、メソッドに属性を付けることでRPCを定義します。
[ServerRpc]: クライアントからサーバーへ向けて実行を要求するメソッド。クライアントがこのメソッドを呼び出すと、実際にはサーバー上でそのメソッドが実行されます。プレイヤーの入力(攻撃ボタンを押した、など)をサーバーに伝えるために使います。[ClientRpc]: サーバーから全クライアントへ向けて実行を命令するメソッド。サーバーがこのメソッドを呼び出すと、接続しているすべてのクライアント上でそのメソッドが実行されます。派手なエフェクトの再生や、サウンドの再生など、全プレイヤーの画面で同時に起こってほしいイベントに使います。
public class PlayerActions : NetworkBehaviour
{
// プレイヤーが攻撃ボタンを押した時にクライアント側で呼ばれる
public void RequestAttack()
{
// サーバーに対してAttackServerRpcの実行を要求
AttackServerRpc();
}
[ServerRpc] // クライアントからサーバーへ
private void AttackServerRpc()
{
// このコードはサーバーでのみ実行される
Debug.Log("Server received attack request.");
// ここで実際の当たり判定などの処理を行う
// 結果として、全クライアントにエフェクト再生を命令
PlayAttackEffectClientRpc();
}
[ClientRpc] // サーバーから全クライアントへ
private void PlayAttackEffectClientRpc()
{
// このコードは全クライアントで実行される
Debug.Log("Playing attack effect on client.");
// パーティクルエフェクトを再生するなどの処理
}
}
まとめ
Netcode for GameObjectsは、Unityにおけるマルチプレイヤー開発の強力なエントリーポイントです。
- ゲームロジックの権威は
サーバーが持ち、各プレイヤーはクライアントとして接続する。 NetworkManagerがセッションを管理し、NetworkObjectが同期対象のオブジェクトを示す。- 位置の同期は
NetworkTransform、変数の同期はNetworkVariable<T>で行う。 - 特定の処理の実行を依頼するには
RPCを使う。クライアント→サーバーは[ServerRpc]、サーバー→クライアントは[ClientRpc]。
ネットワークプログラミングは、ラグ(遅延)の問題や、状態同期の整合性をどう取るかなど、奥が深い分野です。しかし、これらの基本的なコンポーネントと概念を理解することが、オンラインマルチプレイヤーゲーム開発の第一歩となります。