Space Shooter 구현 기록
Pixi.js와 TypeScript로 작은 아케이드 슈팅 게임을 만들며 입력, 적 스폰, 충돌, 난이도 조절을 어떻게 구성했는지 정리합니다.
간단한 레트로 슈팅 게임. 탄막 패턴과 적 스폰 로직, 아이템 드랍을 통해 작은 아케이드 경험을 제공합니다.
- 탄막 패턴과 난이도 스케일링
- 스코어링 및 체력/아이템 관리
- 모바일 조작 대응을 위한 입력 레이어
1. 프로젝트 개요
Space Shooter는 Pixi.js와 TypeScript로 만든 작은 웹 슈팅 게임입니다. 플레이어는 우주선을 움직이며 적을 피하고, 탄을 발사해 점수를 얻습니다. 기능 자체는 단순하지만, 실시간 입력 처리와 렌더링 루프, 충돌 판정, 난이도 증가 같은 게임의 기본 구조를 한 화면에서 확인하기 위해 만든 실험성 프로젝트입니다.
이 프로젝트의 목적은 완성도 높은 상용 게임을 만드는 것이 아니라, 웹 프론트엔드 환경에서 게임 루프를 어떻게 관리할지 확인하는 것이었습니다. 일반적인 웹 UI는 사용자의 이벤트가 발생했을 때 상태를 갱신하는 방식이 많지만, 게임은 매 프레임마다 위치, 속도, 충돌, 점수를 갱신해야 합니다. 그래서 React 컴포넌트 상태보다는 Pixi.js stage와 ticker를 중심으로 런타임 상태를 분리하는 방향으로 구성했습니다.
2. 구현 목표
처음부터 많은 기능을 넣기보다, 플레이 가능한 최소 구조를 먼저 잡았습니다.
- 플레이어 이동과 발사 입력을 처리한다.
- 적은 일정 간격으로 화면 위쪽에서 생성된다.
- 총알과 적이 충돌하면 점수가 증가한다.
- 적이 플레이어와 충돌하거나 화면 아래로 지나가면 게임이 종료된다.
- 시간이 지날수록 적 생성 속도나 이동 속도를 조금씩 높인다.
이 기준을 둔 이유는 게임의 재미보다 구조 검증이 먼저였기 때문입니다. 입력, 스폰, 충돌, 종료 조건이 갖춰지면 이후 아이템, 보스, 이펙트, 사운드 같은 기능은 같은 루프 위에 얹을 수 있습니다.
3. 상태 관리 방식
게임 상태는 크게 세 그룹으로 나누었습니다.
첫 번째는 플레이어 상태입니다. 현재 위치, 이동 방향, 발사 쿨다운, 남은 체력처럼 사용자의 입력과 직접 연결되는 값입니다. 이 값은 매 프레임 갱신되므로 React state로 관리하기보다 일반 객체로 두는 편이 렌더링 비용을 줄이기 쉽습니다.
두 번째는 엔티티 목록입니다. 총알, 적, 아이템처럼 여러 개가 동시에 존재하고 사라지는 객체입니다. 이 목록은 ticker 루프에서 순회하며 위치를 갱신하고, 화면 밖으로 나가거나 충돌한 객체는 제거합니다.
세 번째는 게임 진행 상태입니다. 점수, 난이도, 게임 시작 여부, 게임 오버 여부가 여기에 들어갑니다. UI에 표시해야 하는 값은 React 쪽으로 전달할 수 있지만, 프레임마다 바뀌는 내부 좌표까지 React와 동기화하지는 않는 것이 좋았습니다.
4. 입력과 게임 루프
키보드 입력은 keydown, keyup 이벤트에서 현재 눌린 키 목록을 갱신하는 방식으로 처리했습니다. 실제 이동은 이벤트가 발생한 순간이 아니라 ticker 루프에서 처리합니다. 이렇게 하면 키 반복 속도나 브라우저 이벤트 타이밍에 덜 의존하고, 프레임 기준으로 일정한 이동량을 적용할 수 있습니다.
발사도 같은 방식으로 처리했습니다. 스페이스바를 누르고 있는 동안 매 프레임 총알을 만들면 너무 많은 객체가 생성됩니다. 그래서 발사 쿨다운을 두고, 마지막 발사 이후 일정 시간이 지났을 때만 새 총알을 생성하도록 했습니다.
게임 루프에서는 대략 다음 순서로 상태를 갱신합니다.
- 입력 상태를 읽어 플레이어 위치를 갱신한다.
- 발사 조건을 만족하면 총알을 생성한다.
- 적 스폰 타이머를 갱신하고 새 적을 만든다.
- 총알과 적의 위치를 이동시킨다.
- 충돌 여부를 계산하고 점수/체력을 갱신한다.
- 제거 대상 객체를 stage와 배열에서 정리한다.
- 게임 오버 조건을 확인한다.
이 순서를 고정해 두면 디버깅이 쉬워집니다. 예를 들어 충돌 판정이 이동보다 먼저 실행되면 한 프레임 늦게 맞거나, 이미 화면 밖으로 나간 객체가 충돌 대상으로 남는 문제가 생길 수 있습니다.
5. 충돌 판정
충돌은 단순한 사각형 bounding box 기준으로 처리했습니다. 우주선, 총알, 적 모두 복잡한 도형이 아니기 때문에 정밀한 pixel-level 충돌까지는 필요하지 않았습니다. Pixi.js 객체의 위치와 크기를 기준으로 겹침 여부를 계산하면 충분했습니다.
다만 충돌 후 객체 제거는 조심해야 합니다. 배열을 순회하는 중간에 바로 splice를 반복하면 index가 어긋날 수 있습니다. 그래서 제거 대상은 별도 목록에 담거나, 순회 후 filter로 정리하는 방식이 더 안정적입니다. stage에서도 같은 객체를 제거해야 하므로, 데이터 배열과 Pixi container의 생명주기를 함께 맞추는 것이 중요했습니다.
6. 난이도 조절
난이도는 적의 체력이나 복잡한 패턴보다 생성 간격과 이동 속도부터 조정했습니다. 작은 게임에서는 규칙이 너무 많아지면 원인을 추적하기 어렵습니다. 그래서 시간이 지날수록 적 생성 간격을 줄이고, 이동 속도를 조금씩 높이는 단순한 방식으로 시작했습니다.
이 방식의 장점은 플레이어가 변화를 즉시 느낄 수 있다는 점입니다. 단점은 일정 시간이 지나면 난이도가 급격히 상승할 수 있다는 점입니다. 이후 개선한다면 난이도 증가값에 상한을 두고, 특정 점수 구간마다 패턴을 바꾸는 방식이 더 자연스럽습니다.
7. 플레이 방법
- 방향키 또는 지정된 이동 키로 우주선을 움직입니다.
- 스페이스바로 총알을 발사합니다.
- 화면 위에서 내려오는 적을 맞히면 점수가 올라갑니다.
- 적과 충돌하거나 적이 화면 아래로 지나가면 게임이 종료됩니다.
8. 정리
Space Shooter는 작은 실험이지만, 웹에서 게임형 인터랙션을 만들 때 필요한 기본 단위를 확인하기 좋은 프로젝트였습니다. 특히 React 상태와 Pixi.js 런타임 상태를 무리하게 합치지 않고, 빠르게 변하는 값은 게임 루프 안에 두는 편이 더 예측 가능했습니다.
다음 개선을 한다면 모바일 터치 입력, 적 패턴 분리, 오브젝트 풀링, 사운드 이펙트, 게임 오버 후 재시작 플로우를 순서대로 붙이는 것이 좋습니다. 기능을 한 번에 늘리기보다 루프, 입력, 충돌, 정리라는 기본 구조를 유지하면서 확장하는 편이 안정적입니다.