Multiplayer Sample
このサンプルでは、ARクラウド Pretia を使ったマルチプレイヤーを紹介するために、マップベースのリローカリゼーションを使って共有座標系を確立しています。マップからシェアードアンカーを見つけることに成功すると、サンプルは接続された各プレイヤーにドローンを生成します。このドローンは所有者のデバイスに追従し、プレイヤーはボタンを押すことでドローンをジャンプさせたり、撮影したりすることができます。このサンプルは、最大8人の同時プレイに対応しています。
シーンを理解する
Multiplayer sampleでは、まず MultiplayerScene
を開いてください。
シーンのヒエラルキー
このシーンにはマルチプレイヤーの実装に必要なコンポーネントが全て含まれています。
NetworkManager
は、SDK が提供するプレハブです。これは、シーンにドラッグ&ドロップすることができます。このプレハブには NetworkManagerComponent
と RegisterNetworkPrefab
という2つの重要なコンポーネントが含まれています。
NetworkManagerComponent
NetworkManagerComponent
内には Settings
フィールドがあり、スクリプト可能な NetworkSettings
オブジェクトを割り当てることができます。新しい NetworkSettings
を作成するには、Project
ウィンドウを右クリックし、Create > Pretia ArCloud > Network > NetworkSettings
を選択します。
Note:このコンポーネントはシングルトンモノハビアで、新しいシーンがロードされると、このゲームオブジェクトはシーンに残ります。
RegisterNetworkPrefab
RegisterNetworkPrefab
コンポーネントは、プレハブのインスタンスを作成する際に、接続しているすべてのプレーヤーに同期させる必要がある場合に必要です。この場合、プレハブに NetworkIdentity
コンポーネントをアタッチし、このコンポーネントの Prefabs
リストに追加する必要があります。プレハブをリストに追加したら、NetworkSpawner.Instantiate
メソッドのいずれかを使用して、同期されたインスタンス化を実現できます。
Game Logic
シーン内には、Directional Light
、Canvas
などの Unity のデフォルトゲームオブジェクトと、AR Foundation のデフォルトゲームオブジェクトとコンポーネントが表示されているのがわかります。
AR Session Origin
ゲームオブジェクトの中には、Pretia AR Cloudのリローカリゼーション機能を使うために必要な AR Shared Anchor Manager
コンポーネントが含まれます。これらのコンポーネントはすべて、リローカリゼーションサンプルに関するドキュメントで説明されています。
クライテリアベースのマップ選択をしているので、このアプリケーションで使っているアプリキーにマップキーを割り当てる必要があることに注意してください。マップキーの割り当ては、開発者コンソールから行うことができます。
代わりに GameSessionInitializer
、ShooterManager
、PlayerIdManager
を見てみましょう。これらのゲームオブジェクトには、このサンプルでマルチプレイ機能を実行するために必要なすべてのコンポーネントが含まれているからです。
GameSessionInitializer
GameSessionInitializer
は MonoBehaviour
スクリプトで IGameSession
インスタンスを準備し初期化します。 ほとんどのメッセージング・シンクロナイゼーションAPIは IGameSession
から公開されており、マルチプレイヤーAPIの非常に重要なコンポーネントです。このスクリプトから、既存のパブリックセッションに参加したり、独自のセッションを作成する方法を確認することもできます。
ShooterManager
ShooterManager
は MonoBehaviour
スクリプトで、ゲームプレイのロジックのほとんどを含んでいます。
リローカライズに成功したときにゲームセッションを接続するためのコールバックを含み、接続されたプレイヤーそれぞれのドローンの初期化、各ドローンのジャンプと射撃のメッセージを送るためのボタンのセットアップ、そのほか多くの処理を行っています。
このスクリプトを理解できれば、マルチプレイヤー機能をどのように利用できるかをより深く理解できるようになるでしょう。
PlayerIdManager
このコンポーネントは、接続されている各プレイヤーが一意のID値を持つことを保証し、その値は接続されているすべてのプレイヤーで同期されます。これは uint?
の配列を作成し、ネットワーキングAPIのネットワークスナップショット機能と同期させることで実現しています。
ネットワークスナップショットは新しく接続されたプレイヤーにメッセージとして送信されるので、この機能を使って重要なコンポーネントを初期化することができます。サンプルでは、各プレイヤーがドローンを持っており、それぞれのドローンは異なる色を持っています。各ドローンの色を同期させるために、この PlayerIdManager
コンポーネントでネットワークスナップショットの機能を利用しています。
マルチプレイヤーの機能を理解する
このサンプルでは、ネットワークAPIに存在するすべてのマルチプレイヤー機能を利用し、それぞれの機能をどのように利用できるかを紹介します。その中でも、Network Message
、Network Spawner
、Network Synchronization
、Network Snapshot
について説明しています。
Network Message
ネットワーキングAPIは、すべてのマルチプレイヤー機能のベースとしてネットワークメッセージを使用しています。同期とスナップショットは、ネットワーク・メッセージの上に構築されます。ネットワークメッセージは struct
や class
を使って定義することができ、NetworkMessage
属性と MessagePackObject
属性を追加することで定義できます。
以下はネットワーク・メッセージの定義の例です。定義、メッセージを受信したときのコールバックの登録/解除、そして実際にメッセージを送信する、という4つの重要な部分があります。
// NetworkShootMsg.cs
[NetworkMessage]
[MessagePackObject]
public struct NetworkShootMsg
{
}
// ShooterManager.cs
// Register the network message callback
private async void OnEnable()
{
// ...
// Use GetLatestSessionAsync() to easily get access to the IGameSession
_gameSession = await NetworkManager.Instance.GetLatestSessionAsync();
// Register the callback for NetworkShootMsg
// In this case we're using PlayerMsg to broadcast the message to all the other connected players
_gameSession.PlayerMsg.Register<NetworkShootMsg>(InvokeShootOnCharacter);
// ...
}
// Unregister the network message callback
private async void OnDisable()
{
// ...
if (_gameSession != null)
{
// Don't forget to unregister the callback
_gameSession.PlayerMsg.Unregister<NetworkShootMsg>(InvokeShootOnCharacter);
}
// ...
}
// The function callback definition
// This function is called whenever the client receives the NetworkShootMsg from the PlayerMsg protocol
private void InvokeShootOnCharacter(NetworkShootMsg msg, Player sender)
{
// If the drone exists for the player
if (_ownerToShooterMap.TryGetValue(sender, out ShooterFacade shooter))
{
// Execute the shoot animation
shooter.AnimController.ShootAnimation();
}
}
// This function is called whenever a player joins the game session, and the drone for the player has just been instantiated
private void SetupCharacter(NetworkIdentity networkIdentity)
{
// ...
// If the local player is the owner of the instantiated drone
// Setup the shoot button to correspond to this drone
if (networkIdentity.Owner == _gameSession.LocalPlayer)
{
// Add a callback to send a NetworkShootMsg with the PlayerMsg protocol whenever we press the shoot button
_shootButton.onClick.AddListener(() => _gameSession.PlayerMsg.Send(new NetworkShootMsg()));
}
// ...
}
サンプルでは、ドローンの撮影を同期させる方法として、ネットワークメッセージを使っています。プレイヤーがシュートボタンを押すと、接続している他のすべてのプレイヤーにメッセージが送信されます。このメッセージは、他のすべてのプレイヤーが受信し、それぞれのデバイスが弾を生成して、ボールを撃つための物理をシミュレートします。
このサンプルでは、すでにドローンの照準と射撃方向を同期させているため、各デバイスでローカルに物理シミュレーションを行うことにしました。
Network Spawner
NetworkSpawner
は、接続されたすべてのプレイヤーにプレハブをインスタンス化する機能です。APIはUnityのインスタンス化APIと似ていますが、唯一違うのはインスタンス化されたプレハブのオーナーを指定する必要がある点です。
// Network Instantiation APIs
public void Instantiate(NetworkIdentity prefab, Vector3 position, Quaternion rotation, Player owner);
public void Instantiate(NetworkIdentity prefab, Vector3 position, Quaternion rotation, NetworkIdentity parent, Player owner);
サンプルでは、この機能を使ってドローンをインスタンス化します。インスタンス化は、リローカライズに成功し、CameraNetworkManager
コンポーネントによるローカルカメラのプロキシが準備できた後に行われます。
private async void OnEnable()
{
// Attach a callback to spawn the drone when the local camera proxy is ready
// The local camera proxy will be ready once the game session is connected successfully,
// and the network camera manager instantiates the local camera proxy
// The sample will try to connect the game session once relocalization has finished successfully
_networkCameraManager.OnLocalProxyReady += SpawnCharacter;
// ...
}
// The function callback definition
// In this function we calculate the position in front of the device
// And instantiate the prefab with the NetworkSpawner.Instantiate method
private void SpawnCharacter(Transform localProxy)
{
var spawnPosition = localProxy.position + (localProxy.forward * SPAWN_FORWARD_DISTANCE);
spawnPosition.y = localProxy.position.y;
_gameSession.NetworkSpawner.Instantiate(_characterPrefab, spawnPosition, Quaternion.identity, _gameSession.LocalPlayer);
}
private void OnDisable()
{
// Don't forget to remove the callback on OnDisable
_networkCameraManager.OnLocalProxyReady -= SpawnCharacter;
// ...
}
Network Synchronization
あるコンポーネントの値が変わるたび、その値を更新したいときに、Network Synchronization
を使用します。サンプルでは、この機能を使って各ドローンの位置と回転、そしてエイムを同期させます。この機能の使い方の例として、NetworkDrone.cs
を見てみましょう。
// In order to use the network synchronization feature,
// it's important to derive from the NetworkBehaviour class
public class NetworkDrone : NetworkBehaviour
{
// Enable this flag to use the latest NetworkVariable feature for synchronization
protected override bool NetSyncV2 => true;
// Prepare the component dependency
[SerializeField] private Rigidbody _rigidbody;
// Define the network variables to synchronize
// NetworkVariable lets us define synchronizable fields/properties in a more concise manner
private NetworkVariable<Vector3> Pos;
private NetworkVariable<float> RotY;
[SerializeField] private Transform _barrel;
private NetworkVariable<Quaternion> BarrelRot;
// Initialize and set the initial value for the network variables
private void Awake()
{
Pos = new NetworkVariable<Vector3>(_rigidbody.position);
RotY = new NetworkVariable<float>(_rigidbody.rotation.eulerAngles.y);
BarrelRot = new NetworkVariable<Quaternion>(_barrel.localRotation);
}
// SyncUpdate gets called every network tick.
// This function is only called on the player that has authority over the game object.
// In this function, we update the network variables with their updated values.
protected override void SyncUpdate(int tick)
{
Pos.Value = _rigidbody.position;
RotY.Value = _rigidbody.rotation.eulerAngles.y;
BarrelRot.Value = _barrel.localRotation;
}
// SerializeNetworkVars gets called every network tick, before sending the synchronization updates.
// In this function, we should call the Write function for each network variables we want to synchronize
protected override void SerializeNetworkVars(ref NetworkVariableWriter writer)
{
writer.Write(Pos);
writer.Write(RotY);
writer.Write(BarrelRot);
}
// DeserializeNetworkVars gets called after receiving the synchronization updates.
// In this function, we should call the Read function for each network variables.
// Make sure that the reads are in the same order as the writes
protected override void DeserializeNetworkVars(ref NetworkVariableReader reader)
{
reader.Read(Pos);
reader.Read(RotY);
reader.Read(BarrelRot);
}
// ApplySyncUpdate gets called immediately after deserializing the received synchronization updates.
// In this function, we should apply the updated values to their corresponding components
protected override void ApplySyncUpdate(int tick)
{
_rigidbody.position = Pos.Value;
_rigidbody.rotation = Quaternion.Euler(_rigidbody.rotation.eulerAngles.x, RotY.Value, _rigidbody.rotation.eulerAngles.z);
_barrel.localRotation = BarrelRot.Value;
}
}
ドローンの同期以外にも、サンプルではこの機能を利用して、接続されているすべてのデバイスのポーズを同期しています。これは、
AR Camera
ゲームオブジェクトにアタッチされたNetworkCameraManager
コンポーネントを使用して行われます。NetworkCameraManager
コンポーネントは、SDK が提供するスクリプトで、接続されているすべてのデバイスのポーズを簡単に同期させることができます。
Network Snapshot
このチュートリアルでは、ネットワークスナップショットについて少し触れています。
Network Snapshot
は、新しく接続されたプレイヤーの初期状態を同期させるための機能です。
プレイヤーがゲームセッションに参加するとき、現在のゲームセッションのスナップショットをホストに要求することで、状態を初期化する必要があります。ホストはこの要求を受信し、セッションの最新のスナップショットを新しいプレーヤーに送信します。プレイヤーはスナップショットを受信し、それを適用して、初期状態を同期させることができます。この機能を使用する例として、PlayerIdManager
の実装を見てみましょう。
// Define a network message to use for the snapshot
[NetworkMessage]
[MessagePackObject]
public class PlayerIdSnapshotMsg
{
[Key(0)]
public uint?[] UserNumbers;
}
// Derive from the ISnapshot interface
public class PlayerIdManager : MonoBehaviour, ISnapshot
{
// ...
// Register the callback when the host receives a snapshot request
public void RegisterSnapshotCallback(HostToPlayerMessageHandler hostToPlayerMsg)
{
hostToPlayerMsg.Register<PlayerIdSnapshotMsg>(SetupUserNumbers);
}
// Enqueue a network message containing the snapshot values
public void EnqueueSnapshot(HostToPlayerMessageHandler hostToPlayerMsg)
{
hostToPlayerMsg.Enqueue(new PlayerIdSnapshotMsg { UserNumbers = _userNumbers });
}
// The function callback definition. In this case we update the variable value and find the next id we can use.
private void SetupUserNumbers(PlayerIdSnapshotMsg msg)
{
_userNumbers = msg.UserNumbers;
_nextUsedId = FindNextUsedId();
}
// ...
}
サンプルをビルドする
ビルド設定に MultiplayerScene
が追加されていることを確認してください。
アプリケーションをビルドする前に、いくつかの設定を行う必要があります。アプリケーションのビルド方法については、こちらのガイドを参照してください。
サンプルを実行するRunning the Sample
端末でサンプルを実行し、スタートボタンを押して、アプリケーションに割り当てられたマップキーの領域をリローカライズします。リローカリゼーションが成功すると、カメラの前にドローンが表示されます。このドローンがあなたのキャラクターで、あなたがデバイスを動かすとどこにでもついてくるはずです。また、ドローンを操作して射撃やジャンプをすることも可能です。
1つのデバイスでサンプルを実行しても、マルチプレイヤー機能の特性を活かせませんので、他のデバイスでサンプルを実行するか、Unity Editorからサンプルを実行してみてください。Unity Editorを使用した場合、リローカライズすることなくゲームに参加できるはずです。また、マウスを使ってデバイスの動きをシミュレートすることで、カメラを操作することができます。