3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

KLab EngineerAdvent Calendar 2024

Day 19

Unity + PlayFab(Party/Lobby)でマルチプレイヤー同期処理を作る

Last updated at Posted at 2024-12-18

この記事は、KLab Engineer Advent Calendar 2024 の19日目の記事です。
こんにちは。KLabでエンジニアをしている @tsune2ne です。

マルチプレイヤーゲームの検証でPlayFabを触ることがあったので備忘録替わりの記事です。
PlayFabのSDKや名前がややこしかったり、Unityで使うノウハウがうまく見つけれなかったのでここに残しておきます。

PlayFab概要

icon_playfab.png

PlayFab とは?

  • ゲーム特化のBaas。バックエンド プラットフォーム。
    • CSとモバイルのクロスプラットフォーム対応
  • PlayFabは2014年創業のアメリカ企業、2018年にMicrosftが買収。
  • 日本を含む様々なサービスで利用されている。

提供サービス

CSおよびスマホのプレイヤー認証・プレイヤーデータ管理・ストア提供と販売処理
専用サーバ・テキストチャット・ボイスチャット・マッチメイキング
ランキング・トーナメント・チート防止
エンゲージメント・リテンション・データ分析

提供SDK

PlayFab Services SDK

LiveOps、エコノミー、データ分析をなどの機能の大半を使うことができます。

  • プレイヤー認証、プレイヤーデータ管理
  • ストア提供と販売処理
  • ランキング、トーナメント
  • などなど

SDK の概要 - PlayFab | Microsoft Learn

商品設定 プレイヤー情報
playfab_game-manager-timed-consumables-items-tab.png playfab_game-manager-access-player-details.png

Playfab Multiplayer Game Server SDK

PlayFab マルチプレイヤー サーバー (MPS) の管理に役立ちます

MPSにはサーバー用プログラムを展開することができ
クライアント用ゲームからPlayFabゲームサーバーにアクセスすることで
多人数同時参加型オンラインゲームを構築することができます。

サーバー - PlayFab | Microsoft Learn

playfab_multiplayer-server-hosting-service-diagram.png

PlayFab Lobby and Matchmaking SDK

ロビーとマッチメイキング機能を使用できます。

Lobby

Matchmaking

playfab_lobby_matchmaking.png

PlayFab Party SDK

PlayFab パーティー SDK は、ゲームのネットワークと音声またはテキストによるチャット通信を提供します。

ユーザー同士のP2P通信に対応
ゲームデータを通信することで安価で少人数マルチプレイの提供が可能。

Azure Playfab パーティー SDK

playfab_simplified-party-object-hierarchy.png

Unity+PlayFab Party のサンプル

PlayFabPartySample_SS.gif

事前作業

  • Playfabのアカウント作成

  • PlayFabでスタジオとタイトルを作成

  • DeveloperSecretKeyをメモ

  • パーティSDKインストール

  • PlayFabSharedSettingsに作成したタイトルIDとDeveloperSecretKeyを入力

ログイン処理

諸事情でasync/await対応してます。
コールバックでハンドリングできるのでいい感じに実装してください。

    async Task<LoginResult> LoginIntoPlayfab()
    {
        var taskCompletionSource = new TaskCompletionSource<LoginResult>();
        var request = new LoginWithCustomIDRequest {
            CustomId = UnityEngine.Random.value.ToString(),
            CreateAccount = true 
        };
        void onSuccess(LoginResult result)
        {
            taskCompletionSource.SetResult(result);
        }
        void onFailure(PlayFabError error)
        {
            throw new PlayFabException(error.ErrorMessage);
        }
        PlayFabClientAPI.LoginWithCustomID(request, onSuccess, onFailure);
        return await taskCompletionSource.Task;
    }

Party部屋作成処理

NetworkIdは他プレイヤーの入室に使うのでどうにかこうにか渡してください。

    public async Task<string> CreateRoom()
    {
        var result = await LoginIntoPlayfab();
        var taskCompletionSource = new TaskCompletionSource<string>();
        PlayFabMultiplayerManager.Get().OnNetworkJoined += (sender, networkId) =>
        {
            taskCompletionSource.SetResult(networkId);
            IsHost = true;
        };
        PlayFabMultiplayerManager.Get().OnError += (sender, args) =>
        {
            UnityEngine.Debug.LogError(args.Message);
            throw new PlayFabException(args.Message);
        };
        PlayFabMultiplayerManager.Get().CreateAndJoinNetwork();
        return await taskCompletionSource.Task;
    }

