개발자공부일기

DuckTopia 본문

트러블슈팅

DuckTopia

JavaCPP 2025. 2. 11. 20:15

이번 프로젝트에서 플레이어클래스와 플레이어의 이동 동기화를 담당했습니다.

 

플레이어 클래스는 우리가 흔히 아는 형태로

import {
  ATK_PER_LV,
  PLAYER_MAX_HUNGER,
  PLAYER_SPEED,
  PLAYER_DEFAULT_RANGE,
  PLAYER_DEFAULT_ANGLE,
} from '../../constants/player.js';

class Player {
  constructor(id, atk, x, y) {
    this.id = id;
    this.user = user; // User Class
    this.maxHp = PLAYER_MAX_HP;
    this.hp = PLAYER_MAX_HP;
    this.hunger = PLAYER_MAX_HUNGER;
    this.lv = 1;
    this.atk = atk;
    this.inventory = [];
    this.equippedWeapon = {};
    this.x = x;
    this.y = y;
    this.isAlive = true;
    this.characterType = CharacterType.RED;
    this.packetTerm = 0; //위치 변경 요청 패킷간의 시간차
    this.speed = PLAYER_SPEED;
    this.lastPosUpdateTime = Date.now();

    this.range = PLAYER_DEFAULT_RANGE;
    this.angle = PLAYER_DEFAULT_ANGLE;
  }

  changePlayerHp(damage) {
    this.hp -= damage;
    return this.hp;
  }

  getPlayerPos() {
    return { x: this.x, y: this.y };
  }

  changePlayerPos(x, y) {
    this.x = x;
    this.y = y;
    return { x: this.x, y: this.y };
  }

  // getPlayer() {
  //   return {
  //     characterType: this.characterType,
  //     hp: this.hp,
  //     weapon: this.weapon,
  //     atk: this.atk,
  //   };
  // }

  // getPlayerData() {
  //   return {
  //     playerId: this.id,
  //     nickname: this.name,
  //     character: this.getCharacter(),
  //   };
  // }

  //player 메서드 여기에 만들어놓고 나중에 옮기기

  playerPositionUpdate = (dx, dy) => {
    this.x = dx;
    this.y = dy;
  };

  calculatePosition = (otherPlayer, x, y) => {
    // 현재 위치와 요청받은 위치로 방향을 구하고 speed와 레이턴시를 곱해 이동거리를 구하고 좌표 예측 검증
    const seta = (Math.atan2(y - this.y, x - this.x) * 180) / Math.PI;
    const distance = this.speed * otherPlayer.packetTerm;

    // 만약 거속시로 구한 거리보다 멀면 서버가 알고있는 좌표로 강제 이동
    if (distance > VALID_DISTANCE) {
      console.error(`유효하지 않은 이동입니다.`);
    }

    const dx = Math.cos(seta) * distance;
    const dy = Math.sin(seta) * distance;

    //서버에 저장하는 좌표는 본인 기준으로 계산된 좌표
    if (this.id === otherPlayer.id) {
      this.playerPositionUpdate(dx, dy);
    }
    return { playerId: this.id, x: this.x, y: this.y };
  };

  calculateLatency = () => {
    //레이턴시 구하기 => 수정할 것)각 클라마다 다른 레이턴시를 가지고 계산
    //레이턴시 속성명도 생각해볼 필요가 있다
    this.packetTerm = Date.now() - this.lastPosUpdateTime; //player값 직접 바꾸는건 메서드로 만들어서 사용
    this.lastPosUpdateTime = Date.now();
  };

  changePlayerHunger(amount) {
    this.hunger += amount;
    return this.hunger;
  }
  //플레이어 어택은 데미지만 리턴하기
  getPlayerAtkDamage() {
    return this.atk + this.lv * ATK_PER_LV + this.equippedWeapon.atk;
  }

  playerDead() {
    this.isAlive = false;
    this.inventory = [];
    return this.isAlive;
  }

  findItemIndex(itemId) {
    const targetIndex = this.inventory.findIndex((item) => (item.id = itemId));
    return targetIndex;
  }

  addItem(item) {
    this.inventory.push(item);
    return this.inventory;
  }

  removeItem(itemId) {
    const targetIndex = this.findItemIndex(itemId);
    this.inventory.splice(targetIndex, 1);
    return this.inventory;
  }

  //공격 사거리 변경
  changeRange(range) {
    this.range = range;
  }

  //공격 각도 변경
  changeAngle(angle) {
    this.angle = angle;
  }

  getRange() {
    return this.range;
  }

  getAngle() {
    return this.angle;
  }

  getUser() {
    return this.user;
  }


}

export default Player;

아직 진행중이라 고쳐야 할게 많지만

 

