웹 기반 게임으로 방향을 정했으며 최대 4 VS 4 까지 가능한 데스매치 슈팅 게임입니다. 웹 게임인 만큼 접근성을 쉽게 하기 위해 간단한 회원가입만 마치면 누구나 즐길 수 있는 게임을 기획했습니다.
- NX: monorepo 관리 툴
- Node.js
- Typescript
- React
- Three.js
- Phaser: Game Engine
- Spring boot JPA
- QueryDSL
- H2 Hibernate (Local ver.)
- Postman
- AWS
- AWS EC2
- AWS S3
- AWS RDS (MySQL)
- Client ↔ Main Server 동작표
- 게임 시작 시 동작표
- 우주 배경
- 인트로 페이지
- React-Hook-Form을 이용하여 렌더링 및 코드 최적화
- 정규식과 Hook-Form라이브러리를 이용하여 유효성 검사
- 로비 페이지
- 게임룸
- 배포
- 정렬 기능
- 빠른 시작
- 방 비번방 설정
- 도움말
- 로비방 새로고침 눌러야 방 업데이트 되는 로직으로 변경
- 게임중인 방 구별할 수 있게 색 변경
- 오디오
- 방장 처리
- 시작버튼 여러번 클릭 시 에러 수정
- 팀 변경
- 방에 입장할 수 없는 방 에러 처리
- 전적 정보
- 위 타입 중 가장 중요한 START 타입을 받으면 아래처럼 실행된다.
- 인원 수를 확인 후 짝수일 경우 실행, 아닐 시 인원 수를 체크해달라고 메시지를 보낸다.
- 게임이 시작되면 RestTemplate를 통해 게임 서버로 방 id, 유저 정보, callback 주소 등을 담아서 보낸다.
- 게임 서버에 데이터가 잘 도착하면 Response로 Stage id와 유저 id 및 시크릿 코드를 메인 서버로 보낸다.
- 메인 서버는 위 데이터를 잘 받은 뒤 Client에게 각 유저마다 해당하는 시크릿 코드를 보내준다.
- 게임이 끝나면 게임 서버가 메인 서버의 RESTful API를 호출하여 결과 데이터를 보내준다. 메인 서버는 이것을 DB에 저장하고 종료된다.
- Room & Stage
기존 구조
현재 구조
- 통신
export type JoinEvent = {
//
};
export type InputEvent = {
horizontalAxis: number;
verticalAxis: number;
fire: boolean;
};
export type ClientEventMap = {
join: JoinEvent;
input: InputEvent;
};
emit<K extends keyof ClientEventMap>(name: K, event: ClientEventMap[K]) {
this.channel?.emit(name, event);
}
on<K extends keyof ServerEventMap>(
name: K,
callback: (event: ServerEventMap[K]) => void
) {
this.channel?.on(name, callback as any);
}
메인 서버와 연동 전 진행한 통신 부분 리팩토링. 라이브러리 의존성을 정리하여 후에 쉽게 변경하도록 함. Typescript를 적극 활용하여 이벤트 네임에 따라 페이로드 타입을 확인할 수 있도록 함.
- 동기화
- 직렬화 & 역직렬화
export class Player extends Entity {
serialize(): EntityData {
return {
id: this.id,
x: this.x,
y: this.y,
angle: this.angle,
nickname: this.nickname,
team: this.team,
speed: this.speed,
angularSpeed: this.angularSpeed,
hp: this.hp,
};
}
deserialize(data: EntityData) {
this.x = +(data['x'] ?? this.x);
this.y = +(data['y'] ?? this.y);
this.angle = +(data['angle'] ?? this.angle);
this.nickname = data['nickname'] + '';
this.team = data['team'] === 'RED_TEAM' ? 'RED_TEAM' : 'BLUE_TEAM';
this.speed = +(data['speed'] ?? this.speed);
this.angularSpeed = +(data['angularSpeed'] ?? this.angularSpeed);
this.hp = +(data['hp'] ?? this.hp);
}
}
모든 Entity는 위 함수 두개를 구현하고 있음. Scene를 동기화 할 때 활용 함.
chat과 message를 분리해서 MessageHandler를 사용한건 좋았는데 로비같은 경우는 chat 부분만 stomp 통신을 사용하다 보니 게임방과 로비 사이의 괴리감이 생겨 게임적으로 유연하지 못한 점이 많았음. 예를 들면 로비 입장 시에는 채팅을 제외한 다른 데이터를 REST 통신으로 가져와서 방목록이나 대기실 유저 목록 같은 경우 새로고침을 해줘야 최신화가 됐고, disconnect 이벤트가 발생할 때도 로비는 chat 부분만 사용하므로 chat인지 message인지 확인해서 처리해야 되는 기이한 구조를 갖게 됐음.
- 로비에서 웹소켓이 필요한 부분을 REST 통신으로 진행해 게임방과의 괴리감 발생
- /sub에도 prefix를 추가해 chat과 message에 대한 구독을 분리시켜 관리하거나
- 로비도 게임방처럼 chat과 message prefix를 통한 전반적인 stomp 통신 기반으로 설계 했어야 함.