Party部屋入室処理

作成時にもらったNetworkIdを使って入室します。

    public async Task<string> JoinRoom(string networkId)
    {
        var result = await LoginIntoPlayfab();
        var taskCompletionSource = new TaskCompletionSource<string>();
        PlayFabMultiplayerManager.Get().OnNetworkJoined += (sender, networkId) =>
        {
            taskCompletionSource.SetResult(networkId);
        };
        PlayFabMultiplayerManager.Get().OnError += (sender, args) =>
        {
            UnityEngine.Debug.LogError(args.Message);
            throw new PlayFabException(args.Message);
        };
        PlayFabMultiplayerManager.Get().JoinNetwork(networkId);
        return await taskCompletionSource.Task;
    }

データ送信

byte配列でデータ送信できます

    public void ChangePosition()
    {
        var position = new Vector3(Random.Range(-3f, 3f), 1f, Random.Range(-3f, 3f));
        var text = position.x + ":" + position.y + ":" + position.z;
        var requestAsBytes = Encoding.UTF8.GetBytes(text);
        PlayFabPartyManager.Instance.SendDataMessage(requestAsBytes);
        player.transform.position = position;
    }
    public void SendDataMessageToAllPlayers(byte[] requestAsBytes)
    {
        PlayFabMultiplayerManager.Get().SendDataMessageToAllPlayers(requestAsBytes);
    }

データ受信

    public void StartGame()
    {
        // データ変更通知を監視する
        PlayFabMultiplayerManager.Get().OnDataMessageReceived += OnDataMessageReceived;
    }

    void OnDataMessageReceived(object sender, PlayFabPlayer from, byte[] buffer)
    {
        // 受信処理
        var text = Encoding.UTF8.GetString(buffer);
        var numList = text.Split(":");
        var position = new Vector3(float.Parse(numList[0]), float.Parse(numList[1]), float.Parse(numList[2]));
        player.transform.position = position;
    }

Unity+PlayFab Lobby and Matchmaking のサンプル

PlayFabLobbySample_SS.gif

事前作業

  • Playfabのアカウント作成

  • PlayFabでスタジオとタイトルを作成

  • DeveloperSecretKeyをメモ

  • ロビーマッチメイキングSDKインストール

  • PlayFabSharedSettingsに作成したタイトルIDとDeveloperSecretKeyを入力

ログイン処理

EntityTokenとEntityKeyは保持します

    async Task<LoginResult> Login()
    {
        var taskCompletionSource = new TaskCompletionSource<LoginResult>();
        var request = new LoginWithCustomIDRequest
        {
            CustomId = UnityEngine.Random.value.ToString(),
            CreateAccount = true
        };
        void onSuccess(LoginResult result)
        {
            PlayFabMultiplayer.SetEntityToken(result.AuthenticationContext);
            entityKey = new PFEntityKey(result.AuthenticationContext);
            taskCompletionSource.SetResult(result);
        }
        void onFailure(PlayFabError error)
        {
            throw new PlayFabException(error.ErrorMessage);
        }
        PlayFabClientAPI.LoginWithCustomID(request, onSuccess, onFailure);
        return await taskCompletionSource.Task;
    }

