← ユースケース

Unity のロビー、マッチング、ランキング

PusherClient for .NET で、プレゼンスのロビー、「マッチング成立」、キャッシュしたランキング。

ゲームのメッセージすべてにゲームサーバーが要るわけではありません。ロビー、「マッチング成立」、フレンドのオンライン、チャット、ランキング、ライブ運営のお知らせはイベントです。小さく、ときどき、気にしているプレイヤーにだけ届けばいい。Hibira はこれらを WebSocket で運び、クライアントはオープンソースの .NET ライブラリ PusherClient。バックエンドは HTTP 呼び出し 1 回で配信でき、自前のソケットサーバーは要りません。毎フレーム変わる状態は、これまでどおりゲーム用のネットワーク(Netcode for GameObjects、Photon、Mirror、自作など)に任せてください。

プレイヤーにとっての価値

  • ロビーに誰がいて誰が準備完了か、更新しなくても見えます。
  • マッチメイカーが決めた瞬間に「マッチング成立」。ポーリングのくるくるを待つ必要はありません。
  • ランキングとイベントのカウントダウンは、画面を開いた瞬間から最新です。

使う機能

  • Unity で使う PusherClient(pusher-websocket-dotnet)。
  • ロビーやパーティーのためのプレゼンスチャンネル(presence-)。
  • プレイヤー同士の「準備完了」などの小さな合図に使うクライアントイベント(client-*)。
  • 1 人のプレイヤーに届けるプライベートチャンネル(private-)。
  • 最新のランキングを開いたプレイヤー全員に渡すキャッシュチャンネル(cache-)。

ステップ 1:接続は 1 回、イベントはメインスレッドで

NuGet から PusherClient パッケージを追加し(たとえば NuGetForUnity で)、このコンポーネントを最初のシーンの GameObject に付けます。

using System;
using System.Collections.Concurrent;
using System.Net.Http.Headers;
using PusherClient;
using UnityEngine;

public class Realtime : MonoBehaviour
{
    public static Realtime Instance { get; private set; }
    public Pusher Client { get; private set; }

    // PusherClient raises events on a background thread; Unity's API only works on the main thread.
    readonly ConcurrentQueue<Action> mainThread = new ConcurrentQueue<Action>();
    public void Post(Action action) => mainThread.Enqueue(action);

    async void Awake()
    {
        Instance = this;
        DontDestroyOnLoad(gameObject);
        Client = new Pusher("YOUR_APP_KEY", new PusherOptions
        {
            Host = "ws.hibira.io",
            Encrypted = true,
            ChannelAuthorizer = new HttpChannelAuthorizer("https://api.example.com/pusher/auth")
            {
                AuthenticationHeader = new AuthenticationHeaderValue("Bearer", Session.Token),
            },
        });
        Client.Error += (sender, error) => Debug.LogWarning(error);
        await Client.ConnectAsync();
    }

    void Update()
    {
        while (mainThread.TryDequeue(out var action)) action();
    }

    async void OnApplicationQuit() => await Client.DisconnectAsync();
}

Cluster ではなく Host を設定してください。cluster を設定するとライブラリが pusher.com に向き直ります。アプリのシークレットはバックエンドに置き、認可エンドポイントでプライベート/プレゼンスチャンネルに署名します。

ステップ 2:プレゼンスと「準備完了」のあるロビー

[Serializable]
public class Player { public string name; public int level; }

var rt = Realtime.Instance;
var lobby = await rt.Client.SubscribePresenceAsync<Player>("presence-lobby-" + lobbyId);

foreach (var m in lobby.GetMembers()) rt.Post(() => LobbyUI.Add(m.Key, m.Value));
lobby.MemberAdded += (sender, m) => rt.Post(() => LobbyUI.Add(m.Key, m.Value));
lobby.MemberRemoved += (sender, m) => rt.Post(() => LobbyUI.Remove(m.Key));

// "Ready" goes straight to the other players: turn on Allow client events in the app settings
lobby.Bind("client-ready", (PusherEvent e) => rt.Post(() => LobbyUI.SetReady(e.UserId, true)));
readyButton.onClick.AddListener(() => lobby.Trigger("client-ready", new { ready = true }));

誰がロビーに入れるかは認可エンドポイントが決め、各プレイヤーの user_id と user_info(ここでは名前とレベル)を返します。これはメンバー全員に届きます。

ステップ 3:マッチング成立とランキングをバックエンドから

// When the matchmaker pairs players
await pusher.trigger(players.map((p) => 'private-player-' + p.id), 'match-found', { matchId, server: address });

// The latest standings, kept for players who open the screen later
await pusher.trigger('cache-leaderboard-weekly', 'update', { top: await leaderboard.top(20) });
var rt = Realtime.Instance;

var me = await rt.Client.SubscribeAsync("private-player-" + Session.PlayerId);
me.Bind("match-found", (PusherEvent e) => rt.Post(() => Matchmaking.Join(JsonUtility.FromJson<Match>(e.Data))));

// Listen before subscribing, so the cached standings sent right after the subscription are not missed
rt.Client.BindAll((string eventName, PusherEvent e) =>
{
    if (e.ChannelName == "cache-leaderboard-weekly" && eventName == "update")
        rt.Post(() => LeaderboardUI.Render(JsonUtility.FromJson<Standings>(e.Data)));
});
await rt.Client.SubscribeAsync("cache-leaderboard-weekly");

1 回の trigger で最大 100 チャンネルに届き、何人が購読していてもチャンネルごとに 1 メッセージとして数えます。バックエンド側の書き方は Node.js ガイドと .NET ガイドにあります。

ヒント

  • WebGL ビルドでは C# からソケットを開けません。その場合はページで pusher-js を読み込み、同じホストとチャンネルで .jslib プラグインから呼び出します。
  • モバイルではゲームが一時停止したら(OnApplicationPause)切断し、再開時に接続し直します。ゲームを閉じている間のメッセージにはプッシュ通知を使いましょう。
  • プレゼンスチャンネルのメンバーは最大 100 人で、ロビーやパーティーに向いています。ゲーム全体の「オンライン人数」は大規模な視聴者を参照してください。
  • クライアントイベントは接続ごとにレート制限があり、1 件ごとに 1 メッセージとして数えます。毎フレームではなく、状態が変わったときに送りましょう。