メインコンテンツまでスキップ
バージョン: 1.3.0

Multiplay using Netcode for GameObjects

ここではNGOラッパーについて学習します。

  • 学習時間の目安
    • 60分
  • Unityバージョン
    • 2022.3.17f1

NGOラッパーの学習では学習用に用意したプロジェクトを使います。 この学習用のプロジェクトはCoreの学習で構築したアプリケーションアーキテクチャをベースに作成しています。 Coreの学習を実施していない方はこの学習より先にCoreの学習を実施することを推奨します。

NGOラッパーがセットアップされた学習用のプロジェクトを使って、バーチャル空間でマルチプレイできるようにサーバーとアプリケーションの実装を追加していきます。

Prepare project

step

まずはプロジェクトを準備しましょう。

学習用のプロジェクトをクローンします。

https://github.com/extreal-dev/Extreal.Learning.Multiplay.NGO.git

Unityエディタでクローンしたプロジェクトを開きます。

step

プロジェクトの内容を確認しましょう。

Assetsディレクトリに次の3つのディレクトリを作成しています。 各ディレクトリにディレクトリ名と同じ名前でAssembly Definitionを作成して依存パッケージを制御しています。

  • ExtrealCoreLearning
    • アプリケーションのアセットを格納するディレクトリです。
    • タイトル画面とバーチャル空間を作成済みです。
    • このハンズオンでマルチプレイのクライアント実装を追加します。
  • ExtrealCoreLearning.MultiplayServer
    • マルチプレイサーバーのアセットを格納するディレクトリです。
    • このハンズオンでマルチプレイのサーバー実装を追加します。
  • ExtrealCoreLearning.MultiplayCommon
    • アプリケーションとマルチプレイサーバーに共通するアセットを格納するディレクトリです。
    • NGOに対応したプレイヤーのプレハブを作成済みです。
    • このハンズオンでNetworkManagerを追加します。
step

プロジェクトに問題がないことを確認するためアプリケーションを実行してみましょう。

ExtrealCoreLearning/AppディレクトリにあるAppシーンを実行します。 タイトル画面のGoボタンを押してバーチャル空間に移動できれば成功です。

Project success

バーチャル空間でマルチプレイできるように実装を追加していきます。

Add multiplayer server

まずマルチプレイサーバーを追加します。

step

マルチプレイサーバーのロジックを提供する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();
}
}
}
step

マルチプレイサーバーのエントリーポイントとなる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();
}
}
}
step

マルチプレイサーバーに必要なオブジェクトを組み立てる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>();
}
}
}
step

NetworkManagerをアタッチしたGameObjectをMultiplayServerシーンに作成します。

Add NetworkManager

  • MultiplayServerシーンにNetworkManagerという名前でGameObjectを作成します。
  • インスペクタのAdd ComponentからNetwork Managerを追加します。
  • インスペクタのSelect transport...からUnityTransportを選びます。
  • StageNavigationでシーンを管理しているため、インスペクタのScene Management>Enable Scene Managementのチェックを外します。
step

ScopeスクリプトをMultiplayServerシーンに設定します。

Add scope

  • MultiplayServerシーンにScopeという名前でMultiplayServerScopeスクリプトをアタッチしたGameObjectを作成します。
  • NetworkManagerオブジェクトをインスペクタで設定します。
step

マルチプレイサーバーを実行します。

MultiplayServerシーンを実行します。 Consoleに[Debug:NgoServer] The server has startedと出力されれば成功です。

Server success

Commonize NetworkManager

NetworkManagerはサーバーとクライアントで同じ設定になっている必要があるため、プレハブにしてサーバーとクライアントから同じオブジェクトを利用できるようにします。

step

NetworkManagerオブジェクトをプレハブに変更します。

MultiplayServerシーンのNetworkManagerオブジェクトをExtrealCoreLearning.MultiplayCommonディレクトリにドラッグ&ドロップします。

NetworkManager prefab

Add connection to multiplay room

マルチプレイルームへの参加とマルチプレイルームからの退室をアプリケーションに追加します。

step

まずアプリケーションで使うNgoClientの初期化を行います。

NgoClientの初期化にはNetworkManagerが必要です。 NgoClientとNetworkManagerはアプリケーションで1つ存在すればよいのでAppシーンに含めておき、空間が増えても再利用できるようにしておきます。

AppScopeスクリプトを変更してNgoClientを初期化します。

using Extreal.Core.Common.Retry;
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)
.WithParameter(typeof(IRetryStrategy), NoRetryStrategy.Instance);

builder.RegisterEntryPoint<AppPresenter>();
}
}
}

ExtrealCoreLearning.MultiplayCommonディレクトリにあるNetworkManagerのプレハブをAppシーンにドラッグ&ドロップして、AppScopeオブジェクトのインスペクタで設定します。

NgoClient init

step

プレイヤープレハブをNetworkManagerに設定します。

NGOで同期するプレハブはNetworkManagerに設定する必要があります。 ExtrealCoreLearning.MultiplayCommonディレクトリにあるプレイヤープレハブが入ったNetworkPrefabsListをNetworkManagerのNetworkPrefabsListsに設定します。

Network prefab

step

NgoClientの初期化で問題が起きていないか確認します。

NgoClientを使った処理はまだ入れていないのでAppシーンを実行してこれまでと同じように起動されれば成功です。

