프로젝트 Final

26일 일지

rabbit97 2024. 11. 26. 23:08

# 개발 진행 상황

리액션 핸들러 로직은 이제 마무리

지금은 올 유저의 스테이트를 논값으로 바꿔서 게임을 진행시키는데

 

카드쪽 진행하고 있는 팀원 분 중 한분이 공격및 방어 카드쪽으로 유틸 함수 작성중이여서

 

나중에 그 로직을 가져오면 진짜 마무리

import { getGameSessionBySocket, getGameSessionByUser } from '../../sessions/game.session.js';
import { createResponse } from '../../utils/packet/response/createResponse.js';
import { CARD_TYPE, PACKET_TYPE } from '../../constants/header.js';
import handleError from '../../utils/errors/errorHandler.js';
import userUpdateNotification from '../../utils/notification/userUpdateNotification.js';
import { getUserBySocket } from '../../sessions/user.session.js';
import handleAnimationNotification from '../../utils/notification/animation.notification.js';

const packetType = PACKET_TYPE;
const cardType = CARD_TYPE;

const REACTION_TYPE = {
  NONE_REACTION: 0,
  NOT_USE_CARD: 1,
};

const handleReactionRequest = async ({ socket, payload }) => {
  try {
    console.log('handleReactionRequest - Received payload:', payload);

    if (!payload || typeof payload !== 'object') {
      throw new Error('Payload가 올바르지 않습니다.');
    }

    const { reactionType } = payload;
    console.log('handleReactionRequest - reactionType:', reactionType);

    if (!Object.values(REACTION_TYPE).includes(reactionType)) {
      throw new Error('유효하지 않은 리액션 타입입니다.');
    }

    const gameSession = await getGameSessionBySocket(socket);
    if (!gameSession) {
      throw new Error('해당 유저의 게임 세션이 존재하지 않습니다.');
    }
    console.log('handleReactionRequest - gameSession found');

    const user = getUserBySocket(socket);
    const room = getGameSessionByUser(user);

    if (!room.users || !room.users[user.id]) {
      throw new Error(`User with id ${user.id} not found in room users.`);
    }

    if (reactionType === REACTION_TYPE.NONE_REACTION) {
      console.log(`Immediate damage processing for user ${user.id}`);

      const character = room.users[user.id]?.character;

      if (character && character.hp > 0) {
        // 방어 장비 AUTO_SHIELD가 장착되어 있는지 확인
        const hasAutoShield = character.equips.includes(CARD_TYPE.AUTO_SHIELD);

        if (hasAutoShield) {
          console.log('AUTO_SHIELD equipped, calculating defense chance...');
          const defenseChance = Math.random();
          if (defenseChance <= 1) {
            await handleAnimationNotification({
              socket,
              payload: {
                userId: user.id,
                animationType: 3,
              },
            });
            room.resetStateInfoAllUsers();
            userUpdateNotification(room);

            return;
          }
        }

        // 방어 실패 또는 AUTO_SHIELD 미장착 - 체력 감소
        character.hp -= 1;
        console.log(`Damage applied. New HP for user ${user.id}: ${character.hp}`);
      } else {
        console.error(`User with id ${user.id} not found in room users or already dead.`);
      }

      // 상태 초기화 및 업데이트 알림
      room.resetStateInfoAllUsers();
      userUpdateNotification(room);
    }
    // 리액션 처리 완료 후 응답 전송
    const reactionResponseData = {
      success: true,
      failCode: 0,
    };
    const reactionResponse = createResponse(
      packetType.REACTION_RESPONSE,
      socket.sequence,
      reactionResponseData,
    );
    console.log('handleReactionRequest - Sending response:', reactionResponse);

    if (typeof socket.write === 'function') {
      socket.write(reactionResponse);
    } else {
      throw new Error('socket.write is not a function');
    }
  } catch (error) {
    console.error('리액션 처리 중 에러 발생:', error.message);

    const errorResponse = createResponse(packetType.REACTION_RESPONSE, socket.sequence, {
      success: false,
      failCode: 1,
      message: error.message || 'Reaction failed',
    });

    if (typeof socket.write === 'function') {
      socket.write(errorResponse);
    } else {
      console.error('socket.write is not a function:', socket);
    }

    handleError(socket, error);
  }
};