ロビー作成処理

    public async Task<SampleLobby> CreateLobby(string roomName, string networkId)
    {
        var taskCompletionSource = new TaskCompletionSource<SampleLobby>();
        void onSuccess(Lobby lobby, int result)
        {
            if (LobbyError.SUCCEEDED(result))
            {
                this.lobby = lobby;
                taskCompletionSource.SetResult(new SampleLobby(lobby));
            }
            else
            {
                throw new PlayFabException("Error creating a lobby");
            }
            PlayFabMultiplayer.OnLobbyCreateAndJoinCompleted -= onSuccess;
        }
        void onFailure(PlayFabMultiplayerErrorArgs args)
        {
            throw new PlayFabException("Disconnected from lobby!");
        }

        PlayFabMultiplayer.OnLobbyCreateAndJoinCompleted += onSuccess;
        PlayFabMultiplayer.OnError += onFailure;

        var createConfig = new LobbyCreateConfiguration()
        {
            MaxMemberCount = 4,
            OwnerMigrationPolicy = LobbyOwnerMigrationPolicy.Automatic,
            AccessPolicy = LobbyAccessPolicy.Public
        };

        // ロビーの検索に用いるデータの登録
        createConfig.SearchProperties[PropertyNameRoomName] = roomName;

        // ロビーメンバーで共有できるロビーデータの登録
        createConfig.LobbyProperties[PropertyNamePartyNetworkId] = networkId;

        // メンバーデータ
        var joinConfig = new LobbyJoinConfiguration();

        PlayFabMultiplayer.CreateAndJoinLobby(entityKey, createConfig, joinConfig);
        return await taskCompletionSource.Task;
    }

Lobbyクラスはそのままは使いにくいので
SampleLobbyクラスに変換して利用します。

ロビークラス(SampleLobby)

public class SampleLobby
{
    public string Id { get; private set; }
    public string Name { get; private set; }
    public SampleLobbyMember[] Members { get; private set; }
    public string ConnectionString { get; private set; }

    public string PartyNetworkId;

    public SampleLobby(Lobby lobby)
    {
        Id = lobby.Id;
        Name = lobby.GetSearchProperties()[PlayFabLobbyManager.PropertyNameRoomName];
        PartyNetworkId = lobby.GetLobbyProperties()[PlayFabLobbyManager.PropertyNamePartyNetworkId];
        ConnectionString = lobby.ConnectionString;
    }

    public SampleLobby(LobbySearchResult result)
    {
        Id = result.LobbyId;
        Name = result.SearchProperties[PlayFabLobbyManager.PropertyNameRoomName];
        ConnectionString = result.ConnectionString;
    }
}

ロビー検索処理

    public async Task<IList<SampleLobby>> ListLobbies()
    {
        var taskCompletionSource = new TaskCompletionSource<IList<SampleLobby>>();
        void onSuccess(IList<LobbySearchResult> searchResults, PFEntityKey newMember, int reason)
        {
            if (LobbyError.SUCCEEDED(reason))
            {
                var lobbies = new SampleLobby[searchResults.Count];
                for (var i = 0; i < lobbies.Length; i++)
                {
                    lobbies[i] = new SampleLobby(searchResults[i]);
                }
                taskCompletionSource.SetResult(lobbies);
            }
            else
            {
                throw new PlayFabException("Error finding lobbies");
            }
            PlayFabMultiplayer.OnLobbyFindLobbiesCompleted -= onSuccess;
        }

        LobbySearchConfiguration config = new LobbySearchConfiguration();
        PlayFabMultiplayer.OnLobbyFindLobbiesCompleted += onSuccess;
        PlayFabMultiplayer.FindLobbies(entityKey, config);
        return await taskCompletionSource.Task;
    }

LobbySearchConfigurationを指定することで条件検索できます

ロビー入室処理

    public async Task<SampleLobby> JoinLobby(SampleLobby lobby)
    {
        var taskCompletionSource = new TaskCompletionSource<SampleLobby>();
        void onSuccess(Lobby lobby, PFEntityKey newMember, int reason)
        {
            if (LobbyError.SUCCEEDED(reason))
            {
                this.lobby = lobby;
                var sampleLobby = new SampleLobby(lobby);
                taskCompletionSource.SetResult(sampleLobby);
            }
            else
            {
                throw new PlayFabException("Error finding lobbies! reason=" + reason);
            }
            PlayFabMultiplayer.OnLobbyJoinCompleted -= onSuccess;
        }

        PlayFabMultiplayer.OnLobbyJoinCompleted += onSuccess;

        // 参加するメンバー自身のメンバーデータ
        var memberData = new Dictionary<string, string>{};

        PlayFabMultiplayer.JoinLobby(entityKey, lobby.ConnectionString, memberData);
        return await taskCompletionSource.Task;
    }

サンプルコード

あとがき

なかなかドキュメントがわかりにくく
SDKの名前も公開名と内部名が違っていてかなり混乱しました

SDKごとの対応PFも微妙に違うので
使う場合は気を付けたほうがいいです

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?