게임 'Sleighers'의 서버 Repository입니다.
- 설산에서 즐기는 멀티 레이싱 게임 Sleighers
- 직접 제작한 설산에서 플레이하는 썰매 라이딩
- 한 게임에 최대 5인까지 참여 가능
- Unity 클라이언트 & Node.js 서버
- Out Game과 In Game 서버를 나누어 설계, 구현
- Out Game : Node.js, WebSocket(Socket.io)
- In Game : Node.js, TCP
- DB : MySQL(AWS RDS)
- 배포 : AWS EC2
- DB는 AWS RDS의 MySQL을 사용했습니다.
- 일반적인 로그인, Google OAuth. 두 가지 방법을 구현했습니다.
Login Code 조각
Server-node.js/AuthServer/EventHandler.js
Lines 49 to 69 in 722e4df
if (defaultLogin) { // 기본 로그인 | |
const loginPW = userData.password; | |
connection.query('SELECT * FROM User WHERE email = ? AND password = ?', [loginUser, loginPW], (err, rows) => { | |
if (err) { | |
logger.error(`Login query error: ${err}`); | |
socket.emit('loginFail', 'login query fail'); | |
return; | |
} | |
if (rows.length === 0) { // 등록되지 않은 사용자 | |
logger.error(`등록되지 않은 사용자: ${err}`); | |
socket.emit('loginFail', '존재하지 않는 ID 또는 비밀번호입니다'); | |
} | |
else { | |
const queryResult = new Packet(rows[0].email, null, rows[0].nickname); | |
console.log("query : ", queryResult); | |
socket.emit('loginSucc', 'default login succ'); | |
socket.emit('inquiryPlayer', JSON.stringify(queryResult)); | |
socket.id = rows[0].nickname; | |
connectedPlayers.set(rows[0].nickname, {socket : socket, room : null, state : 'lobby'}); | |
} | |
}); |
Set Name Code 조각
Server-node.js/AuthServer/EventHandler.js
Lines 148 to 158 in 722e4df
connection.query('UPDATE User SET nickname = ? WHERE email = ?', [userData.nickname, userData.email], (err) => { | |
if (err) { | |
logger.error(`SetName query error: ${err}`); | |
socket.emit('setNameFail', 'setName fail'); | |
return; | |
} | |
socket.emit('setNameSucc', userData.nickname); | |
connectedPlayers.set(userData.nickname, {socket : socket, room : null, state : 'lobby'}); | |
connectedPlayers.delete(socket.id); | |
socket.id = userData.nickname; | |
}); |
- 1~5인까지 하나의 게임에 참여할 수 있습니다.
- 5인이 매칭되면 즉시 게임이 시작됩니다.
- 5인 미만일 경우, 매칭 시작 후, 일정 시간이 지나면 매칭에 참여한 인원만으로 게임이 시작됩니다.
Match Making Code 조각
Server-node.js/AuthServer/EventHandler.js
Lines 197 to 213 in 722e4df
if(matchList.length === 5 && !matchList.processed) | |
{ | |
matchList.processed = true; // 처리 플래그 설정 | |
processMatchList(matchList, firstRoomID); | |
sendMatchList(firstRoomID, matchList); | |
} | |
else if(!matchList.timeoutId) | |
{ | |
const timeoutId = setTimeout(() => { | |
if (!matchList.processed) { | |
processMatchList(matchList, firstRoomID); | |
sendMatchList(firstRoomID, matchList); | |
} | |
},60000); // 10초 (10000ms) 후에 실행 | |
matchList.timeoutId = timeoutId; // 매치리스트에 타임아웃 ID 저장 | |
} |
Server-node.js/AuthServer/EventHandler.js
Lines 216 to 249 in 722e4df
function processMatchList(matchList, roomID) { | |
const matchPromise = getMatchList(matchList,roomID); | |
matchPromise.then(sendList => { | |
sendList.forEach(element => { | |
const user = getPlayer(element.nickname); | |
if (user === undefined) { | |
logger.error(`[processMatchList] user is undefined, name : ${element.nickname}`); | |
} else { | |
user.socket.emit('enterRoomSucc', '{"roomID":' + roomID + ',"playerList":' + JSON.stringify(sendList) + '}' ); | |
user.state = 'ready'; | |
} | |
}); | |
logger.info(`Enter Room Succ!! room : ${roomID}`); | |
readyRoomList.set(roomID, {userList : matchList, readyCount : 0}); | |
matchRoomList.delete(roomID); | |
// 5초 후에 moveInGameScene 이벤트 emit | |
setTimeout(() => { | |
sendList.forEach(element => { | |
const user = getPlayer(element.nickname); | |
if (user === undefined) { | |
logger.error(`[processMatchList] user is undefined, name : ${element.nickname}`); | |
} else { | |
user.socket.emit('loadGameScene', 'Move to in-game scene'); | |
user.state = 'ingame' | |
} | |
}); | |
logger.info('Move to in-game scene'); | |
}, 5000); // 5초 (5000ms) 후에 실행 | |
gameRoomList.set(roomID, {userList : matchList}); | |
readyRoomList.delete(roomID); | |
}); | |
} |
CountDown Event 일부
Server-node.js/InGameServer/ProtocolHandler.js
Lines 105 to 134 in 722e4df
if (count === 0) { | |
clearInterval(countDown); | |
logger.info("카운트다운 종료~"); | |
if(protocol === Protocol.GameStart) | |
{ | |
const dataBuffer = classToByte(new Packet(protocol, roomID)); | |
broadcastAll(dataBuffer, roomID); | |
gameRoomList.get(roomID).startTime = Date.now(); | |
} | |
else if(protocol === Protocol.GameEnd) | |
{ | |
const gameRoom = gameRoomList.get(roomID); | |
const endTime = Date.now() - gameRoom.startTime; | |
gameRoomList.get(roomID).state = false; | |
const resultList = []; | |
gameRoom.gameResult.forEach((value, key) => { | |
resultList.push({nickname : key, rank : value.rank, goalTime : value.goalTime}); | |
}); | |
gameRoom.playerList.forEach(player => { | |
if (!gameRoom.gameResult.has(player)) { | |
resultList.push({nickname : player, rank : 0, goalTime : 0}); | |
} | |
}); | |
const dataBuffer = classToByte(new GameResultPacket(roomID, resultList, endTime)); | |
broadcastAll(dataBuffer, roomID); | |
} | |
} | |
else{ | |
count--; | |
} |
- semaphore를 이용해 여러 플레이어가 Goal에 접근한 경우의 동시성 문제를 해결했습니다.
Player Goal 처리 Func
Server-node.js/InGameServer/ProtocolHandler.js
Lines 138 to 153 in 722e4df
function PlayerGoal(id, roomID){ | |
const gameRoom = gameRoomList.get(roomID); | |
if(gameRoom !== undefined && gameRoom.state === true) | |
{ | |
if (gameRoom.goalCount === 0) { | |
CountDown(Protocol.GameEnd, roomID); | |
} | |
sema.take(function() { | |
if (!gameRoom.gameResult.has(id)) { // 중복 goal 방지 | |
gameRoom.goalCount++; | |
gameRoom.gameResult.set(id, {rank : gameRoom.goalCount, goalTime : Date.now() - gameRoom.startTime + 100 }); | |
} | |
sema.leave(); | |
}); | |
} | |
} |