export default handleReactionRequest;

 

일단 더 추가된 점은

 

자동 실드는 해당 장비를 장착한 유저는 데미지를 입을때 25퍼센트 확률로 데미지를 안 입게 하는 장비

 

맞았을때 데미지가 들어오면 클라이언트에서는 리액션 요청을 보내기때문에 리액션 로직에서 로직을 구현

 

아쉬운 점은 실드가 있을 경우에는 자동 실드가 먼저냐 사용 가능한 실드 소모가 먼저냐 인데

장착 장비라 자동 실드로 먼저 확률을 계산하고 사용 가능한 실드를 사용할지 말지 결정하면 참 좋은데

 

클라이언트에서 실드가 사용 가능하면 패킷을 유저에게 선택권을 주고 경우에따라 다르게 보내서

 

해당 검증 작업은 리액션 요청을 받았을 때 - (실드가 없거나 실드가 있지만 사용 안한 상태) 에서 확률을 진행해야한다.

해당 장비를 장착한 유저는 피해를 받았을 때 25퍼센트 확률로 체력 감소 없이 유저 업데이트를 하는 방향으로 진행이 되었다.

그런데 자동 실드가 발동 되면 보내야하는 패킷이 있는데

message S2CAnimationNotification {

string userId = 1;

AnimationType animationType = 2;

}


enum AnimationType {

NO_ANIMATION = 0;

SATELLITE_TARGET_ANIMATION = 1;

BOMB_ANIMATION = 2;

SHIELD_ANIMATION = 3;

}

 

 

 

여기서 생긴 문제는 3번을 보내면 방어 이펙트를 클라이언트에서 출력을 해주긴 하는데 이펙트가 출력 되어있는동안 공격자와 공격을 맞은 유저 전부 무한 로딩에 걸린다.

일단 찾아본 방법

 

이펙트를 3번이 아닌 다른 타입으로 보내면 다른 이펙트들은 계속 켜져 있는게 아니라 1~2초 잠깐 보여주고 사라지는데 사라질때 무한로딩이 풀린 것으로 보아 이펙트가 켜져있을때 무한 로딩이 걸린 것을 확인

 

이펙트를 지우기 위해 여러 방법을 시도해봤는데

 

 

1. 셋타임아웃을 걸어서 3초 뒤 0번 패킷을 다시 보내 이펙트를 논으로 돌린다.

 

이 방법은 통하지 않았다. 왜냐하면 논값을 받아도 쉴드 이펙트가 사라지지 않기 때문

 

 

2. 애니메이션 이벤트를 따로 정의해서 스케줄링하는 방법

사실 위에 방법이랑 똑같은 방법이나 단순히 두번 보내는게 아닌 이벤트의 시작과 종료를 보내는거기 때문에 혹시 하는 마음으로 시도 했던 방법

const handleAnimationNotification = async ({ socket, payload }) => {
  try {
    if (!payload || typeof payload !== 'object') {
      throw new Error('Payload가 올바르지 않습니다.');
    }

    const { userId, animationType, duration = 3000 } = payload;

    if (typeof userId === 'undefined' || typeof animationType === 'undefined') {
      throw new Error('페이로드에 userId 또는 animationType 값이 없습니다.');
    }

    const gameSession = getGameSessionBySocket(socket);
    if (!gameSession) {
      throw new Error('해당 유저의 게임 세션이 존재하지 않습니다.');
    }

    const currentUser = getUserBySocket(socket);
    if (!currentUser) {
      throw new Error('유저가 존재하지 않습니다.');
    }

    // 애니메이션 시작 노티피케이션 전송
    sendAnimationNotification(gameSession, userId, animationType, 'startAnimation');

    // 'duration' 후에 애니메이션을 종료하는 노티피케이션 전송
    setTimeout(() => {
      sendAnimationNotification(gameSession, userId, animationType, 'endAnimation');
    }, duration);

  } catch (error) {
    handleError(socket, error);
  }
};

