Multiplay using Netcode for GameObjects
ここではNGOラッパーについて学習します。
- 学習時間の目安
- 60分
- Unityバージョン
- 2021.3.16f1
NGOラッパーの学習では学習用に用意したプロジェクトを使います。 この学習用のプロジェクトはCoreの学習で構築したアプリケーションアーキテクチャをベースに作成しています。 Coreの学習を実施していない方はこの学習より先にCoreの学習を実施することを推奨します。
NGOラッパーがセットアップされた学習用のプロジェクトを使って、バーチャル空間でマルチプレイできるようにサーバーとアプリケーションの実装を追加していきます。
Prepare project
まずはプロジェクトを準備しましょう。
学習用のプロジェクトをクローンします。
https://github.com/extreal-dev/Extreal.Learning.Multiplay.NGO.git
Unityエディタでクローンしたプロジェクトを開きます。
プロジェクトの内容を確認しましょう。
Assets
ディレクトリに次の3つのディレクトリを作成しています。
各ディレクトリにディレクトリ名と同じ名前でAssembly Definitionを作成して依存パッケージを制御しています。
- ExtrealCoreLearning
- アプリケーションのアセットを格納するディレクトリです。
- タイトル画面とバーチャル空間を作成済みです。
- バーチャル空間にはStarter Assets - Third Person Character Controllerが提供するアセットを使用しています。
- バーチャル空間でマルチプレイを実現します。
- このハンズオンでマルチプレイのクライアント実装を追加します。
- ExtrealCoreLearning.MultiplayServer
- マルチプレイサーバーのアセットを格納するディレクトリです。
- このハンズオンでマルチプレイのサーバー実装を追加します。
- ExtrealCoreLearning.MultiplayCommon
- アプリケーションとマルチプレイサーバーに共通するアセットを格納するディレクトリです。
- NGOに対応したプレイヤーのプレハブを作成済みです。
- プレイヤーのプレハブにはStarter Assets - Third Person Character Controllerが提供するアセットを使用しています。
- このハンズオンでNetworkManagerを追加します。
プロジェクトに問題がないことを確認するためアプリケーションを実行してみましょう。
ExtrealCoreLearning/AppディレクトリにあるApp
シーンを実行します。
タイトル画面のGoボタンを押してバーチャル空間に移動できれば成功です。
バーチャル空間でマルチプレイできるように実装を追加していきます。
Add multiplayer server
まずマルチプレイサーバーを追加します。
マルチプレイサーバーのロジックを提供するModelスクリプトを作成します。
ExtrealCoreLearning.MultiplayServerディレクトリに作成します。 NgoServerを使ってサーバーを開始します。 後ほどクライアントからのメッセージを受けてプレイヤーをスポーンする処理を追加します。
using Cysharp.Threading.Tasks;
using Extreal.Integration.Multiplay.NGO;
namespace ExtrealCoreLearning.MultiplayServer
{
public class MultiplayServer
{
private readonly NgoServer ngoServer;
public MultiplayServer(NgoServer ngoServer)
{
this.ngoServer = ngoServer;
}
public UniTask StartAsync()
{
return ngoServer.StartServerAsync();
}
}
}
マルチプレイサーバーのエントリーポイントとなるPresenterスクリプトを作成します。
ExtrealCoreLearning.MultiplayServerディレクトリに作成します。 MultiplayServerシーンが開始するとサーバーを開始します。
using Cysharp.Threading.Tasks;
using VContainer.Unity;
namespace ExtrealCoreLearning.MultiplayServer
{
public class MultiplayServerPresenter : IStartable
{
private readonly MultiplayServer multiplayServer;
public MultiplayServerPresenter(MultiplayServer multiplayServer)
{
this.multiplayServer = multiplayServer;
}
public void Start()
{
multiplayServer.StartAsync().Forget();
}
}
}
マルチプレイサーバーに必要なオブジェクトを組み立てるScopeスクリプトを作成します。
ExtrealCoreLearning.MultiplayServerディレクトリに作成します。 デバッグログを出力したいのでLoggingの初期化を入れています。 NetworkManagerはGameObjectとして配置するのでインスペクタから受け取ります。
using Extreal.Core.Logging;
using Extreal.Integration.Multiplay.NGO;
using Unity.Netcode;
using UnityEngine;
using VContainer;
using VContainer.Unity;
using LogLevel = Extreal.Core.Logging.LogLevel;
namespace ExtrealCoreLearning.MultiplayServer
{
public class MultiplayServerScope : LifetimeScope
{
[SerializeField] private NetworkManager networkManager;
private static void InitializeApp()
{
const LogLevel logLevel = LogLevel.Debug;
LoggingManager.Initialize(logLevel: logLevel);
}
protected override void Awake()
{
InitializeApp();
base.Awake();
}
protected override void Configure(IContainerBuilder builder)
{
builder.RegisterComponent(networkManager);
builder.Register<NgoServer>(Lifetime.Singleton);
builder.Register<MultiplayServer>(Lifetime.Singleton);
builder.RegisterEntryPoint<MultiplayServerPresenter>();
}
}
}
NetworkManagerをアタッチしたGameObjectをMultiplayServerシーンに作成します。
- MultiplayServerシーンに
NetworkManager
という名前でGameObjectを作成します。 - インスペクタのAdd Componentから
Network Manager
を追加します。 - インスペクタのSelect transport...から
UnityTransport
を選びます。 - StageNavigationでシーンを管理しているため、インスペクタの
Scene Management>Enable Scene Management
のチェックを外します。
ScopeスクリプトをMultiplayServerシーンに設定します。
- MultiplayServerシーンに
Scope
という名前でMultiplayServerScopeスクリプトをアタッチしたGameObjectを作成します。 - NetworkManagerオブジェクトをインスペクタで設定します。
マルチプレイサーバーを実行します。
MultiplayServerシーンを実行します。
Consoleに[Debug:NgoServer] The server has started
と出力されれば成功です。
Commonize NetworkManager
NetworkManagerはサーバーとクライアントで同じ設定になっている必要があるため、プレハブにしてサーバーとクライアントから同じオブジェクトを利用できるようにします。
NetworkManagerオブジェクトをプレハブに変更します。
MultiplayServerシーンのNetworkManagerオブジェクトをExtrealCoreLearning.MultiplayCommon
ディレクトリにドラッグ&ドロップします。
Add connection to multiplay room
マルチプレイルームへの参加とマルチプレイルームからの退室をアプリケーションに追加します。
まずアプリケーションで使うNgoClientの初期化を行います。
NgoClientの初期化にはNetworkManagerが必要です。 NgoClientとNetworkManagerはアプリケーションで1つ存在すればよいのでAppシーンに含めておき、空間が増えても再利用できるようにしておきます。
AppScope
スクリプトを変更してNgoClientを初期化します。
using Extreal.Core.Logging;
using Extreal.Core.StageNavigation;
using Extreal.Integration.Multiplay.NGO;
using Unity.Netcode;
using UnityEngine;
using VContainer;
using VContainer.Unity;
using LogLevel = Extreal.Core.Logging.LogLevel;
namespace ExtrealCoreLearning.App
{
public class AppScope : LifetimeScope
{
[SerializeField] private StageConfig stageConfig;
[SerializeField] private NetworkManager networkManager;
private static void InitializeApp()
{
// Omitted due to no changes
}
protected override void Awake()
{
// Omitted due to no changes
}
protected override void Configure(IContainerBuilder builder)
{
builder.RegisterComponent(stageConfig).AsImplementedInterfaces();
builder.Register<StageNavigator<StageName, SceneName>>(Lifetime.Singleton);
builder.RegisterComponent(networkManager);
builder.Register<NgoClient>(Lifetime.Singleton);
builder.RegisterEntryPoint<AppPresenter>();
}
}
}
ExtrealCoreLearning.MultiplayCommon
ディレクトリにあるNetworkManagerのプレハブをAppシーンにドラッグ&ドロップして、AppScope
オブジェクトのインスペクタで設定します。
プレイヤープレハブをNetworkManagerに設定します。
NGOで同期するプレハブはNetworkManagerに設定する必要があります。
ExtrealCoreLearning.MultiplayCommon
ディレクトリにあるプレイヤープレハブのNetworkPlayer
をNetworkManagerのNetworkPrefabs
に設定します。
NgoClientの初期化で問題が起きていないか確認します。
NgoClientを使った処理はまだ入れていないのでAppシーンを実行してこれまでと同じように起動されれば成功です。
起動時にAdd Scene to Scenes in Build
と表示された場合はNo - Continue
を選択して、NetworkManagerの作成手順が漏れているので次の手順を実施してください。
- StageNavigationでシーンを管理しているため、インスペクタの
Scene Management>Enable Scene Management
のチェックを外します。
MultiplayControlシーンを追加します。
ExtrealCoreLearning/MultiplayControl
ディレクトリを作成します。- 作成したディレクトリに
MultiplayControl
シーンを作成します。 - カメラなど初期設定されているGameObjectを削除しMultiplayControlシーンを一旦空にします。
マルチプレイのロジックを提供するModelスクリプトを作成します。
ExtrealCoreLearning/MultiplayControlディレクトリに作成します。 マルチプレイルームへの参加とマルチプレイルームからの退室を提供しています。
using Cysharp.Threading.Tasks;
using Extreal.Core.Logging;
using Extreal.Integration.Multiplay.NGO;
namespace ExtrealCoreLearning.MultiplayControl
{
public class MultiplayRoom
{
private static readonly ELogger Logger = LoggingManager.GetLogger(nameof(MultiplayRoom));
private readonly NgoClient ngoClient;
public MultiplayRoom(NgoClient ngoClient)
{
this.ngoClient = ngoClient;
}
public async UniTask JoinAsync()
{
await ngoClient.ConnectAsync(new NgoConfig());
if (Logger.IsDebug())
{
Logger.LogDebug("Joined");
}
}
public async UniTask LeaveAsync()
{
await ngoClient.DisconnectAsync();
if (Logger.IsDebug())
{
Logger.LogDebug("Left");
}
}
}
}
マルチプレイルームへの参加と退室を制御するPresenterスクリプトを作成します。
ExtrealCoreLearning/MultiplayControlディレクトリに作成します。 StageNavigatorのイベント通知を使ってマルチプレイルームへの参加とマルチプレイルームからの退室を制御しています。 OnStageTransitionedがステージに入った後、OnStageTransitioningがステージから出る前のタイミングになります。
using System;
using Cysharp.Threading.Tasks;
using Extreal.Core.StageNavigation;
using ExtrealCoreLearning.App;
using UniRx;
using VContainer.Unity;
namespace ExtrealCoreLearning.MultiplayControl
{
public class MultiplayControlPresenter : IInitializable, IDisposable
{
private readonly StageNavigator<StageName, SceneName> stageNavigator;
private readonly MultiplayRoom multiplayRoom;
private readonly CompositeDisposable disposables = new CompositeDisposable();
public MultiplayControlPresenter(StageNavigator<StageName, SceneName> stageNavigator,
MultiplayRoom multiplayRoom)
{
this.stageNavigator = stageNavigator;
this.multiplayRoom = multiplayRoom;
}
public void Initialize()
{
stageNavigator.OnStageTransitioned
.Subscribe(_ => multiplayRoom.JoinAsync().Forget())
.AddTo(disposables);
stageNavigator.OnStageTransitioning
.Subscribe(_ => multiplayRoom.LeaveAsync().Forget())
.AddTo(disposables);
}
public void Dispose()
{
disposables.Dispose();
}
}
}
ModelとPresenterのオブジェクトを組み立てるScopeスクリプトを作成します。
ExtrealCoreLearning/MultiplayControlディレクトリに作成します。
using VContainer;
using VContainer.Unity;
namespace ExtrealCoreLearning.MultiplayControl
{
public class MultiplayControlScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
builder.Register<MultiplayRoom>(Lifetime.Singleton);
builder.RegisterEntryPoint<MultiplayControlPresenter>();
}
}
}
ScopeスクリプトをMultiplayControlシーンに設定します。
- MultiplayControlシーンに
Scope
という名前でMultiplayControlScopeをアタッチしたGameObjectを作成します。 - インスペクタでParentに
AppScope
を指定します。
MultiplayControlシーンが完成したのでステージ設定とBuildSettingsに追加します。
- SceneNameに
MultiplayControl
を追加します。 - StageConfigのインスペクタで
VirtualStage
にMultiplayControl
を追加します。 - BuildSettingsにMultiplayControlシーンを追加します。
マルチプレイルームに接続できるか試してみましょう。
マルチプレイの動作確認にはParrelSyncを使います。 プロジェクトにParrelSyncをインストールしてあるので、ParrelSyncを使って複数のUnityエディタを開いてプレイしてみましょう。
実行するシーンは次の通りです。
- マルチプレイサーバー
/Assets/ExtrealCoreLearning.MultiplayServer/MultiplayServer
- アプリケーション
/Assets/ExtrealCoreLearning/App/App
バーチャル空間に移動してもこれまでと変わりありませんが、次のようなログが出ていれば成功です。
- マルチプレイサーバー
[Debug:NgoServer] The client with client id 1 has connected
- アプリケーション
[Debug:NgoClient] The client has connected to the server
Add player spawn
マルチプレイルームに接続できたのでプレイヤーをスポーンする処理を追加します。 この処理を追加するとマルチプレイできるようになります。
アプリケーションからプレイヤーのスポーンを依頼するメッセージをマルチプレイサーバーに送り、マルチプレイサーバーでプレイヤーをスポーンします。
Application
マルチプレイサーバーにプレイヤーのスポーンを依頼する処理を追加します。
プレイヤーのスポーン依頼にはNGOが提供するメッセージを使います。
メッセージ名はアプリケーションとマルチプレイサーバーで合わせる必要があるのでメッセージ名を表すEnumをExtrealCoreLearning.MultiplayCommon
ディレクトリに作成します。
namespace ExtrealCoreLearning.MultiplayCommon
{
public enum MessageName
{
PlayerSpawn
}
}
マルチプレイサーバーにプレイヤーのスポーンを依頼する処理をMultiplayRoomに追加します。 マルチプレイサーバーに接続されたタイミングでメッセージを送信します。
using System;
using Cysharp.Threading.Tasks;
using Extreal.Core.Logging;
using Extreal.Integration.Multiplay.NGO;
using ExtrealCoreLearning.MultiplayCommon;
using UniRx;
using Unity.Collections;
using Unity.Netcode;
namespace ExtrealCoreLearning.MultiplayControl
{
public class MultiplayRoom : IDisposable
{
private static readonly ELogger Logger = LoggingManager.GetLogger(nameof(MultiplayRoom));
private readonly NgoClient ngoClient;
private readonly CompositeDisposable disposables = new CompositeDisposable();
public MultiplayRoom(NgoClient ngoClient)
{
this.ngoClient = ngoClient;
ngoClient.OnConnected
.Subscribe(_ => SendPlayerSpawnMessage(ngoClient))
.AddTo(disposables);
}
private static void SendPlayerSpawnMessage(NgoClient ngoClient)
{
var messageStream
= new FastBufferWriter(FixedString64Bytes.UTF8MaxLengthInBytes, Allocator.Temp);
ngoClient.SendMessage(MessageName.PlayerSpawn.ToString(), messageStream);
}
public async UniTask JoinAsync()
{
// Omitted due to no changes
}
public async UniTask LeaveAsync()
{
// Omitted due to no changes
}
public void Dispose() => disposables.Dispose();
}
}
Multiplay Server
アプリケーションから送信されるプレイヤーをスポーンするメッセージに対応する処理をMultiplayServerに追加します。
MessageHandlerを登録してアプリケーションからのメッセージに対応します。
プレイヤーのプレハブはAddressablesを使ってロードしています。
ExtrealCoreLearning/MultiplayControlディレクトリのNetworkPlayer
を選択してインスペクタを開くと、PlayerPrefab
という名前でAddressablesに登録されていることが確認できます。
今回はアプリケーションからメッセージの内容を何も送っていませんが、メッセージの内容でアバター名を送りユーザーごとに選択したアバターをプレイヤーとしてスポーンするといったこともできます。 サンプルアプリケーションでユーザーが選択したアバターのスポーンを実現しているので興味がある方はSample Applicationをご覧ください。
using System;
using Cysharp.Threading.Tasks;
using Extreal.Core.Logging;
using Extreal.Integration.Multiplay.NGO;
using ExtrealCoreLearning.MultiplayCommon;
using UniRx;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace ExtrealCoreLearning.MultiplayServer
{
public class MultiplayServer : IDisposable
{
private static readonly ELogger Logger = LoggingManager.GetLogger(nameof(MultiplayServer));
private readonly CompositeDisposable disposables = new CompositeDisposable();
private readonly NgoServer ngoServer;
public MultiplayServer(NgoServer ngoServer)
{
this.ngoServer = ngoServer;
this.ngoServer.OnServerStarted.Subscribe(_ =>
{
ngoServer.RegisterMessageHandler(
MessageName.PlayerSpawn.ToString(), PlayerSpawnMessageHandler);
}).AddTo(disposables);
this.ngoServer.OnServerStopping.Subscribe(_ =>
{
ngoServer.UnregisterMessageHandler(MessageName.PlayerSpawn.ToString());
}).AddTo(disposables);
}
private async void PlayerSpawnMessageHandler(ulong clientId, FastBufferReader messageStream)
{
if (Logger.IsDebug())
{
Logger.LogDebug($"{MessageName.PlayerSpawn}: {clientId}");
}
ngoServer.SpawnAsPlayerObject(clientId, await LoadPlayerPrefab());
}
private static async UniTask<GameObject> LoadPlayerPrefab()
{
var result = Addressables.LoadAssetAsync<GameObject>("PlayerPrefab");
return await result.Task;
}
public UniTask StartAsync()
{
return ngoServer.StartServerAsync();
}
public void Dispose()
{
disposables.Dispose();
}
}
}
Play
全ての実装が完了したのでプレイしてみましょう。
実行するシーンは次の通りです。
- マルチプレイサーバー
/Assets/ExtrealCoreLearning.MultiplayServer/MultiplayServer
- アプリケーション
/Assets/ExtrealCoreLearning/App/App
操作方法は次の通りです。
- 移動
- W:前、A:左、S:後、D:右
- アクション
- スペース:ジャンプ
- 左Shift:走る
ParrelSyncでUnityエディタを開いてプレイしている様子です。 マルチプレイサーバーを実行しているUnityエディタは後ろに隠れています。
Next Step
これでNGOラッパーのハンズオンは終了です。 お疲れさまでした。
このハンズオンを通じてNGOラッパーを使ったマルチプレイサーバーとアプリケーションの作り方を体験しました。 次のステップとしてNGOラッパーがより本格的なアプリケーションでどのように使われるのか関心があると思います。 その期待に応えるため、より本格的な実装例としてSample Applicationを提供しています。 ぜひSample Applicationをご覧ください。