2024-11-21 CH-6 최종 프로젝트 5 Prototype Build Ver.1 서버로직 마무리

MOON·2024년 11월 24일
0

내일배움캠프 과제

목록 보기
38/48

오늘은 어떤 패킷명세를 기준으로 서버로직을 구축했는지에 대해 작성해볼려고 합니다.

1.클라이언트가 TCP서버 연결이 되었을때

S2C_ConnectResponse

{
  unit32 userId = 1;
  string token =2;
}

현재는 임시로 저희가 userId를 주면서 시작하였습니다. 추후 로그인 기능이 생기면 클라측에서 주는 방향으로 바뀔 것 같습니다.

export const onConnection = (socket) => {
...
...

  socket.buffer = Buffer.alloc(0);

  // 현재는 테스트용도 연결된 클라이언트에게 토큰 넘겨주고 나중엔 로그인 시에 jwt토큰을 생성하여 클라에게 넘겨주고 서버 및 세션에 참가할떄 인증검증으로 사용할 예정
  const testToken = config.test.test_token;
  const data = serializer(
    PACKET_TYPE.ConnectResponse,
    { userId: getUUID(), token: testToken },
    0,
  ); // sequence는 임시로 0
  socket.write(data);
...
...
};

그리고 일시적으로 jwt토큰을 이용하기 보단 현재는 임시로 test토큰을 넘겨주면서 확인용으로 로직을 작성해 보았습니다.

2. 게임 시작 요청_(현재는 게임 자체내의 로비로 진행하는 것을 의미합니다. 리썰컴퍼니처럼..)

C2S_ConnectGameRequest - 게임 시작 요청

{
  uint32 userId = 1;
  string token = 2;
}

S2C_ConnectGameResponse - 게임 시작에 대한 응답

{
  uint32 gameId = 1;
  uint32 hostId = 2;
  repeated uint32 existUserIds = 3;
  repeated uint32 ghostTypeIds = 4;
  GlobalFailCode globalFailCode = 5;
  UserState userState = 6; 
  GameSessionState gameSessionState = 7;
  string message = 8;
}

해당 응답패킷이 왔을때를 대비한 connectGameRequestHandler 핸들러 함수입니다.

export const connectGameRequestHandler = ({ socket, payload }) => {
  try {
    const { userId, token } = payload;

    if (config.test.test_token !== token) {
      invalidTokenResponse(socket);
      throw new CustomError(ErrorCodesMaps.AUTHENTICATION_ERROR);
    }

    // 유저 생성 및 인메모리 세션 저장
    const user = addUser(userId, socket);

    socket.userId = user.id;

    // 게임 세션 참가 로직 (임시 로직)
    // 현재 init/index.js에서 게임 세션 하나를 임시로 생성해 두었습니다.
    const gameSession = getGameSession();

    // 본인 제외 이미 게임에 존재하는 유저들
    const existUserIds = gameSession.users.map((user) => user.id);
    gameSession.addUser(user);

    sendConnectGameResponse(socket, gameSession, existUserIds);
  } catch (e) {
    handleError(e);
  }
};

invalidTokenResponse, sendConnectGameResponse 패킷의 내용을 담을 함수들입니다. 함수를 따로 관리하여 토큰이 일치하지 않을 떄는 invalidTokenResponse, 아무 문제가 없을떄는 sendConnectGameResponse 이 함수를 실행하여 패킷을 보내주게 하였습니다.

/**
 * 토큰이 유효하지 않을때 실패 응답 보내주는 함수입니다.
 * @param {*} socket
 */
export const invalidTokenResponse = (socket) => {
  const data = {
    gameId: null,
    hostId: null,
    existUserIds: null,
    ghostTypeIds: null, // 임시 고스트 타입
    gloalFailCode: GLOBAL_FAIL_CODE.INVALID_REQUEST,
    userState: USER_STATE.STAY,
    gameSessionState: GAME_SESSION_STATE.PREPARE,
    message: '해당 토큰이 일치하지 않아 게임을 입장할 수 없습니다.',
  };
  const responseData = serializer(PACKET_TYPE.ConnectGameResponse, data, 0); // sequence도 임시로
  socket.write(responseData);
};

