Skip to content

SnowRail/Server-node.js

Repository files navigation

게임 'Sleighers'의 서버 Repository입니다.

프로젝트 소개 - 🛷 Sleighers

  • 설산에서 즐기는 멀티 레이싱 게임 Sleighers
  • 직접 제작한 설산에서 플레이하는 썰매 라이딩
  • 한 게임에 최대 5인까지 참여 가능
  • Unity 클라이언트 & Node.js 서버

서버 구조
Node.js socket.io MySQL AWS EC2

  • Out Game과 In Game 서버를 나누어 설계, 구현
  • Out Game : Node.js, WebSocket(Socket.io)
  • In Game : Node.js, TCP
  • DB : MySQL(AWS RDS)
  • 배포 : AWS EC2

서버 기능 소개

OutGame 서버

✅ 로그인 / 회원가입

  • DB는 AWS RDS의 MySQL을 사용했습니다.
  • 일반적인 로그인, Google OAuth. 두 가지 방법을 구현했습니다.
Login Code 조각

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 조각

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 조각

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 저장
}
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);
});
}


InGame 서버

✅ Game State, Event 관리

CountDown Event 일부

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--;
}

✅ 플레이어 Position broadcast

✅ 최종 Ranking 판단 및 관리

  • semaphore를 이용해 여러 플레이어가 Goal에 접근한 경우의 동시성 문제를 해결했습니다.
Player Goal 처리 Func

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();
});
}
}


About

Sleighers Server

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •