unity-multiplayer について
unity-multiplayerは、Unityでネットワーク対応ゲームを開発するためのリファレンススキルです。Netcode for GameObjects、ネットワーク構成、状態同期やRPCに加え、Unityのマルチプレイヤーサービスと実装上の注意点を扱います。
主な機能
- 01Netcode for GameObjectsの設定
- 02ネットワークオブジェクトと所有権管理
- 03NetworkVariablesとRPC
- 04接続承認とシーン同期
- 05RelayとLobbyのワークフロー
- 06マルチプレイヤーのアンチパターン指針
活用シーン
- 01ホストまたはクライアント・サーバー型ネットワークの設定
- 02プレイヤーとゲーム状態の同期
- 03ネットワークオブジェクトの所有権とスポーン管理
- 04RelayとLobbyを介したプレイヤー接続
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ nice-wolf-studio/ unity-claude-skills --skill unity-multiplayerインストール後、Claude Code で/unity-multiplayerを入力して読み込まれたか確認します。
同梱ファイルreferences/netcode-gameobjects.mdreferences/transport-layer.md
詳細
Unity マルチプレイヤーとネットワーキング
マルチプレイヤーアーキテクチャの概要
Unity のマルチプレイヤーエコシステムはいくつかのレイヤーで構成されています。
| レイヤー | パッケージ/サービス | 目的 |
|---|---|---|
| ハイレベル | Netcode for GameObjects | GameObject ベースのネットワーキングロジック |
| ハイレベル | Netcode for Entities | DOTS ベースのネットワーキング |
| ローレベル | Unity Transport (com.unity.transport) | 信頼性、順序制御、フラグメンテーションをオプションで利用できる UDP/WebSocket 通信 |
| サービス | Relay | クラウドリレーサーバー経由の NAT トラバーサル |
| サービス | Lobby | マッチメイキングとセッションの検出 |
| サービス | Sessions SDK | プレイヤーグループの管理 |
| ツール | Multiplayer Play Mode | エディター内で最大 4 人のプレイヤーをシミュレート |
| ツール | Multiplayer Tools | 分析、デバッグ、テスト用ユーティリティ |
トポロジーの選択肢:
- クライアント/サーバー(専用): サーバーが権限を持ち、クライアントは入力を送信し、サーバーが検証します
- クライアント/サーバー(リッスン/ホスト): 1 人のプレイヤーがサーバーとクライアントの両方として動作します
- 分散権限: 所有権ベースの権限をクライアント間で分散します
ゲームのニーズに基づくパッケージの推奨を確認するには、Multiplayer Center(Window > Multiplayer > Multiplayer Center)を使用します。
Netcode for GameObjects のセットアップ
インストール
Package Manager から com.unity.netcode.gameobjects(v2.10+)をインストールします。これにより com.unity.transport も自動的にインストールされます。
NetworkManager の設定
シーン内の GameObject に NetworkManager コンポーネントを追加します。これはすべてのネットワーキングにおけるシングルトンのエントリーポイントです。
using Unity.Netcode;
public class GameLauncher : MonoBehaviour
{
public void StartAsHost()
{
NetworkManager.Singleton.StartHost();
}
public void StartAsServer()
{
NetworkManager.Singleton.StartServer();
}
public void StartAsClient()
{
NetworkManager.Singleton.StartClient();
}
public void Shutdown()
{
NetworkManager.Singleton.Shutdown();
}
}
NetworkManager の主なプロパティ:
IsServer、IsClient、IsHost-- 実行コンテキストConnectedClients-- 接続中のクライアントのディクショナリConnectedClientsIds-- クライアント ID の読み取り専用リストLocalClientId-- ローカルクライアントの IDSceneManager--NetworkSceneManagerインスタンスSpawnManager--NetworkSpawnManagerインスタンスNetworkConfig-- プロジェクトのネットワーク設定
NetworkManager の主なイベント: OnClientConnectedCallback、OnClientDisconnectCallback、OnConnectionEvent、OnServerStarted/OnServerStopped、OnClientStarted/OnClientStopped、OnTransportFailure
NetworkObject と NetworkBehaviour
NetworkObject
ネットワーク化されたすべての GameObject に NetworkObject コンポーネントが必要です。ID、所有権、可視性を提供します。
主なプロパティ:
NetworkObjectId(ulong)-- ネットワーク全体で同期される一意の IDIsSpawned-- ネットワーク上でスポーン済みかどうかOwnerClientId-- 現在の所有者のクライアント IDIsOwner-- ローカルプレイヤーがこのオブジェクトを所有している場合は trueHasAuthority-- ローカルインスタンスが権限を持つ場合は true
スポーン(サーバー側のみ):
// Basic spawn
NetworkObject netObj = Instantiate(prefab).GetComponent<NetworkObject>();
netObj.Spawn();
// Spawn with specific owner
netObj.SpawnWithOwnership(clientId);
// Spawn as player object
netObj.SpawnAsPlayerObject(clientId);
// Despawn
netObj.Despawn(destroy: true);
所有権(サーバー側のみ):
netObj.ChangeOwnership(newClientId);
netObj.RemoveOwnership();
可視性:
netObj.NetworkShow(clientId);
netObj.NetworkHide(clientId);
bool visible = netObj.IsNetworkVisibleTo(clientId);
NetworkBehaviour
ネットワーク化されたすべてのスクリプトは、MonoBehaviour ではなく NetworkBehaviour を継承します。
ライフサイクルメソッド(順序どおり):
OnNetworkPreSpawn(ref NetworkManager)-- スポーン処理の前OnNetworkSpawn()-- NetworkObject のスポーン後。ここでハンドラーを登録しますOnNetworkPostSpawn()-- すべての兄弟 NetworkBehaviour のスポーン後OnNetworkPreDespawn()-- デスポーン処理の前OnNetworkDespawn()-- デスポーン時
所有権コールバック:
OnGainedOwnership()/OnLostOwnership()OnOwnershipChanged(ulong previous, ulong current)-- すべてのクライアントで発生します
using Unity.Netcode;
public class PlayerController : NetworkBehaviour
{
public override void OnNetworkSpawn()
{
if (IsOwner)
{
// Initialize local player controls
EnableInput();
}
}
void Update()
{
if (!IsOwner) return;
// Only the owner processes input
HandleMovement();
}
public override void OnNetworkDespawn()
{
// Cleanup
}
}
状態チェック: IsServer、IsClient、IsHost、IsOwner、IsSpawned、IsLocalPlayer、HasAuthority
NetworkVariables
NetworkVariable<T> は状態をサーバーからすべてのクライアントへ自動的に同期します。型 T は unmanaged(プリミティブ型、アンマネージド構造体)である必要があります。
public class PlayerHealth : NetworkBehaviour
{
public NetworkVariable<int> Health = new NetworkVariable<int>(
value: 100,
readPerm: NetworkVariableReadPermission.Everyone,
writePerm: NetworkVariableWritePermission.Server
);
public override void OnNetworkSpawn()
{
Health.OnValueChanged += OnHealthChanged;
}
public override void OnNetworkDespawn()
{
Health.OnValueChanged -= OnHealthChanged;
}
private void OnHealthChanged(int oldValue, int newValue)
{
Debug.Log($"Health changed: {oldValue} -> {newValue}");
UpdateHealthUI(newValue);
}
// Server-side only (due to WritePerm.Server)
public void TakeDamage(int amount)
{
if (!IsServer) return;
Health.Value -= amount;
}
}
書き込み権限:
NetworkVariableWritePermission.Server(デフォルト)-- サーバーのみ書き込み可能NetworkVariableWritePermission.Owner-- 所有者のみ書き込み可能
NetworkList -- 同期リスト(T は unmanaged と IEquatable<T> を満たす必要があります):
public class Inventory : NetworkBehaviour
{
public NetworkList<int> Items;
void Awake()
{
Items = new NetworkList<int>();
}
public override void OnNetworkSpawn()
{
Items.OnListChanged += OnItemsChanged;
}
private void OnItemsChanged(NetworkListEvent<int> changeEvent)
{
Debug.Log($"List changed: {changeEvent.Type}");
}
}
RPC(ServerRpc、ClientRpc)
RPC は、サーバーとクライアント間のリモートプロシージャコールです。メソッドは NetworkBehaviour 内に定義し、送信先として SendTo ターゲットを指定した [Rpc] 属性を使用する必要があります。
統合 Rpc 属性(v2.x)
public class CombatSystem : NetworkBehaviour
{
// Server executes this when any client calls it
[Rpc(SendTo.Server)]
void AttackRpc(int targetId, RpcParams rpcParams = default)
{
ulong senderId = rpcParams.Receive.SenderClientId;
ProcessAttack(senderId, targetId);
}
// All clients (and host) execute this
[Rpc(SendTo.ClientsAndHost)]
void ShowDamageEffectRpc(Vector3 position, int damage)
{
SpawnDamagePopup(position, damage);
}
// Only the owner executes this
[Rpc(SendTo.Owner)]
void NotifyOwnerRpc(string message)
{
Debug.Log(message);
}
// Everyone including sender
[Rpc(SendTo.Everyone)]
void PlaySoundRpc(int soundId)
{
AudioManager.Play(soundId);
}
}
SendTo ターゲット
| ターゲット | 説明 |
|---|---|
Server | サーバーで実行します。サーバー上で呼び出された場合はローカルで実行します |
NotServer | サーバーを除くすべてのクライアント(ホストを除く) |
Owner | オブジェクトの所有者のみ |
NotOwner | 所有者以外の全員 |
Authority | クライアント・サーバー方式ではサーバー、分散オーソリティ方式では所有者 |
NotAuthority | オーソリティを持たないすべてのインスタンス |
ClientsAndHost | ホストを含むすべてのクライアント |
Everyone | オブザーバーリスト上のすべてのインスタンス |
Me | ローカルでのみ実行 |
NotMe | 送信者以外の全員 |
SpecifiedInParams | RpcSendParams を介して実行時にターゲットを設定 |
従来の属性(引き続きサポート)
[ServerRpc]
void RequestSpawnServerRpc(ServerRpcParams rpcParams = default)
{
// Runs on server; only owner can call by default
}
[ClientRpc]
void UpdateUIClientRpc(int score)
{
// Runs on all clients
}
接続承認
NetworkManager の ConnectionApprovalCallback を使用して、接続するクライアントを検証します。
// Server-side: register approval callback
NetworkManager.Singleton.ConnectionApprovalCallback = (request, response) =>
{
string password = System.Text.Encoding.UTF8.GetString(request.Payload);
response.Approved = (password == "secret");
response.CreatePlayerObject = response.Approved;
// Optionally set: response.PlayerPrefabHash, response.Position, response.Rotation
if (!response.Approved) response.Reason = "Invalid password";
response.Pending = false; // Signal decision is made
};
// Client-side: set payload before connecting
NetworkManager.Singleton.NetworkConfig.ConnectionData =
System.Text.Encoding.UTF8.GetBytes("secret");
NetworkManager.Singleton.StartClient();
シーン管理
NetworkSceneManager(NetworkManager.Singleton.SceneManager 経由でアクセス)は、同期されたシーンの読み込みを処理します。
public class GameSceneManager : NetworkBehaviour
{
public void LoadGameScene()
{
if (!IsServer) return;
// Server-only: loads scene on all clients
NetworkManager.Singleton.SceneManager.LoadScene("GameScene", LoadSceneMode.Single);
}
public void LoadAdditiveScene()
{
if (!IsServer) return;
NetworkManager.Singleton.SceneManager.LoadScene("Arena", LoadSceneMode.Additive);
}
public override void OnNetworkSpawn()
{
NetworkManager.Singleton.SceneManager.OnSceneEvent += OnSceneEvent;
}
void OnSceneEvent(SceneEvent sceneEvent)
{
switch (sceneEvent.SceneEventType)
{
case SceneEventType.LoadComplete:
Debug.Log($"Client {sceneEvent.ClientId} loaded {sceneEvent.SceneName}");
break;
case SceneEventType.LoadEventCompleted:
Debug.Log($"All clients loaded {sceneEvent.SceneName}");
break;
case SceneEventType.SynchronizeComplete:
Debug.Log($"Client {sceneEvent.ClientId} fully synchronized");
break;
}
}
}
シーンイベントの種類: OnLoad、OnUnload、OnSynchronize、OnLoadComplete、OnUnloadComplete、OnLoadEventCompleted、OnUnloadEventCompleted、OnSynchronizeComplete
重要: シーンイベントのコールバック内で新しいシーンイベントを開始しないでください。
Unity Multiplayer Services(Relay、Lobby)
統合例の全容は
references/transport-layer.mdを参照してください。
Unity Relay
Relay はクラウドリレーサーバーを介して NAT のホールパンチングを実現します(ポートフォワーディングは不要です)。UGS 認証が必要です。
ホストのフロー: アロケーションを作成し、参加コードを取得して、トランスポートを設定し、ホストを開始します。 クライアントのフロー: コードを使用してアロケーションに参加し、トランスポートを設定し、クライアントを開始します。
// Host: create relay and start
Allocation allocation = await RelayService.Instance.CreateAllocationAsync(maxPlayers);
string joinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId);
transport.SetRelayServerData(allocation.ToRelayServerData("dtls")); // "dtls" = encrypted UDP
NetworkManager.Singleton.StartHost();
// Client: join relay and connect
JoinAllocation join = await RelayService.Instance.JoinAllocationAsync(joinCode);
transport.SetRelayServerData(join.ToRelayServerData("dtls"));
NetworkManager.Singleton.StartClient();
Unity Lobby
Lobby はセッションの検索とマッチメイキングを提供します。Relay の参加コードはロビーのデータに保存します。
// Create lobby with relay join code
_lobby = await LobbyService.Instance.CreateLobbyAsync(name, maxPlayers, new CreateLobbyOptions {
Data = new Dictionary<string, DataObject> {
{ "JoinCode", new DataObject(DataObject.VisibilityOptions.Member, relayJoinCode) }
}
});
// Query available lobbies
QueryResponse response = await Lobbies.Instance.QueryLobbiesAsync(new QueryLobbiesOptions {
Filters = new List<QueryFilter> {
new QueryFilter(QueryFilter.FieldOptions.AvailableSlots, "0", QueryFilter.OpOptions.GT)
}
});
// Join and extract relay code
Lobby lobby = await LobbyService.Instance.JoinLobbyByIdAsync(lobbyId);
string joinCode = lobby.Data["JoinCode"].Value;
// IMPORTANT: Send heartbeats every 15s or lobby expires
await LobbyService.Instance.SendHeartbeatPingAsync(lobby.Id);
よくあるパターン
カスタム Prefab を使用したプレイヤーのスポーン(サーバー側)
// In a NetworkBehaviour on the server:
NetworkManager.Singleton.OnClientConnectedCallback += (ulong clientId) => {
GameObject player = Instantiate(playerPrefab);
player.GetComponent<NetworkObject>().SpawnAsPlayerObject(clientId);
};
オーナー権限による移動
public class PlayerMovement : NetworkBehaviour
{
public NetworkVariable<Vector3> Position = new(writePerm: NetworkVariableWritePermission.Owner);
void Update()
{
if (!IsOwner) return;
// Note: Uses legacy Input for brevity. See unity-input for the new Input System.
Vector3 move = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
transform.position += move * Time.deltaTime * 5f;
Position.Value = transform.position;
}
}
入力 RPC を使用したサーバー権限方式
public class ServerAuthMovement : NetworkBehaviour
{
[Rpc(SendTo.Server)]
void MoveRpc(Vector3 input) { transform.position += input * Time.deltaTime * 5f; }
void Update()
{
if (!IsOwner) return;
MoveRpc(new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"))); // legacy Input; see unity-input
}
}
アンチパターン
-
権限なしで NetworkVariable に書き込む -- 書き込めるのはサーバー(または
WritePerm.Ownerを持つ所有者)のみです。クライアントからの書き込みは通知なく無視されます。 -
Update で
IsOwnerチェックを忘れる -- オーナーのチェックがないと、すべてのクライアントで入力処理が実行され、状態の競合が発生します。 -
SpawnなしでInstantiateを使用する --Instantiateのみで作成されたオブジェクトはローカルにしか存在しません。ネットワーク上で見えるようにするには、必ずNetworkObjectに対してSpawn()を呼び出してください。 -
クライアントコードからスポーンする -- NetworkObject をスポーンできるのはサーバーのみです。クライアントはスポーンを要求する RPC を送信する必要があります。
-
NetworkVariable ではなく RPC に大きなデータを含める -- RPC は送信して終わりのため、後から参加したクライアントには届きません。永続的な状態には NetworkVariable を使用してください。
-
OnValueChanged の購読解除を忘れる --
OnNetworkSpawnで購読し、リークを防ぐためにOnNetworkDespawnで購読を解除します。 -
シーンイベントのコールバック内でシーンイベントを開始する --
NetworkSceneManagerで禁止されており、未定義の動作を引き起こします。 -
Lobby のハートビートを送信しない -- 15~30 秒ごとに
SendHeartbeatPingAsyncを呼び出さないと、ロビーは期限切れになります。 -
頻繁な小規模更新に
NetworkVariableを使用する -- 高頻度のデータ(位置など)には、NetworkTransformまたはカスタムシリアライゼーションを優先してください。 -
OnNetworkSpawnより前に RPC を呼び出す -- RPC には NetworkObject がスポーン済みである必要があります。OnNetworkSpawnまで呼び出しを延期してください。
主要 API クイックリファレンス
| クラス | 主なメンバー |
|---|---|
NetworkManager | StartHost(), StartServer(), StartClient(), Shutdown(), Singleton, ConnectedClients, LocalClientId, SceneManager, ConnectionApprovalCallback |
NetworkObject | Spawn(), SpawnWithOwnership(), SpawnAsPlayerObject(), Despawn(), ChangeOwnership(), NetworkShow(), NetworkHide(), NetworkObjectId, OwnerClientId, IsOwner, HasAuthority |
NetworkBehaviour | OnNetworkSpawn(), OnNetworkDespawn(), OnGainedOwnership(), OnLostOwnership(), IsServer, IsClient, IsHost, IsOwner, IsSpawned, HasAuthority, RpcTarget |
NetworkVariable<T> | .Value, OnValueChanged, ReadPerm, WritePerm, CheckDirtyState() |
NetworkList<T> | Add(), Remove(), Insert(), Clear(), Count, OnListChanged |
NetworkSceneManager | LoadScene(), UnloadScene(), OnSceneEvent |
[Rpc(SendTo.X)] | Server, Owner, NotOwner, ClientsAndHost, Everyone, NotMe, Authority, SpecifiedInParams |
関連スキル
- unity-foundations -- Unity の基本概念、GameObject、コンポーネント、シーンの階層構造
- unity-scripting -- C# スクリプティング、MonoBehaviour のライフサイクル、コルーチン
- unity-physics -- 物理システム。同期待ちの物理演算には
NetworkRigidbodyを使用します