var game = require('../game');
// Create Socket.io Server
game(server);
HTTP 서버가 준비되면 소켓 서버를 함께 구동하도록 game(server)를 연결해준다.
이렇게 해주면 동일 포트에서 HTTP와 WebSocket이 함께 동작한다.
1.2. 방 매칭 로직과 이벤트 라우팅
game.js는 실제 Socket.io 서버를 생성하고
접속한 클라이언트를 대기열 → 방 배정 → 게임 시작 흐름으로 연결한다.
const { v4: uuidv4 } = require('uuid');
module.exports = function(server) {
const io = require('socket.io')(server, {
transports: ['websocket']
});
// 방 정보
var rooms = [];
var socketRooms = new Map();
io.on('connection', (socket) => { // 클라이언트가 서버에게 연결된다면 socket 매개변수로 아래 내용 동작
// 서버 구현
console.log('A user connected :', socket.id);
// 특정 Socket(클라이언트)이 입장했을 때 처리
// 1. 대기방에 있으면 입장
// 2. 대기방에 방이 없으면 새로 생성 후 입장
if (rooms.length > 0) {
var roomId = rooms.shift(); // shift = 배열에 있는 값을 꺼낸다.
socket.join(roomId);
socket.emit('joinRoom', { roomId: roomId }); // 클라이언트(소켓 대상)에게 메세지 보내기
socket.to(roomId).emit('StartGame', {roomId: roomId});
socketRooms.set(socket.id, roomId);
}else {
var roomId = uuidv4();
socket.join(roomId);
socket.emit("createRoom", {roomId: roomId});
rooms.push(roomId);
socketRooms.set(socket.id, roomId);
}
// 특정 Socket(클라이언트)이 방이 나갔을 때 처리
socket.on('leaveRoom', function(data) {
var roomId = data.roomId;
socket.leave(roomId);
socket.emit('exitRoom');
socket.to(roomId).emit('endGame');
// 혼자 들어간 방에서 나갈 때 방 삭제
const roomIdx = rooms.indexOf(roomId);
if (roomId !== -1) {
rooms.splice(roomIdx, 1);
console.log('Room Deleted : ', roomId);
}
// 방 나간 소켓 정보 삭제
socketRooms.delete(socket.id);
});
// Socket(클라이언트)이 특정 Block을 터치했을 때 처리
socket.on('doPlayer', function(playerInfo) {
var roomId = playerInfo.roomId;
var blockIndex = playerInfo.blockIndex;
console.log('Player action in room :', roomId, 'Block index : ', blockIndex);
socket.to(roomId).emit('doOpponent', {blockIndex: blockIndex});
});
});
};
[전체 흐름]
대기열(rooms): 아직 1명만 들어온 방의 ID를 저장한다. 새로운 플레이어가 접속하면 이 대기열에서 하나 꺼내 짝을 맞춰준다
소켓-방 매핑(socketRooms): 어떤 소켓이 어느 방에 있는지 빠르게 찾기 위한 자료구조다
socket.join(roomId): 소켓을 특정 방에 묶는다. 이후 socket.to(roomId).emit(...)으로 그 방의 상대에게만 이벤트를 보낼 수 있다
이벤트 흐름
createRoom: 내가 새 방을 만들고 대기 중임
joinRoom: 대기 중인 방에 두 번째 플레이어로 합류함
StartGame/startGame: 상대가 들어왔으니 게임을 시작하라는 신호
doPlayer → doOpponent: 내 입력을 서버가 받아 같은 방의 상대에게만 전달함
leaveRoom → exitRoom/endGame: 방에서 나가거나 연결이 끊긴 경우 정리 신호
[메서드 설명]
io.on('connection', (socket) => { ... }) : 어떤 클라이언트가 연결되면 이 콜백이 실행되고 그 연결을 대표하는 객체가 socket이다.
socket.on('이벤트명', (payload) => { ... }) : 해당 소켓에서 도착한 커스텀 이벤트를 받는다.
socket.emit('event', data) : 해당 소켓에만 이벤트를 보낸다
socket.to(roomId).emit('event', data) : 같은 방의 다른 사람들에게 이벤트를 보낸다 (나 자신 제외)
(참고) 나 포함 모두에게 이벤트를 보내고싶을 땐 io.to(roomId).emit(..)을 사용한다.
이 메서드들은 모두 Socket.IO 서버 라이브러리가 제공하는 API 이다.
[uuid v4 ?]
UUID는 고유 식별자이고
v4는 랜덤 기반으로 128비트의 문자열을 만든다.
방 ID를 1, 2, 3같은 연속 숫자로 만들면 추측/충돌 위험이 커지는데 이러한 문제점을 방지하기 위하여 uuid v4를 사용해준다고 한다.
[playerInfo]
서버의 socket.on('doPlayer', function(playerInfo) { ... })에서 playerInfo는 클라이언트가 보낸 페이로드를 그대로 받은 매개변수이다.
2. 클라이언트
2.1. 소켓 서버 주소와 상태 정의
멀티 플레이에서 사용할 서버의 주소와 상태값을 enum 타입으로 정의해 주었다.
public static class Constants
{
public const string SocketServerURL = "ws://localhost:3000";
public enum MultiplayContollerState
{
CreateRoom, // 방 생성
JoinRoom, // 생성된 방에 참여
StartGame, // 생성한 방에 다른 유저가 참여해서 게임을 시작
ExitRoom, // 클라이언트가 방을 빠져 나왔을 때
EndGame // 상대방이 접속을 끊거나 방을 나갔을 때
}
}
2.2. 소켓 연결과 이벤트 핸들러 틀
using System;
using Newtonsoft.Json;
using SocketIOClient;
using UnityEngine;
// joinRoom/createRoom 이벤트 전달할 때 전달되는 정보의 타입
public class RoomData
{
[JsonProperty("roomId")]
public string roomId { get; set; }
}
// 상대방이 둔 마커 위치
public class BlockData
{
[JsonProperty("blockIndex")]
public int blockIndex { get; set; }
}
public class MultiplayController
{
private SocketIOUnity _socket;
public MultiplayController()
{
var uri = new Uri(Constants.SocketServerURL);
_socket = new SocketIOUnity(uri, new SocketIOOptions
{
Transport = SocketIOClient.Transport.TransportProtocol.WebSocket
});
_socket.On("createRoom", CreateRoom);
_socket.On("joinRoom", JoinRoom);
_socket.On("startGame", StartGame);
_socket.On("exitGame", ExitGame);
_socket.On("endGame", EndGame);
_socket.On("doOpponent", DoOpponent);
}
private void CreateRoom(SocketIOResponse response)
{
var data = response.GetValue<RoomData>();
}
private void JoinRoom(SocketIOResponse response)
{
var data = response.GetValue<RoomData>();
}
private void StartGame(SocketIOResponse response)
{
var data = response.GetValue<RoomData>();
}
private void ExitGame(SocketIOResponse response)
{
}
private void EndGame(SocketIOResponse response)
{
}
private void DoOpponent(SocketIOResponse response)
{
var data = response.GetValue<BlockData>();
}
}
생성자에서 Constants.SocketServerURL로 접속하고 전송 프로토콜을 WebSocket으로 지정한다.