[CH 4 타워 디펜스 게임 프로젝트] 트러블슈팅 1016

2024. 10. 16. 22:06[Node.js_6기 본캠프 TIL]

 

⛓️ 클라이언트와 서버 간의 연결고리 만들기

서버에서 검증된 데이터를 기반으로 클라이언트에서 타워를 그려내야 했지만, 앞선 트러블슈팅에서 언급했듯 웹소켓에 대한 이해도가 많이 부족한 상황이었다. 클라이언트와 게임 서버의 역할 그 자체에 대한 이해도가 많이 부족했다고도 할 수 있을 것 같다. 그래서 클라이언트에서 우선 구현한 기능들을 서버와 어떻게 주고받아야 할지 연결고리를 이해하고 만드는 것까지 도전의 연속이었다.

 

주말 내내 github에 'tower defense'를 검색해서 비슷한 프로젝트들의 코드를 뜯어보고, 팀원분들이 먼저 구현한 몬스터나 스테이지 관련 코드들을 뜯어보고 나서야 기본적인 구조가 이해되기 시작했다. 이전 개인 프로젝트를 진행할 당시, 스켈레톤 코드에서 이미 거의 완성되어 있어 건드릴 생각도 못한 sendEvent와 핸들러의 역할을 이제서야 제대로 이해하게 된 것이다. (개인 과제가 유난히 어려웠던 것도 이런 기본적인 부분을 제대로 이해하지 못하고 넘어가서가 아닐까 하는 생각이 든다.)

 

구조를 겨우 이해하고 난 뒤, 타워 관련 기능들을 웹소켓의 특성을 활용하여 구현하려면 클라이언트와 서버 간에 어떤 과정이 있어야 하는지 하나씩 정리해보기 시작했다.

 

  1. 클라이언트에서 타워 구매 요청(구매 버튼 클릭 등) 이벤트를 신호로 보낸다.  *sendEvent( handlerId, { payload: payload } )
  2. 서버에서 신호와 함께 받은 데이터를 기반으로 검증 로직을 돌린다. 
  3. 검증 결과를 클라이언트에 반환한다. *socket.emit( 'event', { status: 'success', message: 'message' })
  4. 서버에서 받은 데이터를 기반으로 클라이언트에 타워를 그려낸다. *socket.on( 'event', (data) => { } )
  5. 서버와 클라이언트의 데이터를 동기화한다.

 

하기와 같이 클라이언트에서만 억지로 구현한 코드를 하나씩 목적에 맞게 분리하기 시작했다.

// public/src/game.js


function placeNewTower() {
  if (towers.length >= 10) {
    alert('타워는 10개까지만 건설할 수 있습니다.');
  } else if (userGold < towerCost) {
    alert('잔액이 부족합니다.');
  } else if (userGold >= towerCost) {
    const { x, y } = getRandomPositionNearPath(200);
    const tower = new Tower(x, y, 0);
    towers.push(tower);
    tower.draw(ctx, towerImage);
    towerId++; // 타워 건설 후, 타워 Id를 더한다.
    console.log(towerId);
    userGold -= towerCost;
  }
}

 

 

분리한 코드는 하기와 같다.

// public/src/game.js


// 클릭 이벤트 발생 시, 데이터 전송 및 서버에 검증 요청
function clickBuyTower() {
  audioManager.playSoundEffect('click');
  const towerId = Math.floor(Math.random() * assets.tower.data.length);
  sendEvent(21, { userGold: userGold, towerId: towerId });
}

// 서버에서 검증 완료 후 'success' 신호를 받아 타워 그리기
function placeNewTower(towerId) {
  const { x, y } = getRandomPositionNearPath(200);
  const tower = new Tower(x, y, towerImages, 1, assets.tower, towerId, audioManager);
  towers.push(tower);
  // 그려진 타워 정보를 서버에 보내 데이터 동기화 진행
  sendEvent(30, { towerData: tower, index: towers.length - 1 });
}

serverSocket.on('buyTower', (data) => {
    if (data.status === 'success') {
      placeNewTower(data.towerId);
      userGold -= data.cost;
    } else if (data.status === 'fail' && data.message === 'tower limit') {
      gameStateMessage.showMessage(9);
    } else if (data.status === 'fail' && data.message === 'not enough gold') {
      gameStateMessage.showMessage(10);
    } else {
      console.error('Error occurred while buy the tower!');
      alert('Error occurred while buy the tower!');
    }
  });
// src/handlers/towerHandlers.js


// 클라이언트에서 받은 데이터를 기반으로 검증 진행
export const handleBuyTower = async (userId, payload, socket) => {
  const { userGold, towerId } = payload;
  const { tower } = getGameAssets();

  const towers = getAllUserTowers(userId);

  // 타워 개수 확인
  if (towers.length >= 15) {
    socket.emit('buyTower', { status: 'fail', message: 'tower limit' });
    return;
  }
  // 골드 확인
  else if (userGold < tower.data[towerId].cost) {
    socket.emit('buyTower', { status: 'fail', message: 'not enough gold' });
    return;
  }
  // 골드 차감
  else if (userGold >= tower.data[towerId].cost) {
    socket.emit('buyTower', { status: 'success', cost: tower.data[towerId].cost, towerId });
    return;
  }
};

// 클라이언트로부터 전달받은 데이터를 기반으로 서버 데이터 동기화
export const userTowerUpdate = async (userId, payload) => {
  const { towerData, index } = payload;

  updateUserTowerData(userId, towerData, index);
};

 

같은 구조로 상황과 목적에 따라 코드를 분리 및 정리하다보니 기존에 목표했던 타워 구매/판매/강화 기능 뿐만 아니라 트랩이라는 추가 기능까지 완성할 수 있었다.