const sendAnimationNotification = (gameSession, userId, animationType, command) => {
  const animationResponseData = {
    command,        // 'startAnimation' or 'endAnimation'
    userId,
    animationType,
  };

  console.log(`${command} Notification Data:`, animationResponseData);

  const animationResponse = createResponse(
    packetType.ANIMATION_NOTIFICATION,
    gameSession.sequence++,   // Assuming sequence management
    animationResponseData,
  );

  const allUser = gameSession.getAllUsers();
  allUser.forEach((notiUser) => {
    notiUser.socket.write(animationResponse);
  });
};

export default handleAnimationNotification;

 

 

이 방법도 통하진 않았다.

 

 

3. 상태를 실시간으로 동기화 해보자

const handleAnimationNotification = async ({ socket, payload }) => {
  try {
    if (!payload || typeof payload !== 'object') {
      throw new Error('Payload가 올바르지 않습니다.');
    }

    const { userId, animationType } = payload;

    if (typeof userId === 'undefined' || typeof animationType === 'undefined') {
      throw new Error('페이로드에 userId 또는 animationType 값이 없습니다.');
    }

    const gameSession = getGameSessionBySocket(socket);
    if (!gameSession) {
      throw new Error('해당 유저의 게임 세션이 존재하지 않습니다.');
    }

    const currentUser = getUserBySocket(socket);
    if (!currentUser) {
      throw new Error('유저가 존재하지 않습니다.');
    }

    // 애니메이션 상태 저장
    gameSession.animationStates = gameSession.animationStates || {};
    gameSession.animationStates[userId] = { animationType, timestamp: Date.now() };

    // 클라이언트에 상태 전송
    sendAnimationStatus(gameSession);

    // 주기적인 상태 동기화
    if (!gameSession.syncInterval) {
      gameSession.syncInterval = setInterval(() => {
        sendAnimationStatus(gameSession);
      }, 1000); // 1초마다 상태 동기화
    }

  } catch (error) {
    handleError(socket, error);
  }
};

const sendAnimationStatus = (gameSession) => {
  const animationStates = gameSession.animationStates;

  const response = createResponse(
    packetType.ANIMATION_STATUS,
    gameSession.sequence++,
    { animationStates },
  );

  const allUser = gameSession.getAllUsers();
  allUser.forEach((notiUser) => {
    notiUser.socket.write(response);
  });
};

export default handleAnimationNotification;

 

사실 이 방법도 위에랑 똑같이 3번 타입의 패킷을 보내고 나서 0번 타입을 보내면 이펙트가 꺼지지 않는 이유라서 실패한 방법

 

 

4. 큐로 관리

import { getGameSessionBySocket } from '../../sessions/game.session.js';
import { createResponse } from '../packet/response/createResponse.js';
import { getUserBySocket } from '../../sessions/user.session.js';
import handleError from '../../utils/errors/errorHandler.js';
import { PACKET_TYPE } from '../../constants/header.js';

const packetType = PACKET_TYPE;

const AnimationManager = (() => {
  // 애니메이션 상태를 저장하기 위한 객체
  const animationStates = {};

  const startInterval = () => {
    setInterval(() => {
      const now = Date.now();
      for (const userId in animationStates) {
        if (animationStates[userId] && animationStates[userId].endTime <= now) {
          // 애니메이션 종료
          const { gameSession, animationType } = animationStates[userId];
          sendAnimationNotification(gameSession, userId, 0, 'endAnimation');
          delete animationStates[userId];
        }
      }
    }, 1000); // 1초마다 상태를 체크
  };

  const queueAnimation = (gameSession, userId, animationType, duration) => {
    const endTime = Date.now() + duration;
    animationStates[userId] = { gameSession, animationType, endTime };

    // 애니메이션 시작
    sendAnimationNotification(gameSession, userId, animationType, 'startAnimation');
  };

  return {
    startInterval,
    queueAnimation,
  };
})();

