見出し画像

Unity Netcode for GameObjectsを使ったオンラインマルチプレイゲーム開発Tips【Advent Calendar 12/23】

はじめに

この記事はColorful Palette アドベントカレンダー 12/23の記事になります。

株式会社Colorful Paletteでサーバサイドエンジニアをしている村田です。
今回はUnityのNetcode for GameObjectsの正式版を使い、オンラインマルチプレイのゲームを作ってみました。どのようにして作ることができるか、簡単ですが紹介したいと思います。

作ったゲーム

障害物を避けながら、プレイヤー同士で邪魔し合いつつ、ゴールを目指してスコアを稼ぐゲームです。

弾を撃ち合って落下させたり、

待ち伏せして落としたりできます。

Netcode for GameObjectsとは

「Netcode for GameObjects (以降、Netcode)」はマルチプレイの開発を行いやすくするためのパッケージです。以前は「MLAPI」という名称でしたが、2021年10月21日に「Netcode」としてプレリリースされ、今年2022年6月に晴れて正式版1.0.0が公開されました。
Package Managerで簡単にインストールすることができます。またGitHubにソースコードも公開されており、新しいバージョンを早く試したい場合はGitHubから取得しても良いです。
https://github.com/Unity-Technologies/com.unity.netcode.gameobjects
Netcodeを使うことで、ネットワークを返してGameObjectの同期を簡単に行うことができます。ホスト、クライアント、サーバーのソースコードを1つのUnityプロジェクトで管理できるのも特徴の1つです。

Netcodeでどのようなゲームを作れるか

多少遅延があってもゲーム体験が成立するようなアクションゲームやターン制のようなゲームであれば、Netcodeを使うだけで簡単に作れると思います。
格闘ゲームのようにネットワークの低遅延が求められ同期がシビアなゲームだと単純にNetcodeを使うだけでは良いユーザー体験の実現が難しいかもしれません。また、MMOのように大多数のユーザーが集うゲームだと、同期するオブジェクトが多くなり、オブジェクトを間引くような独自な対応を求められそうです。

プレイヤー間のネットワーク構成

Netcodeはホスト・クライアント型やサーバー・クライアント型のネットワーク構成のゲームを構築できます。

ホスト <> クライアント

プレイヤー1人が通信のホストになり、他プレイヤーがクライアントとしてホストに接続します。

Unity Relayを使うと同一ネットワークだけでなく、インターネットを返して遊ぶことも可能です。
https://unity.com/ja/products/relay

これらの構成だと、ホストにデータが集約してクライアントに同期されるので、ゲームによってはホストの画面更新がクライアントより早まるため、ホストが有利になる場合もあります。また、ホストのネットワークが切断されてしまうとゲームが中断してしまいます。

サーバー <> クライアント

サーバーを用意する構成も可能です。全プレイヤーがクライアントという扱いになりホスト有利という状況が生じません。自身で用意したサーバーにUnityのビルドをデプロイすることになるため手間はかかりますが、サーバー側にあるデータベースを参照するなど自由度の高いゲームが作れます。

Netcodeの主な使い方、知っておくと良いこと

Netcodeを使う上でのtipsを書いておきます。

PrefabにNetworkObject等を追加

PrefabにNetcodeのスクリプトを追加します。
例えばAnimatorを使っていて、他プレイヤーにもアニメーションを同期させて表示させたい場合はNetwork Animatorを追加します。Network Objectは必須のスクリプトです。

空のGameObjectにNetworkManagerスクリプトを追加

Sceneに空のGameObjectを追加して、それにNetcodeのNetworkManagerスクリプトを追加します。また、NetworkManagerという名前にします。

NetworkManagerにPrefabを登録

登録先は「Player Prefab」と「Network Prefabs」の2種類があり、先述のPrefabを登録します。
「Player Prefab」はプレイヤーが操作するオブジェクトを登録します。クライアントがホスト(サーバー)に接続した際、「Player Prefab」のオブジェクトが生成されます。
「Network Prefabs」は動的に生成することがある全てのPrefabを登録します。

NetworkManagerにUnity Transportの追加と選択

「Transport」とは、Netcodeで使われる通信手段を指します。contributionsに様々なTransportが用意されていますが、今回は標準の「Unity Transport」を使います。「Connection Data」のAddressとPortは、ホストおよびサーバーの接続先になりますが、ゲーム実行中に変更できるUIを用意するといいです。

上記のUnity Transportを追加後、Network Managerの「Network Transport」で「Unity Transport」を選択します。

ホスト・サーバー・クライアントの実行方法

NetworkManager.Singletonには機能別にメソッドが用意されています。

// ホスト(兼サーバーとしても振る舞う)
NetworkManager.Singleton.StartHost();

// サーバー
NetworkManager.Singleton.StartServer();

// クライアント
var transport = NetworkManager.Singleton.NetworkConfig.NetworkTransport;
if (transport is Unity.Netcode.Transports.UTP.UnityTransport unityTransport)
{
    // 接続先のIPアドレスとポートを指定
    unityTransport.SetConnectionData(ipAddress, port);
}
NetworkManager.Singleton.StartClient();

Network PrefabはMonoBehaviourの代わりにNetworkBehaviourを使う

NetworkBehaviourを継承することで、Netcodeの各種機能が使えるようになります。

