← 使用情境

Unity 大廳、配對與排行榜

用 PusherClient for .NET 做 presence 大廳、「配對成功」與快取排行榜。

遊戲裡不是每則訊息都需要遊戲伺服器。大廳、「配對成功」、好友上線、聊天、排行榜與營運公告都是事件:小、不頻繁,而且只送給在意的玩家。Hibira 透過 WebSocket 傳送這些事件,用戶端是開源的 .NET 函式庫 PusherClient,你的後端一次 HTTP 呼叫就能發布,不必自己架 socket 伺服器。每一幀都在變的狀態,請繼續交給你的遊戲網路方案(Netcode for GameObjects、Photon、Mirror 或自製)。

玩家得到什麼

  • 大廳裡誰在、誰已準備好,不必重新整理就看得到。
  • 配對器一決定就跳出「配對成功」,不用盯著輪詢的轉圈圈。
  • 排行榜與活動倒數在畫面打開的那一刻就是最新的。

用到的功能

  • Unity 中的 PusherClient(pusher-websocket-dotnet)。
  • 用於大廳與隊伍的 presence 頻道(presence-)。
  • 玩家之間傳「準備好了」等小訊號的用戶端事件(client-*)。
  • 傳訊息給單一玩家的私有頻道(private-)。
  • 快取頻道(cache-),把最新排行榜交給每位打開它的玩家。

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

請設定 Host 而不是 Cluster:設定 cluster 會讓函式庫連回 pusher.com。App secret 留在你的後端,由後端在授權端點簽署私有與 presence 頻道。

步驟 2:有 presence 與「準備好了」的大廳

[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");

一次 trigger 最多可送到 100 個頻道,不論多少玩家訂閱,每個頻道都只算一則訊息。後端的伺服器端寫法請看 Node.js 指南與 .NET 指南。

小提醒

  • WebGL 版本無法從 C# 開啟 socket。這時請在網頁載入 pusher-js,透過 .jslib 外掛以相同的 host 與頻道呼叫它。
  • 在行動裝置上,遊戲暫停時(OnApplicationPause)斷線,恢復時再連上;遊戲關閉時的訊息,請用推播通知。
  • presence 頻道最多 100 位成員,適合大廳與隊伍。整個遊戲的「線上玩家數」請看大量觀眾。
  • 用戶端事件有每條連線的頻率限制,而且每則都算一則訊息,所以請送狀態變化,而不是每一幀。