LobbyとRelayを連携してマルチプレイゲームを完成させる[Unity]

これまでNetcode for GameObjectsの機能を利用して、マルチプレイゲームを作る準備を進めて来ました。今回はその締め、ロビーで合流してRelay機能でゲーム部分を接続する処理を作りたいと思います。

締めにしてはソースコードが多い。

目次

事前準備

今回進める上で、あらかじめ準備して頂きたいものがあります。また、どのような進め方で実装していくかをお伝えしておこうと思います。

先に用意しておいて欲しいもの

今回の内容は、以下の内容を作成した上での解説になります。

リレー機能に関してはNetcode for GameObjectsの基本部分も必要になりますので、実質もう一つ追加になります。

ソースコードの編集はLobbyをメインに修正していきます。学習順序としてはリレーを先にやったけど、実際の制作はロービから行う。こういうのは積み上げてきた経験が無いとちんぷんかんぷんになりますので過去の内容しっかり振り返りましょう。

どんな感じに作るか

Relayでゲームをつなぐ時、Relayコードという文字列を発行しました。このパラメータ、リレーのテスト時は文字を打ち込んで行いました。これをロビー内で共有します。リレーコードが共有されたかどうかはPollingメソッドを用意して、値が共有されたかを定期的に確認します。

ホスト側

  • ロビーにメンバーが集合したらGameStartの音頭を取る
  • Relayを作成、リレーコードを取得
  • ロビー内でリレーコード共有

クライアント側

  • ロビーに合流
  • 一定時間ごとに更新情報を確認
  • リレーコードが共有されたらリレーに参加

上記のプログラムはほとんどがLobbyManagerの中に記述されます。そのためホスト・クライアントどちらなのかを意識出来るように心がける必要があります。

LobbyManagerの変更点

一番あぶらっこい部分になるので、各機能ごとに小分けしつつ、最後に完成品載せます。

変数の追加

先にこの後利用する変数を追加しておきたいと思います。hostLobbyはエディターのりネーム機能などを使ってjoinedLobbyに変更してください。

public class LobbyManager : MonoBehaviour
{
    private bool isHost = false;
    //private Lobby hostLobby;    // リネーム -> joinedLobby
    private Lobby joinedLobby;  // hostLobbyから変更

    // Relay機能のスクリプトをプロジェクトに追加する必要があります。
    [SerializeField] private RelayTest relayTest;

    // ロビー内で情報を共有する
    public const string KEY_RELAY_CODE = "RelayCode";

    // pollingを一定時間ごとに行う
    private float pollingTimer = 0f;
}

ロビー作成メソッド修正

主は変更点としては以下

  • 作る人はホスト:isHost = true;
  • オプションにDataを追加
    • リレーコードをロビー内のメンバーに公開
    • 初期値は0の文字列(未設定状態)
    public async void CreateLobby()
    {
        try
        {
            string lobbyName = "TestLobby";
            int maxPlayers = 4;
            CreateLobbyOptions createLobbyOptions = new CreateLobbyOptions()
            {
                IsPrivate = false,
                Player = new Player(AuthenticationService.Instance.PlayerId)
                {
                    Data = new Dictionary<string, PlayerDataObject>(){
                        {"PlayerName", new PlayerDataObject(PlayerDataObject.VisibilityOptions.Public, playerName)}
                    }
                },
                Data = new Dictionary<string, DataObject>(){
                    {KEY_START_GAME, new DataObject(DataObject.VisibilityOptions.Member, "0")}
                }
            };
            isHost = true;
            joinedLobby = await LobbyService.Instance.CreateLobbyAsync(lobbyName, maxPlayers, createLobbyOptions);
            Debug.Log("Lobby created: " + joinedLobby.Id);
        }
        catch (LobbyServiceException e)
        {
            Debug.Log("Error creating lobby: " + e.Message);
        }
    }

ゲーム開始処理

後から作るゲーム開始を促され、ゲームを開始する処理。こちらはホスト側のみが実行するものになります。

  • インスペクターでセットしたRelayTestを利用してリレー作成
  • 所属しているロビーの情報を更新
    • KEY_RELAY_CODEのパラメータ>RelayTestから生成されたrelayCode
    public async void StartGame()
    {
        if (isHost)
        {
            try
            {
                Debug.Log("StartGame");
                // ここ、エラーが出ます。RelayTest更新までは右辺を"test";とかにしておいても良し
                string relayCode = await relayTest.CreateRelay();
                Lobby lobby = await Lobbies.Instance.UpdateLobbyAsync(joinedLobby.Id, new UpdateLobbyOptions
                {
                    Data = new Dictionary<string, DataObject>{
                        {KEY_RELAY_CODE, new DataObject(DataObject.VisibilityOptions.Member, relayCode)}
                    }
                });
                Debug.Log("Lobby started: " + joinedLobby.Id);
            }
            catch (LobbyServiceException e)
            {
                Debug.Log("Error starting lobby: " + e.Message);
            }
        }
    }

ロビーの状態を更新する処理

