← 활용 사례

멀티플레이 게임 방과 로비

파티, 퀴즈, 보드, 카드 게임: 코드로 들어가는 방, 자리, 방장, 준비 상태, 채팅을 게임 서버 없이 프레즌스 채널과 클라이언트 이벤트로 만듭니다.

파티 게임, 퀴즈, 보드게임과 카드 게임, 함께 보는 방은 모두 같은 방식으로 시작합니다. 방 코드, 누가 들어와 있는지 보여 주는 목록, 준비 버튼, 그리고 시작을 누르는 방장입니다. 이 부분에는 게임 서버가 필요 없습니다. 방은 프레즌스 채널이고, 오픈소스 패키지 @hibira-io/lobby가 브라우저에서 이를 자리, 방장, 준비 상태, 강퇴, 시작, 방 채팅으로 만들어 줍니다.

플레이어가 얻는 것

  • 코드나 링크로 들어가는 방. 자리가 차 가고, 가득 차면 "방이 가득 찼습니다"가 분명하게 보입니다.
  • 준비 상태, 방장 배지, 시작 버튼을 모두가 동시에 봅니다.
  • "입력 중…"이 보이는 방 채팅. 방장이 나가면 다음 사람이 방장이 됩니다.

사용하는 기능

  • 프레즌스 채널(presence-): 각 방에 있는 사람들의 목록.
  • 클라이언트 이벤트(client-*): 준비 상태, 설정, 시작, 채팅, 게임 중 조작을 브라우저끼리 주고받습니다.
  • HTTP API(GET /channels?filter_by_prefix=presence-room-&info=user_count): 데이터베이스 없이 방 목록을 만듭니다.
  • @hibira-io/lobby: 위의 기능들을 묶어 방 로직으로 만들어 줍니다.

1단계: 서버에서 방 인가

인가 엔드포인트는 누가 들어올 수 있는지 정하고, 각 플레이어가 들어온 시각에 서명하며, 방이 가득 차면 거절합니다. 모든 플레이어가 그 시각으로 순서를 정하므로 자리와 방장에 대해 모두의 판단이 같습니다.

import Pusher from 'pusher';
import { authorizeRoom, listRooms, RoomFullError } from '@hibira-io/lobby/server';

const hibira = new Pusher({ appId, key, secret, host: 'ws.hibira.io', useTLS: true });

app.post('/rooms/auth', requireLogin, express.urlencoded({ extended: false }), async (req, res) => {
  try {
    res.json(await authorizeRoom(hibira, {
      socketId: req.body.socket_id,
      channel: req.body.channel_name,
      userId: req.user.id,
      userInfo: { name: req.user.name },
      capacity: 4,
    }));
  } catch (err) {
    res.status(err instanceof RoomFullError ? 403 : 500).end();
  }
});

// Room browser: [{ id: 'KQTZ', channel: 'presence-room-KQTZ', members: 3 }]
app.get('/rooms', async (req, res) => res.json(await listRooms(hibira)));

2단계: 브라우저에서 방에 입장

앱 설정에서 클라이언트 이벤트 허용을 켠 다음:

import { Room } from '@hibira-io/lobby';

const room = Room.join(pusher, 'presence-room-KQTZ', { capacity: 4 });

room.on('members', ({ players, host }) => renderSeats(players, host));
room.on('message', (m) => chat.append(m.info.name, m.text));
room.on('full', () => show('This room is full'));

readyButton.onclick = () => room.setReady(!room.me.ready);
chatForm.onsubmit = () => room.send(input.value);
input.oninput = () => room.sendTyping();

room.players는 자리 순서대로 플레이어를 나열하며, 각 플레이어에 ready와 host 값이 있습니다. 내 조작은 바로 반영되고, 방장만 보낼 수 있는 이벤트를 다른 사람이 보내면 무시됩니다.

3단계: 게임 시작

모두 준비되면 방장이 시작합니다. start()에 넘긴 데이터는 모든 플레이어에게 전달됩니다. 예를 들어 라운드 타이밍, 난수 시드, 게임 서버 주소 등입니다.

if (room.isHost && room.allReady) room.start({ seed: 1234, rounds: 5 });

room.on('start', ({ data }) => game.begin(data));

게임 중 조작은 같은 채널의 클라이언트 이벤트 room.channel.trigger('client-move', move)로 보내거나, 게임 서버가 필요한 게임이라면 게임 서버를 거치면 됩니다.

Laravel에서는

프레즌스 채널 콜백에서 입장 시각을 반환하고, Echo.join()의 채널을 사용합니다.

// routes/channels.php
Broadcast::channel('room.{code}', function ($user, $code) {
    return ['id' => $user->id, 'name' => $user->name, 'joined_at' => (int) floor(microtime(true) * 1000)];
});
const room = new Room(Echo.join(`room.${code}`).subscription, { capacity: 4 });

직접 해 보기

Reaction rooms 예제는 완성된 Next.js 게임입니다. 방 목록, 자리 4개, 준비, 채팅, 5라운드 반응 속도 게임이 있으며 무료 앱으로 동작합니다.

팁

  • 방 하나에는 최대 100명까지 들어갈 수 있습니다(프레즌스 채널 한도).
  • 클라이언트 이벤트는 받는 플레이어 수와 관계없이 메시지 1건으로 계산됩니다. 4명이 짧은 게임을 해도 수십 건 정도입니다.
  • 게임이 시작된 뒤에는 클라이언트를 신뢰하는 구조입니다. 부정행위가 중요한 게임이라면 서버에서 결과를 확인하거나 게임 상태를 게임 서버에 두고, 방은 로비로만 쓰세요.
  • Unity로 만든다면 Unity 로비, 매칭, 리더보드도 참고하세요.