웹 브라우저에서 돌아가는 2D 게임을 만들고 싶어서 Phaser 3를 선택했다. Unity나 Godot 같은 본격적인 엔진도 있지만, 간단한 2D 게임이라면 Phaser로 충분하다. 특히 웹 배포가 기본이라 별도의 빌드 과정 없이 브라우저에서 바로 테스트할 수 있는 게 큰 장점이다.
이번에 만들 건 탑다운 뷰의 간단한 RPG 스타일 게임이다. 캐릭터가 맵을 돌아다니고, 장애물에 충돌하고, 아이템을 줍는 정도의 기본 기능을 구현해본다.
프로젝트 세팅
mkdir topdown-game && cd topdown-game
npm init -y
npm install phaser
Vite를 번들러로 쓰면 개발이 편하다:
npm install -D vite
index.html을 만든다:
<!DOCTYPE html>
<html>
<head>
<title>Topdown Game</title>
<style>
body { margin: 0; background: #000; display: flex; justify-content: center; align-items: center; height: 100vh; }
</style>
</head>
<body>
<script type="module" src="/src/main.js"></script>
</body>
</html>
게임 설정
src/main.js에서 Phaser 게임을 초기화한다:
import Phaser from 'phaser';
import { GameScene } from './scenes/GameScene';
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
physics: {
default: 'arcade',
arcade: {
gravity: { y: 0 }, // 탑다운이니까 중력 없음
debug: false,
},
},
scene: [GameScene],
pixelArt: true, // 픽셀 아트 스타일이면 이걸 켜야 안티앨리어싱이 안 먹힌다
};
new Phaser.Game(config);
탑다운 게임은 중력이 없다는 게 핵심이다. 플랫포머와 달리 위에서 내려다보는 시점이니까 gravity.y를 0으로 설정한다.
타일맵 만들기
맵을 만들려면 Tiled라는 무료 맵 에디터를 쓰는 게 좋다. Tiled에서 만든 JSON 맵 파일을 Phaser에서 바로 로드할 수 있다.
간단하게 설명하면:
- Tiled에서 타일셋 이미지를 불러온다 (16x16 또는 32x32 타일)
- 바닥 레이어, 장애물 레이어, 오브젝트 레이어를 나눠서 만든다
- JSON 형식으로 내보낸다
Phaser에서 맵을 로드하는 코드:
export class GameScene extends Phaser.Scene {
constructor() {
super('GameScene');
}
preload() {
this.load.image('tiles', '/assets/tileset.png');
this.load.tilemapTiledJSON('map', '/assets/map.json');
this.load.spritesheet('player', '/assets/player.png', {
frameWidth: 32,
frameHeight: 32,
});
}
create() {
// 맵 생성
const map = this.make.tilemap({ key: 'map' });
const tileset = map.addTilesetImage('tileset', 'tiles');
const groundLayer = map.createLayer('Ground', tileset);
const wallLayer = map.createLayer('Walls', tileset);
// 벽 레이어에 충돌 설정
wallLayer.setCollisionByProperty({ collides: true });
// 플레이어 생성
this.player = this.physics.add.sprite(400, 300, 'player');
this.player.setCollideWorldBounds(true);
// 벽과 플레이어 충돌
this.physics.add.collider(this.player, wallLayer);
// 키보드 입력
this.cursors = this.input.keyboard.createCursorKeys();
// 카메라가 플레이어를 따라가게
this.cameras.main.startFollow(this.player);
this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
}
update() {
const speed = 160;
this.player.setVelocity(0);
if (this.cursors.left.isDown) {
this.player.setVelocityX(-speed);
} else if (this.cursors.right.isDown) {
this.player.setVelocityX(speed);
}
if (this.cursors.up.isDown) {
this.player.setVelocityY(-speed);
} else if (this.cursors.down.isDown) {
this.player.setVelocityY(speed);
}
// 대각선 이동 시 속도 정규화
this.player.body.velocity.normalize().scale(speed);
}
}
여기서 중요한 부분이 대각선 이동 속도 정규화다. 이걸 안 하면 대각선으로 이동할 때 가로+세로 속도가 합쳐져서 다른 방향보다 약 1.4배 빨라진다. normalize().scale(speed) 한 줄로 해결된다.
캐릭터 애니메이션
스프라이트시트를 만들어서 방향별 걷기 애니메이션을 넣는다:
// create() 안에서
this.anims.create({
key: 'walk-down',
frames: this.anims.generateFrameNumbers('player', { start: 0, end: 3 }),
frameRate: 8,
repeat: -1,
});
this.anims.create({
key: 'walk-up',
frames: this.anims.generateFrameNumbers('player', { start: 4, end: 7 }),
frameRate: 8,
repeat: -1,
});
this.anims.create({
key: 'walk-left',
frames: this.anims.generateFrameNumbers('player', { start: 8, end: 11 }),
frameRate: 8,
repeat: -1,
});
this.anims.create({
key: 'walk-right',
frames: this.anims.generateFrameNumbers('player', { start: 12, end: 15 }),
frameRate: 8,
repeat: -1,
});
update()에서 이동 방향에 따라 애니메이션을 전환한다:
if (this.cursors.left.isDown) {
this.player.anims.play('walk-left', true);
} else if (this.cursors.right.isDown) {
this.player.anims.play('walk-right', true);
} else if (this.cursors.up.isDown) {
this.player.anims.play('walk-up', true);
} else if (this.cursors.down.isDown) {
this.player.anims.play('walk-down', true);
} else {
this.player.anims.stop();
}
아이템 수집 시스템
맵에 코인을 배치하고 플레이어가 닿으면 수집되게 만든다:
// create() 안에서
this.coins = this.physics.add.group();
this.score = 0;
this.scoreText = this.add.text(16, 16, 'Score: 0', {
fontSize: '18px',
fill: '#fff',
}).setScrollFactor(0); // 카메라 이동해도 UI는 고정
// 맵에서 코인 위치를 불러와서 배치
const coinObjects = map.getObjectLayer('Coins');
coinObjects.objects.forEach((coin) => {
const c = this.coins.create(coin.x, coin.y, 'coin');
c.body.setAllowGravity(false);
});
// 플레이어가 코인에 닿으면 수집
this.physics.add.overlap(this.player, this.coins, (player, coin) => {
coin.destroy();
this.score += 10;
this.scoreText.setText(`Score: ${this.score}`);
});
빌드와 배포
npx vite build
dist 폴더가 생기고, 이걸 아무 정적 호스팅에 올리면 된다. Cloudflare Pages에 올리면 무료로 빠르게 배포할 수 있다.
정리
Phaser 3로 탑다운 게임의 기본 골격을 만들어봤다. 타일맵, 캐릭터 이동, 충돌 처리, 아이템 수집까지 핵심 기능이 모두 들어있다. 여기에 NPC 대화 시스템이나 인벤토리, 전투 시스템을 얹으면 꽤 그럴듯한 게임이 된다.
Phaser의 장점은 웹 기술 스택을 그대로 쓸 수 있다는 점이다. React로 UI를 입히거나, 서버와 WebSocket으로 멀티플레이어를 구현하는 것도 가능하다. 가벼운 2D 게임을 빠르게 프로토타이핑하고 싶다면 Phaser 3를 추천한다.