[ft_transcendence] 1. 게임 설계
개요
본 과제에서 만들어야 하는 서비스에는 채팅과 게임이라는 두 개의 주요 기능이 있다.
그중 나는 게임을 구현하는 부분을 맡았다.
만들어야 하는 게임은 1972년에 출시된 Pong의 규칙을 따르는 간단한 핑퐁게임이었다.
Pong과 동일한 규칙으로 진행되기만 하면 게임을 구현하는 데에 별도의 제한이 없었으므로 우리는 최대한 간단하게 만들기로 했다.
팀원 중 한 분의 블랙홀이 얼마 남지 않아 시간이 없었기 때문에...ㅠㅠ
게임 부분의 요구사항은 다음과 같다.
1) 실시간 온라인 1:1 게임이 가능해야 하며, 모든 조작은 플레이할 수 있을 수준으로 부드럽게 작동해야 한다.
2) 별도로 방을 생성하거나 초대 없이 매칭 큐 등록을 통해 랭크게임을 진행할 수 있어야 한다.
3) 채팅 인터페이스를 통해 다른 유저를 초대하여 랭크 점수에 반영이 되지 않는 친선게임을 진행할 수 있어야 한다.
4) 다른 플레이어의 게임을 실시간으로 관전 할 수 있어야 한다.
5) 게임에는 난이도, 맵, 아이템 등 유저가 커스텀할 수 있는 요소가 있어야 한다.
실시간 게임과 관전을 구현하기 위해 socket.io를 사용하여 서버와 통신하였으며,
게임을 프론트 상에 띄우기 위해서는 별도의 게임엔진 등을 사용하지 않고 HTML canvas를 사용하였다.
특수한 이펙트나 물리효과 등을 집어넣을 생각도 없었고, 또 게임엔진 사용법까지 학습하자니 시간이 촉박했기 때문이다...
게임 진행에 있어 필요한 상태 관리는 Recoil을 활용하였다 역시 사용법이 간편하고 학습이 쉬웠기 때문.
전체적인 게임 제작은 아래의 영상을 참고하여 진행할 수 있었다.
https://www.youtube.com/watch?v=nl0KXCa5pJk&t=2285s
게임 설계
먼저 게임에 관련된 연산이 어디서 이루어질 것인가부터 정할 필요가 있었다. 여기에는 크게 다음과 같은 2가지 옵션이 있었다.
- 서버에서는 프론트에 상대방의 Paddle 위치만 전송하고 공의 튕김, 위치, 점수 등등은 프론트에서 연산. 게임의 결과는 프론트에서 결정되고 백은 이 결과를 받아서 처리.
- 서버에서 모든 연산이 이루어지고 프론트는 서버에서 전송하는 렌더링 데이터를 받아 뿌려주기만 함. 프론트 역할은 사용자의 입력만 전달.
원래는 지연속도 때문에 플레이가 매끄럽지 않을 것이라 생각해서 1번 방법으로 가려고 했는데,
백엔드 팀원 분들이 프론트에서 액세스 토큰이 탈취당하면 유저가 마음대로 결과를 전송해버릴 수 있는 보안상 결점이 생긴다고 하여 2번 방법으로 가기로 결정했다.
덕분에 나는 그냥 렌더 데이터를 받아서 화면 뿌려주기만 하면 되어 공수가 많이 줄어들게 되었다.
이 점이 결정된 다음에는 게임이 어떻게 진행될지 단계를 잘 나눌 필요가 있다고 생각했다.
우리가 설계한 UI 상 랭크매치, 일반매치, 관전모드가 모두 화면의 한 부분 안에서 진행되어야만 했는데, 컴포넌트 설계를 잘못하면 중복된 코드를 여러 번 작성해야 할 수도 있었기 때문이다.
어떻게 할지 고민을 하다가 먼저 게임의 진행 단계를 나누고, 각각의 진행 단계에 맞춰 state 값을 바꿔 컴포넌트를 렌더링 해주는 형식으로 게임을 만들기로 했다.
다행히도 리액트는 스위프트에서 enum과 switch문을 활용하는 것처럼
특정한 state의 값에 따라 어떤 컴포넌트를 렌더링 할 것인지 결정할 수 있는 기능을 제공하고 있었다.
export type GameStatus =
| 'INTRO'
| 'ON_MATCHING'
| 'MATCHED'
| 'PLAYING'
| 'WATCHING'
| 'FINISHED';
const GameContainer = () => {
const [currentStatus, setCurrentStatus] = useRecoilState(currentGameStatus);
//...
return (
<div className="flex h-full w-full flex-col bg-neutral-900">
{
{
INTRO: <GameIntro />,
ON_MATCHING: <GameOnMatching />,
MATCHED: <GameMatched />,
PLAYING: <GameWindow />,
WATCHING: <GameWindow />,
FINISHED: <GameFinished />,
}[currentStatus]
}
</div>
);
};
이런식으로 GameContainer라는 래퍼 용도의 컨테이너를 만든 후, GameStatus라는 타입의 값에 따라 각 단계에 알맞는 컴포넌트를 렌더링하도록 했다.
게임의 진행 순서는
- INTRO: 유저가 서비스에 접속한 후 Start Game 버튼을 마주하게 되는 단계
- ON_MATCHING: 유저가 Start Game 버튼을 눌러 매칭큐에 등록하는 단계
- MATCHED: 유저가 매칭되어 상대방을 확인한 후, 게임 옵션을 설정하고 Ready 버튼을 눌러 게임 시작 준비를 하는 단계
- PLAYING: 유저가 게임을 실제로 진행하는 단계
- FINISHED: 유저가 게임의 결과를 확인하는 단계
의 총 5단계로 나누었으며, 별도로 관전을 처리하기 위해 WATCHING이라는 상태를 하나 더 만들어 두었다.
이렇게 처음에 조금 게임 진행단계를 생각해가며 설계를 해 둔 덕에 소켓의 에러처리도 편하게 할 수 있었고, 관전모드와 친선경기도 손쉽게 구현할 수 있었으며, 또 막판에 긴급하게 서버쪽에서 처리할 수 없는 에러들을 막아야 하는 일이 생겼는데 GameStatus의 값에 따라 아예 서버쪽에 요청을 전송하지 않는 식으로 치명적인 에러들을 처리할 수 있었다.