Sảnh chờ, ghép trận và bảng xếp hạng trong Unity
Sảnh presence, “đã tìm thấy trận” và bảng xếp hạng cache với PusherClient for .NET.
Không phải tin nhắn nào trong game cũng cần máy chủ game. Sảnh chờ, "đã tìm thấy trận", bạn bè trực tuyến, chat, bảng xếp hạng và thông báo sự kiện đều là event: nhỏ, thỉnh thoảng mới có, và chỉ gửi tới những người chơi quan tâm. Hibira chuyển chúng qua WebSocket với thư viện .NET mã nguồn mở PusherClient, nên backend của bạn phát chỉ bằng một lệnh gọi HTTP mà không cần máy chủ socket riêng. Hãy giữ phần mạng gameplay (Netcode for GameObjects, Photon, Mirror hoặc tự viết) cho trạng thái thay đổi mỗi khung hình.
Người chơi nhận được gì
- Sảnh chờ hiển thị ai đang ở đó và ai đã sẵn sàng, không cần tải lại.
- "Đã tìm thấy trận" ngay khi hệ thống ghép trận quyết định, thay vì một vòng quay polling.
- Bảng xếp hạng và đồng hồ đếm ngược sự kiện luôn mới ngay khi mở màn hình.
Dùng những gì
- PusherClient (pusher-websocket-dotnet) trong Unity.
- Kênh presence (
presence-) cho sảnh chờ và nhóm. - Client event (
client-*) cho "sẵn sàng" và các tín hiệu nhỏ khác giữa người chơi. - Kênh riêng tư (
private-) cho tin nhắn tới một người chơi. - Kênh cache (
cache-), giao bảng xếp hạng mới nhất cho mọi người chơi mở nó.
Bước 1: kết nối một lần, xử lý event trên luồng chính
Thêm gói PusherClient từ NuGet (ví dụ bằng NuGetForUnity), rồi gắn component này vào một GameObject trong scene đầu tiên:
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();
}Đặt Host chứ không đặt Cluster: đặt cluster sẽ khiến thư viện trỏ lại pusher.com. App secret nằm ở backend của bạn, nơi ký các kênh riêng tư và presence tại endpoint cấp quyền.
Bước 2: sảnh chờ với presence và "sẵn sàng"
[]
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 }));Endpoint cấp quyền của bạn quyết định ai được vào sảnh và trả về user_id cùng user_info của từng người chơi (ở đây là tên và cấp độ), mà mọi thành viên đều nhận được.
Bước 3: ghép trận và bảng xếp hạng từ backend
// 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");Một lần trigger tới được tối đa 100 kênh, và tính là một tin nhắn cho mỗi kênh dù có bao nhiêu người chơi đăng ký. Hướng dẫn Node.js và hướng dẫn .NET có phần máy chủ cho backend của bạn.
Mẹo
- Bản build WebGL không mở được socket từ C#. Khi đó hãy tải pusher-js trên trang và gọi nó qua một plugin
.jslibvới cùng host và kênh. - Trên di động, ngắt kết nối khi game tạm dừng (
OnApplicationPause) và kết nối lại khi tiếp tục; với tin nhắn khi game đã đóng, hãy dùng thông báo đẩy. - Kênh presence chứa tối đa 100 thành viên, hợp với sảnh chờ và nhóm. Để có số "người chơi trực tuyến" của cả game, xem khán giả đông.
- Client event bị giới hạn tốc độ theo từng kết nối và mỗi cái tính là một tin nhắn, nên hãy gửi khi trạng thái đổi, không phải mỗi khung hình.