가장 중점적으로 본게 

  calculatePosition = (otherPlayer, x, y) => {
    // 현재 위치와 요청받은 위치로 방향을 구하고 speed와 레이턴시를 곱해 이동거리를 구하고 좌표 예측 검증
    const seta = (Math.atan2(y - this.y, x - this.x) * 180) / Math.PI;
    const distance = this.speed * otherPlayer.packetTerm;

    // 만약 거속시로 구한 거리보다 멀면 서버가 알고있는 좌표로 강제 이동
    if (distance > VALID_DISTANCE) {
      console.error(`유효하지 않은 이동입니다.`);
    }

    const dx = Math.cos(seta) * distance;
    const dy = Math.sin(seta) * distance;

    //서버에 저장하는 좌표는 본인 기준으로 계산된 좌표
    if (this.id === otherPlayer.id) {
      this.playerPositionUpdate(dx, dy);
    }
    return { playerId: this.id, x: this.x, y: this.y };
  };

  calculateLatency = () => {
    //레이턴시 구하기 => 수정할 것)각 클라마다 다른 레이턴시를 가지고 계산
    //레이턴시 속성명도 생각해볼 필요가 있다
    this.packetTerm = Date.now() - this.lastPosUpdateTime; //player값 직접 바꾸는건 메서드로 만들어서 사용
    this.lastPosUpdateTime = Date.now();
  };

이 메서드랑

.

import makePacket from '../../utils/packet/makePacket.js';
import { PACKET_TYPE } from '../../config/constants/header.js';

const updateLocationHandler = ({ socket, payload }) => {
  try {
    const { x, y } = payload;

    // 유저 객체 조회
    const user = userSession.getUser(socket);
    if (!user) {
      throw new Error('유저 정보가 없습니다.');
    }

    // RoomId 조회
    const roomId = user.getRoomId();
    if (!roomId) {
      throw new Error(`User(${user.id}): RoomId 가 없습니다.`);
    }

    // 룸 객체 조회
    const room = roomSession.getRoom(roomId);
    if (!room) {
      throw new Error(`Room ID(${roomId}): Room 정보가 없습니다.`);
    }

    // 게임 객체(세션) 조회
    const game = room.getGame();
    if (!game) {
      throw new Error(`Room ID(${roomId}): Game 정보가 없습니다.`);
    }

    // 플레이어 객체 조회
    const player = game.getPlayer(user.id);
    if (!player) {
      throw new Error(`Room ID(${roomId})-User(${user.id}): Player 정보가 없습니다.`);
    }

    const latency = player.calculateLatency();

    game.players.forEach((otherPlayer) => {
      // 계산한 좌표 전송(브로드캐스트)
      const payload = player.calculatePosition(otherPlayer, x, y);

      //payload 인코딩
      const notification = makePacket([PACKET_TYPE.PLAYER_UPDATE_POSITION_NOTIFICATION], payload);

      player.socket.write(notification);
    });
  } catch (error) {
    handleError(socket, error);
  }
};

export default updateLocationHandler;

이 핸들러다.

 

동기화 방법에 대해 굉장히 많이 고민을 했는데,

 

첫번째 방법은 클라로부터 좌표만 받아서 서버가 가진 좌표와 클라로부터 전송받은 새 좌표를 통해 삼각함수를 사용해

방향(각도)를 구하고 레이턴시만큼 계산해서 추측항법을 사용하는것.

 

두번째 방법은 첫번째와 비슷한데 방향키로 방향을 같이 받아서 계산한다.

 

근데 제공받은 클라이언트에서 위치변경요청 패킷에서 일단은 좌표만 주고 있었기에 첫번째 방법을 택하게 되었다.

클라를 고칠수도 있었지만 팀에 클라를 하겠다고(할 수 있는) 사람이 한사람 뿐이었고 클라이언트에 최대한 일을 안주려고 했기도 했다.

 

각 클라이언트들은 0.2초마다 본인의 위치를 서버로 전송하고 서버는 그 위치를 받고 본인을 제외한 다른 클라에게 각 클라이언트마다의 레이턴시를 추측항법에 사용해 위치를 동기화하려고 했다.

 

나머지 클라이언트들도 움직이지 않더라도 0.2초마다 본인의 위치를 쏴주고 있었기 때문에 calculateLatency ()  에서 각 클라이언트들의 레이턴시(0.2초+@)를 계속 갱신해서 가지고 있을 수 있었고 모든 클라이언트의 레이턴시들의 최댓값 또는 평균값을 쓰려고 했는데 한개의 클라이언트 레이턴시가 순간 엄청나게 커지면 그 값들이 망가진다고 생각해 이렇게 해보게 됐다. 

 

테스트를 해보고 싶었는데 회의에서 우리 기획에 맵과 구조물이 있고 그 구조물에 대한 장애물 판정을 어떻게 하냐 얘기하다가 서버에 맵 데이터가 있어야 서버에서 이동을 담당할 수 있다고 했고 맵 데이터를 한달남짓한 마감기한 안에 만들고 나머지 기능을 구현하는게 불가능하다고 판한해 서버가 아닌 클라이언트에서 이동처리를 모두 하고 서버가 검증하는걸로 바뀌면서 테스트를 해볼 기회가 사라지게 되었다. 공부도 많이 하고 생각도 많이 한만큼 시간이 많이 들어가서 애정하는 코드였는데 실패조차 못해보고 묻게돼서 마음이 굉장히 아팠다. 그래서 지금 기록하려한다. 나중에 내 개인 프로젝트를 하게 될 때 이 코드를 사용해서 동기화를 시도해보고싶다.

 

코드에 애정이 생긴다는게 뭔가 했는데 오늘 알아버렸다...

'트러블슈팅' 카테고리의 다른 글

중복 랜더링  (0) 2025.01.17
패킷 길이문제  (0) 2025.01.16
웹소캣게임 트러블슈팅  (0) 2024.12.18