public class PlayerController : NetworkBehaviour
{

クライアントが何か変更を加えたい場合は、ホスト(サーバー)に指示を送る

クライアント側だけでtransformを変更しても、プレイヤー間で同期されません。クライアントからホスト(サーバー)に対して、変更を依頼します。

クライアントがホスト(サーバー)に接続するとPlayer Prefabが生成されOnNetworkSpawnが実行される

// 「Player Prefab」のPrefabにPlayerControllerを追加している
public class PlayerController : NetworkBehaviour
{
    // ホスト(サーバー)に接続するとPrefabのオブジェクトが生成され、
    // ホスト(サーバー)側とクライアント側の両方でOnNetworkSpawnが実行される
    public override void OnNetworkSpawn()
    {
        // ホスト(サーバー)判定
        if (IsServer)
        {
	    // ホスト(サーバー)側で、初期位置にtransformを書きかえる
            MoveToStartPosition();
        }

        // Prefabのオーナー判定
        if (IsOwner)
        {
            // クライアントがホスト(サーバー)に接続する度に、
            // Prefabが生成されOnNetworkSpawnが実行されるので、
                        // オーナーのPrefabオブジェクトのみカメラを移動させるようにしている
            var camera = Camera.main.GetComponent<PlayerFollowCamera>();
            camera.Player = transform;
        }
    }

プレイヤーのキー入力でプレイヤーオブジェクト(Player Prefabオブジェクト)を移動させる

public class PlayerController : NetworkBehaviour
{
    private Vector2 _moveInput;
    private bool _isKeySpace;

    // クライアント側でこのメソッドを実行すると、ホスト(サーバー)側で_moveInputと_isKeySpaceがセットされます
    [Unity.Netcode.ServerRpc]
    private void SetInputServerRpc(float x, float y, bool space)
    {
        _moveInput = new Vector2(x, y);
        _isKeySpace = space;
    }

    private void Update()
    {
        // オーナー側
        if (IsOwner)
        {
            // キー入力をホスト(サーバー)に送る
            SetInputServerRpc(
                Input.GetAxisRaw("Horizontal"),
                Input.GetAxisRaw("Vertical"),
                Input.GetKey(KeyCode.Space)
            );
        }
    }

    private void FixedUpdate()
    {
        // サーバー側
        if (IsServer)
        {
            // _moveInputを使って、プレイヤーオブジェクトの移動処理を実行する
            // 各プレイヤーの画面で反映されます

オブジェクトの生成

サーバー側でInstantiateで生成した後、NetworkObjectのSpawnを実行することで各プレイヤーの画面に反映されます。

var gmo = GameObject.Instantiate(bulletPrefab, transform.position + new Vector3(0, 1.0f, 0), Quaternion.identity);
gmo.transform.localScale = new Vector3(1.0f, 0.5f, 1.0f);

var netObject = gmo.GetComponent<NetworkObject>();
netObject.Spawn(true);

NetworkVariableを使ってプレイヤーのログを表示する

NetworkVariableを使うと、プレイヤー間で値が同期されるオブジェクトを作成できます。

private NetworkVariable<Unity.Collections.FixedString4096Bytes> _log = new();
private List<string> _clientLog = new();

// ホスト(サーバー)でメソッドを実行してNetworkVariableを書きかえる
public void addClientLog(string log)
{
    _clientLog.Insert(0, log);
    while (_clientLog.Count > 20)
    {
        _clientLog.RemoveAt(0);
    }

    string dispLog = "";
    foreach(string l in _clientLog)
    {
        dispLog += l + "\\n";
    }

    _log.Value = dispLog;
}

void OnGUI()
{
    // プレイヤーログの描画
    GUILayout.BeginArea(new Rect(10, 60, 300, 500));
    GUILayout.Label("Log\\n" + _log.Value.Value);
    GUILayout.EndArea();
}

Dedicated Serverビルドについて

Build SettingsのPlatformで「Dedicated Server」を選択することで、サーバー専用のビルドができます。画面描画を持たないCLIのビルドです。

Dedicated Serverビルドのときは、必ずStartServerを実行するようにします。

#if UNITY_SERVER
    if (!NetworkManager.Singleton.IsServer)
    {
        NetworkManager.Singleton.StartServer();
    }
#endif

Unity Transportの「Server Listen Address」は、少なくとも開発中は「0.0.0.0」にしておくと良いです。「0.0.0.0」にすると、全てのIPアドレスからの接続を許可します。Server Listen Addressが未設定だとAddressを利用するため、Addressからの接続のみ許可になってしまうので、他PCからの接続できないといった事態を防げます。

LinuxのDedicated ServerビルドをDockerで実行する例です。udpを指定します。

docker run -it -v $(pwd):/temp -p 7777:7777/udp debian:buster-20211201 /temp/LinuxServer.x86_64

最後に

Netcodeを使えば簡単にマルチプレイのゲームを作ることができます。クライアントとサーバーも1つのUnityプロジェクトとしてソースコードを管理できるメリットもあります。サーバー側も作ることができるので、サーバー側でチート判定をするなど、自由度や拡張性の高い開発ができそうですね。
作ったゲームのコードはGithubに公開しています。
https://github.com/muratahiroshi/ColorfulePaletteAdventCalendar2022Netcode

明日のColorful Palette アドベントカレンダー は、まるじゅんさんの「初めてのアドカレ運営での工夫としくじり」という、アドベントカレンダーを運営してみての振り返り記事となっております!