const handleAnimationNotification = async ({ socket, payload }) => {
  try {
    if (!payload || typeof payload !== 'object') {
      throw new Error('Payload가 올바르지 않습니다.');
    }

    const { userId, animationType, duration = 3000 } = payload;

    if (typeof userId === 'undefined' || typeof animationType === 'undefined') {
      throw new Error('페이로드에 userId 또는 animationType 값이 없습니다.');
    }

    const gameSession = getGameSessionBySocket(socket);

    if (!gameSession) {
      throw new Error('해당 유저의 게임 세션이 존재하지 않습니다.');
    }

    const currentUser = getUserBySocket(socket);

    if (!currentUser) {
      throw new Error('유저가 존재하지 않습니다.');
    }

    // 애니메이션 큐에 추가
    AnimationManager.queueAnimation(gameSession, userId, animationType, duration);
    
  } catch (error) {
    handleError(socket, error);
  }
};

const sendAnimationNotification = (gameSession, userId, animationType, command) => {
  const animationResponseData = {
    command,         // 'startAnimation' or 'endAnimation'
    userId,
    animationType,
  };

  console.log(`${command} Notification Data:`, animationResponseData);

  const animationResponse = createResponse(
    packetType.ANIMATION_NOTIFICATION,
    gameSession.sequence++,   // Assuming sequence management
    animationResponseData,
  );

  const allUser = gameSession.getAllUsers();
  allUser.forEach((notiUser) => {
    notiUser.socket.write(animationResponse);
  });
};

// 서버 시작 시 애니메이션 매니저의 인터벌 시작
AnimationManager.startInterval();

export default handleAnimationNotification;

 

이런 방식으로 호출해서 사용을 했는데

import { getGameSessionBySocket, getGameSessionByUser } from '../../sessions/game.session.js';
import { createResponse } from '../../utils/packet/response/createResponse.js';
import { CARD_TYPE, PACKET_TYPE } from '../../constants/header.js';
import handleError from '../../utils/errors/errorHandler.js';
import userUpdateNotification from '../../utils/notification/userUpdateNotification.js';
import { getUserBySocket } from '../../sessions/user.session.js';
import { handleAnimationNotification, handleAnimationConfirmation } from '../../utils/notification/animation.notification.js';

const packetType = PACKET_TYPE;
const cardType = CARD_TYPE;

const REACTION_TYPE = {
  NONE_REACTION: 0,
  NOT_USE_CARD: 1,
};

