← 활용 사례

Unity 로비, 매칭, 리더보드

PusherClient for .NET으로 프레즌스 로비, ‘매칭 완료’, 캐시된 리더보드.

게임의 모든 메시지에 게임 서버가 필요한 것은 아닙니다. 로비, "매칭 완료", 친구 접속, 채팅, 리더보드, 라이브 운영 공지는 이벤트입니다. 작고, 가끔 있으며, 관심 있는 플레이어에게만 가면 됩니다. Hibira는 이것들을 WebSocket으로 전달하고, 클라이언트는 오픈소스 .NET 라이브러리 PusherClient입니다. 백엔드는 HTTP 호출 한 번으로 발행하며 소켓 서버를 직접 운영할 필요가 없습니다. 매 프레임 바뀌는 상태는 지금처럼 게임 네트워킹(Netcode for GameObjects, Photon, Mirror 또는 자체 구현)에 맡기세요.

플레이어가 얻는 것

  • 로비에 누가 있고 누가 준비됐는지 새로고침 없이 보입니다.
  • 매치메이커가 결정하는 순간 "매칭 완료". 폴링 스피너를 기다릴 필요가 없습니다.
  • 리더보드와 이벤트 카운트다운은 화면을 여는 순간부터 최신입니다.

사용하는 기능

  • Unity에서 쓰는 PusherClient(pusher-websocket-dotnet).
  • 로비와 파티를 위한 프레즌스 채널(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();
}

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

trigger 한 번으로 최대 100개 채널에 보낼 수 있고, 구독한 플레이어 수와 상관없이 채널마다 메시지 1건으로 계산됩니다. 백엔드 쪽 코드는 Node.js 가이드와 .NET 가이드에 있습니다.

팁

  • WebGL 빌드에서는 C#에서 소켓을 열 수 없습니다. 이때는 페이지에서 pusher-js를 불러와 같은 호스트와 채널로 .jslib 플러그인을 통해 호출하세요.
  • 모바일에서는 게임이 일시 정지되면(OnApplicationPause) 연결을 끊고 재개할 때 다시 연결하세요. 게임이 닫혀 있는 동안의 메시지는 푸시 알림을 쓰세요.
  • 프레즌스 채널은 멤버가 최대 100명이라 로비와 파티에 알맞습니다. 게임 전체의 "접속 중인 플레이어 수"는 대규모 시청자를 참고하세요.
  • 클라이언트 이벤트는 연결마다 속도 제한이 있고 하나하나가 메시지로 계산되므로, 매 프레임이 아니라 상태가 바뀔 때 보내세요.