본문 바로가기

Node

webSocket 게임 서버 개발 강의 코드 이해하기

  • src/init/socket.js
    초기화 및 설정
import { Server as SocketIO } from 'socket.io';
import registerHandler from '../handlers/register.handler.js';

const initSocket = (server) => {
  const io = new SocketIO();
  io.attach(server);

  registerHandler(io);
};

export default initSocket;

Socket.IO 서버를 초기화하고 registerHandler을 통해 클라이언트 연결 인벤트를 처리한다.

 

  • src/handlers/register.handler.js
    사용자 등록 및 연결
import { addUser } from '../models/user.model.js';
import { v4 as uuidv4 } from 'uuid';
import { handleConnection, handleDisconnect, handlerEvent } from './helper.js';
const registerHandler = (io) => {
  //접속시 이벤트
  io.on('connection', (socket) => {
    const userUUID = uuidv4();
    addUser({ uuid: userUUID, socketId: socket.id });
    handleConnection(socket, userUUID);

    socket.on('event', (data) => handlerEvent(io, socket, data));

    //접속 해제시 이벤트
    socket.on('disconnect', (socket) => handleDisconnect(socket, userUUID));
  });
};

export default registerHandler;

클라이언트가 서버에 열결될 때 호출된다. 새로운 사용자 UUID를 생성하고 사용자 정보를 저장한다.

handleConnection 을 호출하여 연결된 소캣에 대한 초기 처리를 한다.

연결된 소켓에서 event이벤트를 수신하면 handlerEvent를 호출하여 해당 이벤트를 처리한다.

 

  • src/handlers/helper.js
    이벤트 핸들링
import { Socket } from 'socket.io';
import { getUser, removeUser } from '../models/user.model.js';
import { getGameAssets } from '../init/assets.js';
import { createStage, getStage, setStages } from '../models/stage.model.js';
import { CLIENT_VERSION } from '../constants.js';
import handlerMappings from './handlerMapping.js';

export const handleDisconnect = (socket, uuid) => {
  removeUser(socket.id);
  console.log(`User disconnected:${socket.id}`);
  console.log('Current users:', getUser());
};

//스테이지에 따라 더 높은 점수 획득
//1스테 , 0 ->1점
//2스테 , 1000 -> 2점

export const handleConnection = (socket, uuid) => {
  console.log(`New user connected!${uuid} with socket ID ${socket.id}`);
  console.log(`Current users:${getUser()}`);
  createStage(uuid);
  socket.emit('connection', { uuid });
};

export const handlerEvent = (io, socket, data) => {
  if (!CLIENT_VERSION.includes(data.clientVersion)) {
    socket.emit('response', { status: 'fail', message: 'Client version Not Found' });
    return;
  }

  const handler = handlerMappings[data.handlerId];

  if (!handler) {
    socket.emit('response', { status: 'fail', message: 'Handler not found' });
    return;
  }
  const response = handler(data.userId, data.payload);

  if (response.broadcast) {
    io.emit('response', 'broadcast');
    return;
  }

  socket.emit('response', response);
};

handlerEvent 함수는 클라이언트의 요청에 따라 적절한 핸들러(gameStart,gameEnd,moveStageHandler)를 호출한다.

각 핸들러는 요청에 포함된 handlerId에 따라 다르게 동작한다.

핸들러가 성공적으로 처리되면 클라이언트에 응답을 전송한다.

 

  • src/handlers/game.handler.js
    게임 로직 처리
import { getGameAssets } from '../init/assets.js';
import { clearStage, getStage, setStages } from '../models/stage.model.js';

export const gameStart = (uuid, payload) => {
  const { stages } = getGameAssets();

  clearStage(uuid);
  setStages(uuid, stages.data[0].id, payload.timeStamp);

  console.log('stage:', getStage(uuid));

  return { status: 'success' };
};
export const gameEnd = (uuid, payload) => {
  //클라이언트는 게임 종료 시 타임스탬프와 총 점수를 줌
  const { timeStamp: gemaEndTime, score } = payload;
  const stages = getStage(uuid);

  if (!stages.length) {
    return { status: 'fail', message: 'No stages found for user' };
  }

  //각 스테이지의 지속 시간을 계산하여 촘 점수 계산
  let totalScore = 0;
  stages.forEach((stage, index) => {
    let stageEndTime;
    if (index === stages.length - 1) {
      stageEndTime = gemaEndTime;
    } else {
      stageEndTime = stages[index + 1].timeStamp;
    }

    const stageDuration = (stageEndTime - stage.timeStamp) / 1000;
    totalScore += stageDuration; // 1초당 1점
  });

  //점수와 타임스탬프를 검증
  //오차범위 5
  if (Math.abs(score - totalScore) > 5) {
    return { status: 'fail', message: 'Score verification error' };
  }

  return { status: 'success', message: 'Game End', score };
};

gameStart:게임 시작시 호출되며 게임 자산을 가져와 초기 스테이지를 설정한다.