/**
 * 호스트에게 게임 초기 정보를 응답으로 보내주는 함수입니다.
 * @param {*} socket
 * @param {*} gameSession
 */
export const sendConnectGameResponse = (socket, gameSession, existUserIds) => {
  const data = {
    gameId: gameSession.id,
    hostId: gameSession.hostId,
    existUserIds: existUserIds,
    ghostTypeIds: [1, 2, 3, 4, 5], // 임시 고스트 타입 5마리 소환하라고 보냅니다.
    gloalFailCode: GLOBAL_FAIL_CODE.NONE,
    userState: USER_STATE.INGAME,
    gameSessionState: gameSession.state,
    message: '게임 세션 입장에 성공하였습니다.',
  };
  const responseData = serializer(PACKET_TYPE.ConnectGameResponse, data, 0); // sequence도 임시로
  socket.write(responseData);
};

3. 중간에 참여한 플레이어의 정보를 보내주는 패킷입니다.

S2C_ConnectNewPlayerNotification

{
  uint32 userId = 1;
}

connectNewPlayerNotification 함수 입니다.

export const connectNewPlayerNotification = async (gameSession, newUser) => {
  const userId = newUser.id;
  const responseData = serializer(
    PACKET_TYPE.ConnectNewPlayerNotification,
    { userId },
    0,
  );
  gameSession.users.forEach((user) => {
    user.socket.write(responseData);
  });
};

4. 실제로 게임을 진행할떄 보내게되는 패킷들입니다.

해당 클라이언트(Host)는 고스트 타입에 맞게 고스트를 생성했다는 패킷을 줍니다.
C2S_SpawnInitialGhostRequest

{
  repeated Ghost ghosts = 1;
}

spawnInitialGhostRequestHandler를 통해 고스트의 생성된 정보를 서버에서 세션에 저장하고 관리합니다.

export const spawnInitialGhostRequestHandler = ({ socket, payload }) => {
  try {
    const { ghosts } = payload;

    const gameSession = getGameSession();
    if (!gameSession) {
      throw new CustomError(ErrorCodesMaps.GAME_NOT_FOUND);
    }

    ghosts.forEach((ghostInfo) => {
      const ghost = new Ghost(
        ghostInfo.ghostId,
        ghostInfo.ghostTypeId,
        ghostInfo.moveInfo.position,
        ghostInfo.moveInfo.rotation,
        ghostInfo.moveInfo.state,
      );

      gameSession.addGhost(ghost);
    });

    gameSession.startGame();
  } catch (e) {
    handleError(e);
  }
};

지금 보니깐 순서를 한번 정리하면 좋을 것 같네요..

5. 실제 게임을 실행하라고 통지하는 패킷입니다.

S2C_StartGameNotification

{
  uint32 mapId = 1;
  GameSessionState gameSessionState = 2; // PREPARE, INPRGRESS, END 
}
/**
 * 게임 시작을 알리는 함수
 */
export const startGameNotification = (gameSession) => {
  const payload = {
    mapId: 1,
    gameSessionState: gameSession.state,
  };

  const packet = serializer(PACKET_TYPE.StartGameNotification, payload, 0);

  gameSession.users.forEach((user) => {
    user.socket.write(packet);
  });
};

6. 움직임 동기화

  • 플레이어 이동 동기화

C2S_PlayerMoveRequest

{
  PlayerMoveInfo playerMoveInfo = 1;
}

S2C_PlayerMoveNotification

{
  repeated PlayerMoveInfo playerMoveInfos = 1;
}

