← 使用场景

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 位成员,适合大厅和队伍。整个游戏的“在线玩家数”请看大量观众。
  • 客户端事件有每个连接的频率限制,而且每条都算一条消息,所以请发送状态变化,而不是每一帧。