const handleReactionRequest = async ({ socket, payload }) => {
  try {
    console.log('handleReactionRequest - Received payload:', payload);

    if (!payload || typeof payload !== 'object') {
      throw new Error('Payload가 올바르지 않습니다.');
    }

    const { reactionType } = payload;
    console.log('handleReactionRequest - reactionType:', reactionType);

    if (!Object.values(REACTION_TYPE).includes(reactionType)) {
      throw new Error('유효하지 않은 리액션 타입입니다.');
    }

    const gameSession = await getGameSessionBySocket(socket);
    if (!gameSession) {
      throw new Error('해당 유저의 게임 세션이 존재하지 않습니다.');
    }
    console.log('handleReactionRequest - gameSession found');

    const user = getUserBySocket(socket);
    const room = getGameSessionByUser(user);

    if (!room.users || !room.users[user.id]) {
      throw new Error(`User with id ${user.id} not found in room users.`);
    }

    if (reactionType === REACTION_TYPE.NONE_REACTION) {
      console.log(`Immediate damage processing for user ${user.id}`);

      const character = room.users[user.id]?.character;

      if (character && character.hp > 0) {
        const hasAutoShield = character.equips.includes(CARD_TYPE.AUTO_SHIELD);

        if (hasAutoShield) {
          console.log('AUTO_SHIELD equipped, calculating defense chance...');
          const defenseChance = Math.random();
          if (defenseChance <= 1) {
            await handleAnimationNotification({
              socket,
              payload: {
                userId: user.id,
                animationType: 3,
                duration: 3000, // 애니메이션 지속 시간 설정
              },
            });
            room.resetStateInfoAllUsers();
            userUpdateNotification(room);
            return;
          }
        }
        character.hp -= 1;
        console.log(`Damage applied. New HP for user ${user.id}: ${character.hp}`);
      } else {
        console.error(`User with id ${user.id} not found in room users or already dead.`);
      }

      room.resetStateInfoAllUsers();
      userUpdateNotification(room);
    }

    const reactionResponseData = {
      success: true,
      failCode: 0,
    };
    const reactionResponse = createResponse(
      packetType.REACTION_RESPONSE,
      socket.sequence,
      reactionResponseData,
    );
    console.log('handleReactionRequest - Sending response:', reactionResponse);

    if (typeof socket.write === 'function') {
      socket.write(reactionResponse);
    } else {
      throw new Error('socket.write is not a function');
    }
  } catch (error) {
    console.error('리액션 처리 중 에러 발생:', error.message);

    const errorResponse = createResponse(packetType.REACTION_RESPONSE, socket.sequence, {
      success: false,
      failCode: 1,
      message: error.message || 'Reaction failed',
    });

    if (typeof socket.write === 'function') {
      socket.write(errorResponse);
    } else {
      console.error('socket.write is not a function:', socket);
    }

    handleError(socket, error);
  }
};

export default handleReactionRequest;

 

이 방법이 특정 상황에서만 잘 되고 어떤 상황에서는 안되고 해서

확실하게 어떤 상황에서는 된다! 하면 수정을 하면 되는데 랜덤으로 어떤 상황에서는 되고 또 갑자기 안되고 이래서

 

로그를 보니 시퀀스랑 관련해서 문제가 있었어가지고

class SequenceManager {
  constructor() {
    this.sequences = new Map();
  }

  getSequence(socket) {
    if (!this.sequences.has(socket)) {
      this.sequences.set(socket, 0);
    }

    const currentSequence = this.sequences.get(socket);
    this.sequences.set(socket, currentSequence + 1);
    return currentSequence;
  }

  resetSequence(socket) {
    this.sequences.set(socket, 0);
  }
}

const sequenceManager = new SequenceManager();

export default sequenceManager;

 

시퀀스 매니저도 한번 선언하고

 

import { getGameSessionBySocket } from '../../sessions/game.session.js';
import { createResponse } from '../packet/response/createResponse.js';
import { getUserBySocket } from '../../sessions/user.session.js';
import handleError from '../../utils/errors/errorHandler.js';
import { PACKET_TYPE } from '../../constants/header.js';
import sequenceManager from '../../utils/sequenceManager.js';

const packetType = PACKET_TYPE;

const AnimationManager = (() => {
  const animationStates = {};
  const confirmationStates = {};

  const startInterval = () => {
    setInterval(() => {
      const now = Date.now();
      for (const userId in animationStates) {
        if (animationStates[userId] && animationStates[userId].endTime <= now) {
          const { socket, animationType } = animationStates[userId];
          if (!confirmationStates[userId]) {
            sendAnimationNotificationWithConfirmation(socket, userId, 0, 'endAnimation')
              .catch((error) => handleError(socket, error));
            confirmationStates[userId] = now;
          }
        }
      }
      for (const userId in confirmationStates) {
        if (now - confirmationStates[userId] >= 5000) {
          const { socket } = animationStates[userId];
          sendAnimationNotificationWithConfirmation(socket, userId, 0, 'endAnimation')
            .catch((error) => handleError(socket, error));
          confirmationStates[userId] = now;
        }
      }
    }, 1000);
  };

  const queueAnimation = (socket, userId, animationType, duration) => {
    const endTime = Date.now() + duration;
    animationStates[userId] = { socket, animationType, endTime };

    sendAnimationNotification(socket, userId, animationType, 'startAnimation')
      .catch((error) => handleError(socket, error));
  };

  const confirmAnimationEnd = (userId) => {
    delete animationStates[userId];
    delete confirmationStates[userId];
  };

  return {
    startInterval,
    queueAnimation,
    confirmAnimationEnd,
  };
})();