해당 클라이언트에서 움직임 값을 받으면 서버에선 그값에 맞게 다른 클라이언트에게 움직임 값을 전달해 줍니다.

// 플레이어 이동 요청에 따른 핸들러 함수
export const movePlayerRequestHandler = ({ socket, payload }) => {
  try {
    const { playerMoveInfo } = payload;

    const { userId, position, rotation } = playerMoveInfo;

    // 현재 프로토빌드로 게임 첫번째 세션을 반환하도록 함.
    const gameSession = getGameSession();

    // 게임 세션에서 유저 찾기
    const user = gameSession.getUser(userId);
    if (!user) {
      console.error('user가 존재하지 않습니다.');
      return;
    }

    //이전 값 저장
    user.character.lastPosition.updateClassPosition(user.character.position);
    user.character.lastRotation.updateClassRotation(user.character.rotation);

    //수정 해야함
    user.character.position.updatePosition(position.x, position.y, position.z);
    user.character.rotation.updateRotation(rotation.x, rotation.y, rotation.z);

    //시간 저장
    user.character.lastUpdateTime = Date.now();
  } catch (e) {
    console.error(e.message);
  }
};
  • 귀신 이동 동기화

C2S_GhostMoveRequest

{
  repeated GhostMoveInfo ghostMoveInfos = 1;
}

S2C_GhostMoveNotification

{
  repeated GhostMoveInfo ghostMoveinfos = 1;
}

호스트가 요청을 보내면 호스트 기준의 고스트 위치값을 다른 클라이언트에게 전달해 줍니다. (호스트 기준)

// 호스트 유저만 요청을 보냅니다.
export const moveGhostRequestHandler = ({ socket, payload }) => {
  try {
    const { ghostMoveInfos } = payload;

    const gameSession = getGameSession();
    if (!gameSession) {
      throw new CustomError(ErrorCodesMaps.GAME_NOT_FOUND);
    }

    // 해당 게임 세션에 고스트들의 정보 저장
    ghostMoveInfos.forEach((ghostMoveInfo) => {
      const { ghostId, position, rotation } = ghostMoveInfo;

      const ghost = gameSession.getGhost(ghostId);
      if (!ghost) {
        console.error('해당 귀신 정보가 존재하지 않습니다.');
      }
      ghost.position.updatePosition(position.x, position.y, position.z);
      ghost.rotation.updateRotation(rotation.x, rotation.y, rotation.z);
    });
  } catch (e) {
    handleError(e);
  }
};

게임 세션이 시작될떄 인터벌로 noti를 날려서 다른 클라이언트들의 위치를 알려주게 해주었습니다.

class Game {
  constructor(id) {
    this.id = id;
    this.hostId = null;
    this.users = [];
    this.ghosts = [];
    this.state = GAME_SESSION_STATE.PREPARE;
  }

  startGame() {
    // 귀신 5마리 정도 세팅

    // 게임 상태 변경
    this.state = GAME_SESSION_STATE.INPROGRESS;

    IntervalManager.getInstance().addPlayersInterval(
      this.id,
      () => usersLocationNotification(this),
      1000 / 60,
    );

    IntervalManager.getInstance().addGhostsInterval(
      this.id,
      () => ghostsLocationNotification(this),
      100,
    );

    startGameNotification(this);
  }

...
...
...

오늘의 회고
이제 슬슬 Prototype Build Ver.2 또는 Mvp를 대비해 패킷명세추가 작업을 생각하면서 이어 나가야 될 것 같습니다.
하하.. 그리고 중간 테스트를 진행하면서 잠깐의 회의를 통해 패킷명세가 자주 바뀌면서 로직도 바뀌고 정신이 없네요.. 확실히 게임하나를 만드는 일은 오래걸리는 작업일 수 밖에 없겠구나라고 생각이 들었습니다. 으악

오늘도 화이팅

profile
안녕하세요

0개의 댓글

관련 채용 정보