起動時にAdd Scene to Scenes in Buildと表示された場合はNo - Continueを選択して、NetworkManagerの作成手順が漏れているので次の手順を実施してください。

  • StageNavigationでシーンを管理しているため、インスペクタのScene Management>Enable Scene Managementのチェックを外します。
step

MultiplayControlシーンを追加します。

  • ExtrealCoreLearning/MultiplayControlディレクトリを作成します。
  • 作成したディレクトリにMultiplayControlシーンを作成します。
  • カメラなど初期設定されているGameObjectを削除しMultiplayControlシーンを一旦空にします。
step

マルチプレイのロジックを提供する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");
}
}
}
}
step

マルチプレイルームへの参加と退室を制御するPresenterスクリプトを作成します。

ExtrealCoreLearning/MultiplayControlディレクトリに作成します。 StageNavigatorのイベント通知を使ってマルチプレイルームへの参加とマルチプレイルームからの退室を制御しています。 OnStageTransitionedがステージに入った後、OnStageTransitioningがステージから出る前のタイミングになります。

using Cysharp.Threading.Tasks;
using Extreal.Core.Common.System;
using Extreal.Core.StageNavigation;
using ExtrealCoreLearning.App;
using UniRx;
using VContainer.Unity;

namespace ExtrealCoreLearning.MultiplayControl
{
public class MultiplayControlPresenter : DisposableBase, IInitializable
{
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);
}

protected override void ReleaseManagedResources()
{
disposables.Dispose();
}
}
}
step

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>();
}
}
}
step

ScopeスクリプトをMultiplayControlシーンに設定します。

MultiplayControl scope

  • MultiplayControlシーンにScopeという名前でMultiplayControlScopeをアタッチしたGameObjectを作成します。
  • インスペクタでParentにAppScopeを指定します。
step

MultiplayControlシーンが完成したのでステージ設定とBuildSettingsに追加します。

  • SceneNameにMultiplayControlを追加します。
  • StageConfigのインスペクタでVirtualStageMultiplayControlを追加します。
  • BuildSettingsにMultiplayControlシーンを追加します。
step

マルチプレイルームに接続できるか試してみましょう。

マルチプレイの動作確認にはParrelSyncを使います。 プロジェクトにParrelSyncをインストールしてあるので、ParrelSyncを使って複数のUnityエディタを開いてプレイしてみましょう。

ParrelSync

実行するシーンは次の通りです。

  • マルチプレイサーバー
    /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

step

マルチプレイサーバーにプレイヤーのスポーンを依頼する処理を追加します。

プレイヤーのスポーン依頼にはNGOが提供するメッセージを使います。 メッセージ名はアプリケーションとマルチプレイサーバーで合わせる必要があるのでメッセージ名を表すEnumをExtrealCoreLearning.MultiplayCommonディレクトリに作成します。

namespace ExtrealCoreLearning.MultiplayCommon
{
public enum MessageName
{
PlayerSpawn
}
}

マルチプレイサーバーにプレイヤーのスポーンを依頼する処理をMultiplayRoomに追加します。 マルチプレイサーバーに接続されたタイミングでメッセージを送信します。

using Cysharp.Threading.Tasks;
using Extreal.Core.Common.System;
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 : DisposableBase
{
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
}

protected override void ReleaseManagedResources() => disposables.Dispose();
}
}

Multiplay Server

step

アプリケーションから送信されるプレイヤーをスポーンするメッセージに対応する処理をMultiplayServerに追加します。

MessageHandlerを登録してアプリケーションからのメッセージに対応します。

プレイヤーのプレハブはAddressablesを使ってロードしています。 ExtrealCoreLearning/MultiplayControlディレクトリのNetworkPlayerを選択してインスペクタを開くと、PlayerPrefabという名前でAddressablesに登録されていることが確認できます。

今回はアプリケーションからメッセージの内容を何も送っていませんが、メッセージの内容でアバター名を送りユーザーごとに選択したアバターをプレイヤーとしてスポーンするといったこともできます。 サンプルアプリケーションでユーザーが選択したアバターのスポーンを実現しているので興味がある方はSample Applicationをご覧ください。

using Cysharp.Threading.Tasks;
using Extreal.Core.Common.System;
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 : DisposableBase
{
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();
}

protected override void ReleaseManagedResources()
{
disposables.Dispose();
}
}
}

Play

step

全ての実装が完了したのでプレイしてみましょう。

実行するシーンは次の通りです。

  • マルチプレイサーバー
    /Assets/ExtrealCoreLearning.MultiplayServer/MultiplayServer
  • アプリケーション
    /Assets/ExtrealCoreLearning/App/App

操作方法は次の通りです。

  • 移動
    • W:前、A:左、S:後、D:右
  • アクション
    • スペース:ジャンプ
    • 左Shift:走る

ParrelSyncでUnityエディタを開いてプレイしている様子です。 マルチプレイサーバーを実行しているUnityエディタは後ろに隠れています。

Play

Next Step

これでNGOラッパーのハンズオンは終了です。 お疲れさまでした。

このハンズオンを通じてNGOラッパーを使ったマルチプレイサーバーとアプリケーションの作り方を体験しました。 次のステップとしてNGOラッパーがより本格的なアプリケーションでどのように使われるのか関心があると思います。 その期待に応えるため、より本格的な実装例としてSample Applicationを提供しています。 ぜひSample Applicationをご覧ください。