const handleAnimationNotification = async ({ socket, payload }) => {
  try {
    if (!payload || typeof payload !== 'object') {
      throw new Error('Payload가 올바르지 않습니다.');
    }

    const { userId, animationType, duration = 3000 } = payload;

    if (typeof userId === 'undefined' || typeof animationType === 'undefined') {
      throw new Error('페이로드에 userId 또는 animationType 값이 없습니다.');
    }

    const gameSession = getGameSessionBySocket(socket);

    if (!gameSession) {
      throw new Error('해당 유저의 게임 세션이 존재하지 않습니다.');
    }

    const currentUser = getUserBySocket(socket);

    if (!currentUser) {
      throw new Error('유저가 존재하지 않습니다.');
    }

    AnimationManager.queueAnimation(socket, userId, animationType, duration);

  } catch (error) {
    handleError(socket, error);
  }
};

const sendAnimationNotification = async (socket, userId, animationType, command) => {
  const animationResponseData = {
    command,
    userId,
    animationType,
  };

  console.log(`${command} Notification Data:`, animationResponseData);

  const sequence = sequenceManager.getSequence(socket);
  const animationResponse = createResponse(
    PACKET_TYPE.ANIMATION_NOTIFICATION,
    sequence,
    animationResponseData,
  );

  await socket.write(animationResponse);
};

const sendAnimationNotificationWithConfirmation = async (socket, userId, animationType, command) => {
  await sendAnimationNotification(socket, userId, animationType, command);

  const confirmationResponseData = {
    command: 'checkAnimationEnd',
    userId,
    animationType,
  };

  console.log(`Confirmation Notification Data:`, confirmationResponseData);

  const sequence = sequenceManager.getSequence(socket);
  const confirmationResponse = createResponse(
    PACKET_TYPE.ANIMATION_NOTIFICATION,
    sequence,
    confirmationResponseData,
  );

  await socket.write(confirmationResponse);
};

const handleAnimationConfirmation = async ({ socket, payload }) => {
  try {
    if (!payload || typeof payload !== 'object') {
      throw new Error('Payload가 올바르지 않습니다.');
    }

    const { userId } = payload;

    if (typeof userId === 'undefined') {
      throw new Error('페이로드에 userId 값이 없습니다.');
    }

    AnimationManager.confirmAnimationEnd(userId);

  } catch (error) {
    handleError(socket, error);
  }
};

AnimationManager.startInterval();

export { handleAnimationNotification, handleAnimationConfirmation };

 

이런 식으로 사용을 했다

 

이 방법이 제일 무한 로딩이 생기지 않던 방법이였는데 이 방법도 진짜 가끔 이유 모를 특정 상황에서 무한로딩이 걸려

결국에 사용한 방법은

 

그냥 클라이언트 코드를 수정 했다 (내 12시간 ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠ)

 

시퀀스 매니저랑 애니메이션을 큐에 담아서 관리했던 작업 모두 날리고 초반으로 롤백해서 사용 중

 

하지만 이렇게 진행을 하면 문제는 공격자랑 공격을 받은 유저 둘 다 이펙트가 잠깐 켜져있는 1초 움직일 수 없는 상황이여가지고

이 로직은 일단 여기까지 진행을 하고 남은 급한 로직을 먼저 처리하는걸로 결정

 

데브 브렌치에도 지금 롤백한 초기 로직이 올라가있어 팀원들 모두 지금 코드일텐데

 

혹시나 나중에 서버에서 관리를 할 수 있는 방법이 발견되면 그걸로 수정하면 되고

안된다면 그때 팀원들이 클라이언트를 수정해도 늦지는 않기에

 

내일은 남은 디버프 로직을 진행