ロビーに入ったら、KEY_RELAY_CODEの内容が更新されていないかが気になります。その確認するための処理が必要です。

  • ロビーに参加していない場合は動かない
  • pollingTimerを使って1秒毎に確認を行う
  • 参加中のロビーIDを使って更新
  • ホスト以外の場合はKEY_RELAY_CODEを確認
    • relayCodeを利用してrelayTestに参加(JoinRelay)
    • 参加後はjoindLobbyに別れを告げる
    private async void HandleLobbyPolling()
    {
        if (joinedLobby == null)
        {
            return;
        }

        pollingTimer += Time.deltaTime;
        if (pollingTimer < 1f)
        {
            return;
        }
        pollingTimer = 0f;

        joinedLobby = await LobbyService.Instance.GetLobbyAsync(joinedLobby.Id);

        if (!isHost)
        {
            if (joinedLobby.Data[KEY_RELAY_CODE].Value != "0")
            {
                Debug.Log("StartGame");
                string relayCode = joinedLobby.Data[KEY_RELAY_CODE].Value;
                relayTest.JoinRelay(relayCode);
                joinedLobby = null;
            };
        }
    }

その他呼び出す場所の追加など

あとは準備した処理を呼び出しましょう。

  • StartGameの呼び出し
    • StartメソッドでLobbyListViewのイベント経由で発火
    • ただしLobbyListViewの更新はまだなのでここはコメントアウト
    • 実装後に解除してください(コメントアウトを外す)
  • HandleLobbyPollingの呼び出し
    • メソッド内で一定時間のインターバールを計測しているのでUpdateメソッドに呼び出すだけでOK

    private async void Start()
    {
        /*
        LobbyListView.OnGameStartRequest += (sender, e) =>
        {
            StartGame();
        };
        */
    }


    private void Update()
    {
        // hostLobbyがnullの時、Cキーを押すとロビーを作成する
        if (joinedLobby == null && Input.GetKeyDown(KeyCode.C))
        {
            CreateLobby();
        }
        RefreshLobbyHeatbeat();
        HandleLobbyPolling();
    }

その他の更新場所

あとはいくつかのスクリプトを合わせて更新しましょう。

LobbyListView

主な変更点はゲーム開始のきっかけを作る処理です。

  • メンバー変数追加
    • [SerializeField] private Button startGameButton
    •  public static EventHandler OnGameStartRequest;
  • Startメソッドでイベント呼び出し
    • スタートボタンが押されたらOnGameStartRequestを呼ぶ
    • リストを更新する処理と同じ感じ
  • UIの作成
    • ボタンを用意して、インスペクターにセットしてください。
public class LobbyListView : MonoBehaviour
{
    [SerializeField] private GameObject lobbyBannerPrefab;
    [SerializeField] private Transform lobbyListContent;
    [SerializeField] private Button refreshButton;
    [SerializeField] private Button startGameButton;

    public static EventHandler OnRefreshLobbyListRequest;
    public static EventHandler OnGameStartRequest;

    private void Start()
    {
        refreshButton.onClick.AddListener(() =>
        {
            OnRefreshLobbyListRequest?.Invoke(this, EventArgs.Empty);
        });
        startGameButton.onClick.AddListener(() =>
        {
            Debug.Log("StartGameButton");
            OnGameStartRequest?.Invoke(this, EventArgs.Empty);
        });
    }
    public void Refresh(List<Lobby> lobbyList)
    {
        foreach (Transform child in lobbyListContent)
        {
            Destroy(child.gameObject);
        }

        foreach (var lobby in lobbyList)
        {
            GameObject lobbyBanner = Instantiate(lobbyBannerPrefab, lobbyListContent);
            lobbyBanner.GetComponent<LobbyBanner>().Init(lobby);
        }
    }
}

RelayTest

大きく変わる部分は2点。その他はほぼ同じになります。

  • Startメソッドを削除
    • UnityService系の初期化処理などがありますが、Lobbyの方で行うのでやらなくてもOK。
    • サインインも同様
  • CreateRelayの変更
    • リレーを作った時にRelayCodeを返すようにします。
    • これでLobbyManagerの方も合わせて更新。
// using追加
using System.Threading.Tasks;

public class RelayTest : MonoBehaviour
{
    /*
    private async void Start()
    {
        // 省略
    }
    */

    public async Task<string> CreateRelay()
    {
        try
        {
            Allocation allocation = await RelayService.Instance.CreateAllocationAsync(3);
            string joinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId);
            Debug.Log(joinCode);

            NetworkManager.Singleton.GetComponent<UnityTransport>().SetHostRelayData(
                allocation.RelayServer.IpV4,
                (ushort)allocation.RelayServer.Port,
                allocation.AllocationIdBytes,
                allocation.Key,
                allocation.ConnectionData
                );

            NetworkManager.Singleton.StartHost();
            return joinCode;
        }
        catch (RelayServiceException e)
        {
            Debug.Log(e);
            return null;
        }
    }
}

動かして確認

あとはプログラムを動かして確認します。例のごとくアプリとエディター両方を起動して確認しましょう。

操作手順

エディター側をHostで行う場合

  • アプリエディターでゲームを起動
  • エディター側でCキーを押す
    • アプリ・エディターでRefreshボタンを押してロビーが作られたことを確認
  • アプリ側でJoinボタンを押す
    • 再びRefreshボタンでロビーの人数が増えたことを確認
  • エディター側でGameボタンを押す
    • キャラクターが表示される(UIが邪魔)
    • Pollingのタイミングでクライアント側も合流する

接続はされるけど、Lobbyの表示が残ったままなので、ゲーム開始のイベントでロビー非表示処理とかを追加出来るといいですね。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

目次