gameEnd:게임 종료시 호출되어 각 스테이지의 점수를 계산하고 검증한다. 점수가 일치하지 않는다면 오류 메시지를 반환한다.

 

  • src/handlers/stage.handler.js
    src/models/stage.model.js
    스테이지 관리

src/handlers/stage.handler.js 코드

//유저는 스테이지를 하나씩 올라갈 수 있다. (1스테 ->2, 2->3)

import { getGameAssets } from '../init/assets.js';
import { getStage, setStages } from '../models/stage.model.js';

//유저는 일정 점수 도달시 다음 스테이지로 이동
export const moveStageHandler = (userId, payload) => {
  //유저의 현재 스테이지 정보
  let currentStages = getStage(userId);
  if (!currentStages.length) {
    return { status: 'fail', message: 'No stages found for user' };
  }
  //오름차순 -> 가장 큰 스테이지 ID를 확인 <- 유저의 현재 스테이지

  currentStages.sort((a, b) => a.id - b.id);
  const currentStage = currentStages[currentStages.length - 1];

  //클라이언트 vs 서버 비교
  if (currentStage.id !== payload.currentStages) {
    console.log('asdasd:', currentStage);
    return { status: 'fail', message: 'Current Stage Mismatch' };
  }

  //점수 검증
  const serverTime = Date.now(); //현재 타임 스탬프
  const elapsedTime = (serverTime - currentStages.timeStamp) / 1000;
  //1스테이지 -> 2스테이지
  //여기서 105 는 임의로 정한 오차범위임
  if (elapsedTime < 100 || elapsedTime > 105) {
    return { stages: 'fail', message: 'Invalid elapsed time' };
  }

  //타겟 스테이지 검즘 <- 게임에셋에 존재하는가?
  const { stages } = getGameAssets();
  if (!stages.data.some((stage) => stage.id === payload.targetStage)) {
    return { status: 'fail', message: 'Target stage not found' };
  }

  setStages(userId, payload.targetStage, serverTime);

  return { stage: 'success' };
};

사용자가 스테이지를 이동할 경우 호출된다. 현재 스테이지 정보를 확인하고 클라이언트에서 보낸 데이터와 비교하여

유효성을 검증한다. 성공적으로 이동하면 새로운 스테이지를 설정한다.

 

src/models/stage.model.js 코드

//key : uuid ,value : array -> 스테이지 정보는 배열
const stages = {};

//스테이지 초기화
export const createStage = (uuid) => {
  stages[uuid] = [];
};

export const getStage = (uuid) => {
  return stages[uuid];
};

export const setStages = (uuid, id, timeStamp) => {
  return stages[uuid].push({ id, timeStamp });
};

export const clearStage = (uuid) => {
  stages[uuid] = [];
};

스테이지 정보를 저장하는 객체를 관리한다 스테이지를 생성,가져오기,설정,초기화 하는 함수가 포함되어 있다.

 

  • src/init/assets.js
    자산 로딩

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const basePath = path.join(__dirname, '../../assets');

//파일 읽는 함수
//비동기 병렬로 파일을 읽는다.
const readFileAsync = (filename) => {
  return new Promise((resolve, reject) => {
    fs.readFile(path.join(basePath, filename), 'utf8', (err, data) => {
      if (err) {
        reject(err);
        return;
      }
      resolve(JSON.parse(data));
    });
  });
};

//Promise.all()
export const loadGameAssets = async () => {
  try {
    const [stages, items, itemsUnlock] = await Promise.all([
      readFileAsync('stage.json'),
      readFileAsync('item.json'),
      readFileAsync('item_unlock.json'),
    ]);

    gameAssets = { stages, items, itemsUnlock };
    return gameAssets;
  } catch (err) {
    console.log(err);
    throw new Error('서버 에러:' + err.message);
  }
};

let gameAssets = {};

export const getGameAssets = () => {
  return gameAssets;
};

게임자산(스테이지,아이템 등)을 비동기로 로드하는함수가 포함되어있다.

loadGameAssets 함수가 호출되어 JSON 파일로부터 게임 자산을 읽는다. 이 자산은 다른 핸들러에서 참조됨

 

  • src/models/user.model.js
    사용자 관리

const users = [];

export const addUser = (user) => {
  users.push(user);
};

export const removeUser = (socketId) => {
  const index = users.findIndex((user) => user.socketId === socketId);
  if (index !== -1) {
    return users.splice(index, 1)[0];
  }
};

export const getUser = () => {
  return users;
};

사용자 정보를 관리하는 모듈이다. 사용자를 추가,제거하고 현재 사용자 목록을 가져오는 기능이 포함됨 

'Node' 카테고리의 다른 글

socket 이벤트 / 버퍼(Buffer)  (0) 2024.10.23
Socket.io 기본 기능  (0) 2024.09.30
HTTP의 특징,webSocket의 특징  (2) 2024.09.26
객체 지향  (1) 2024.09.25
Mongoose Schema  (0) 2024.09.06