지금까지는 canvas
요소를 이용해서 간단한 게임을 만들어봤다. 새롭고 재밌는 활동이었지만, 시행착오가 많았던 시간이었다. 이제 본격적으로 phaser
를 이용해서 게임을 만드는 과정을 다루어 보려고 한다.
먼저 오늘은 phaser
가 무엇인지, 어떻게 동작하고 사용하는지를 정리해보려고 한다.
phaser
란,phaser
는 canvas API
를 맵핑한 자바스크립트 기반의 게임 프레임워크다. 데스크탑이나 모바일 브라우저 등 다양한 웹 게임을 만들 수 있게 도와준다. phaser
는 자바스크립트로 개발되었기 때문에 자바스크립트 또는 타입스크립트를 사용하여 게임을 개발할 수 있다.
phaser
설치는 Phaser - Download 링크를 통해서 다운로드 해서 직접 설칳하거나 명령어로 간단하게 설치할 수 있다.
npm i phaser // "phaser": "^3.60.0"
사용법을 익히기 위해 phaser
의 튜토리얼을 따라서 진행할 예정이다. phaser
튜토리얼을 진행하기 전에 먼저 다운받아야하는 파일들이 있다. 먼저 필요한 파일을 다운받자
나는 vite
룰 이용해서 기본 프로젝트를 생성하였고(typescript
를 사용) src/
에 있는 모든 내용을 제거하고 index.ts를 파일을 추가하였다. 그리고 아래와 같이 코드를 작성해보았다.
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: "#B23A3A",
};
const game = new Phaser.Game(config);
먼저, type
은 phaser
의 렌더링 방식을 설정할 수 있다. 기본값은 Phaser.AUTO
이다.(Phaser.WEBGL
이나 Phaser.CANVAS
두 가지 중 선택하여 사용할 수 있다)
여기서 width
와 height
은 phaser
를 생성하는 canvas
요소의 크기이다.(단위: pixel) backgroundColor
속성을 추가하여 게임의 배경을 지정할 수 있고, 일반적으로 hex
값을 사용한다.
그럼 아래와 같은 화면이 나타난다.
phaser
는 scene
이라는 개념은 게임의 맵이나 화면 하나를 나타내는 단위이다. 보통 게임의 로직을 작성한다.(캐릭터의 움직임이나 효과음등 게임이 동작하기 위해 필요한 작업)
나는 Scene/GameScene.ts
파일을 추가해줬다. phsser
의 장면은 Phaser.Scene
을 상속받아 만들어진다.
class GameScene extends Phaser.Scene {
constructor() {
super({ key: "GameScene" }); // key는 Phaser에서 Scene을 식별하기 위한 값
}
// 외부 파일 혹은 assets을 미리 불러오기 위한 작업 처리
preload(): void {
$.load.image("sky", "assets/sky.png");
this.load.image("ground", "assets/platform.png");
this.load.image("star", "assets/star.png");
this.load.image("bomb", "assets/bomb.png");
this.load.spritesheet("dude", "assets/dude.png", {
frameWidth: 32,
frameHeight: 48,
});
}
// 게임 시작시에 필요한 GameObject를 정의
create(): void {
this.add.image(400, 300, "sky");
this.add.image(400, 300, "star");
}
// 애니메이션을 정의하거나 게임상에서 상호작용을 해야하는 경우 처리
update(): void {}
}
export default GameScene;
이렇게 만들어진 장면(Scene
)을 index.ts의 다음 코드에 추가해준다.
import Phaser from "phaser";
import GameScene from "./\bScene/GameScene";
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: "#B23A3A",
scene: GameScene,
};
const game = new Phaser.Game(config);
그럼 아래와 같은 결과 화면을 볼 수 있다. 배경이 나타났다.
preload
함수에서 미리 불러온 assets들을 이용해 하나씩 게임 요소를 부착해볼 것이다. 먼저, ground라는 key값을 이용해서 platform.png를 가져올 것이다. 먼저,index.ts에서 phsics
를 추가해보자
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: "#B23A3A",
physics: {
default: "arcade",
arcade: {
gravity: { y: 300 },// y방향 300의 중력을 적용, y가 클수록 빠르게 하강
debug: false, // 디버그 모드 활성화시, 충돌 영역 및 기타 물리 관련 정보 표시
},
},
scene: [GameScene],
};
const game = new Phaser.Game(config);
physics
에 대해 간단하게 설명해보자면 물리 엔진을 설정하기 위해 사용하는 속성이다.
자주 사용하는 물리 엔진 종류는 크게 arcade
/ matter
/ impact
가 있다.
arcade
: 가벽고 간단한 2D 스타일의 물리 엔진을 만들 때 사용 / 박스기반 물리엔진matter
: 복잡하고 강력한 2D 엔진을 만들 때 사용 / 세부적인 조작이 가능한 물리엔진path
간 처리 가능impact
: impact.js 라이브러리에서 파생된 2D 물리 엔진으로, HTML5 기반 게임에 최적화그리고 다시 GameScene.ts로 돌아가서 땅 이미지를 추가하는 코드를 작성한다.
create() : void {
...
// 땅 이미지 추가하여 맵 생성
const platforms = this.physics.add.staticGroup();
platforms.create(400, 568, "ground").setScale(2).refreshBody();
platforms.create(600, 400, "ground");
platforms.create(50, 250, "ground");
platforms.create(750, 220, "ground");
}
이제 캐릭터를 생성해보자. GameScene.ts에 create함수 안에 아래 내용을 추가한다.
create() : void {
...
const player = this.physics.add.sprite(100, 450, "dude").setName("player");
// player가 바닥에 닿았을 때, 약간 튕기는 느낌을 설정
player.setBounce(0.2); // y값을 생략하면 x와 y모두 동일한 값을 적용하는 것과 같음
// player가 게임 화면 밖으로 나가지 않게 설정
player.setCollideWorldBounds(true);
}
여기서 사용한 sprite(x, y, imageKey)
메서드는 게임에 등장하는 이미지나 애니메이션을 표현하기 위한 스프라이트 객체를 생성해준다. 이렇게 생성된 캐릭터(player)는 중력이나 충돌과 같은 물리적 특징을 부여할 수 있다.
출력된 이미지를 자세히 보면 플레이어가 플랫폼보다 아래에 나타나는 것을 확인할 수 있다. 플레이어와 플랫폼 간의 충돌을 감지하여 충돌시 플레이어가 플랫폼 위로 올라올 수 있도록 중력을 적용해보자
맨 마지막 줄에 아래 코드를 추가하면 된다. collider
는 phaser
에서 충돌을 체크해주는 메서드이다.
this.physics.add.collider(player, platforms);
다시 화면을 출력해보면 플레이어가 땅위로 올라온 것을 확인할 수 있다.
GameScene.ts 안에 애니메이션을 생성하는 코드를 아래와 같이 추가한다. 사용할 애니메이션을 먼저 생성해준다.
create() : void {
...
// 애니메이션 생성 - 나중에 key로 애니메이션 식별해서 참조
this.anims.create({
key: "left",
frames: this.anims.generateFrameNumbers("dude", { start: 0, end: 3 }),
frameRate: 10, // 초당 프레임 수를 나타냄(초당 10프레임 설정 의미)
repeat: -1, // 무한 반복을 의미
});
this.anims.create({
key: "turn",
frames: [{ key: "dude", frame: 4 }],
frameRate: 20,
});
this.anims.create({
key: "right",
frames: this.anims.generateFrameNumbers("dude", { start: 5, end: 8 }),
frameRate: 10,
repeat: -1,
});
}
그리고 GameScene안에 아래와 같이 작성한다. 먼저, create
메서드 안에 createCursorKeys()
를 통해 키보드의 화살표 키들에 대한 입력을 처리한다.
cursors = this.input.keyboard?.createCursorKeys();
다음에 update
메서드 안에 해당 키보드가 눌렸을 때 플레이어의 상태를 추가해준다. setVelocityX(value)
를 통해 프레임마다 움직일 속도를 지정하고, play(애니메이션키, 반복여부)
메서드를 통해서 애니메이션을 실행한다.
// 애니메이션을 정의하거나 게임상에서 상호작용을 해야하는 경우 처리
update(): void {
const cursors = this.input.keyboard?.createCursorKeys();
const player = this.children.getByName(
"player"
) as Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
if (!cursors || !player) return;
if (cursors.left.isDown) {
player.setVelocityX(-160); // 프레임마다 움직일 속도
player.anims.play("left", true); // 애니메이션 실행 메서드(애니메이션 키, 반복여부)
} else if (cursors.right.isDown) {
player.setVelocityX(160);
player.anims.play("right", true);
} else {
player.setVelocity(0);
player.anims.play("turn");
}
if (cursors.up.isDown && player.body.touching.down) {
player.setVelocityY(-330);
}
}
그럼 아래와 같이 동작하는 플레이어를 구현할 수 있다.
이번에는 dude가 모아야 하는 별 그룹을 만들어보자. GameScene.ts안에 create()
에 아래 코드를 추가하여 별 그룹을 생성한다. x좌표 70만큼 간격을 두고 11번 반복하여 별을 생성한다.
// 별 생성
stars = this.physics.add.group({
key: "star",
repeat: 11,
setXY: { x: 12, y: 0, stepX: 70 },
});
// 별 이미지 요소 튕김 효과
stars.children.iterate((child) => {
const image = child as Phaser.Physics.Arcade.Image;
image.setBounceY(Phaser.Math.FloatBetween(0.4, 0.8));
return null;
});
그리고 disableBody(active, visible)
을 이용하여 플레이어와 별이 닿았을 때를 별을 수집한 것과 같은 처리를 한다. (게임 객체를 비활성화 시키고 보이지 않게 설정한다)
collectStar(_player: Phaser.Types.Physics.Arcade.GameObjectWithBody | Phaser.Tilemaps.Tile,
star: Phaser.Types.Physics.Arcade.GameObjectWithBody | Phaser.Tilemaps.Tile) {
const stars = this.data.get("stars") as Phaser.Physics.Arcade.Group;
const target = star as Phaser.Physics.Arcade.Image;
target.disableBody(true, true);
}
이번에는 create()
안에 아래와 같이 작성하여 텍스트 객체를 생성한다. 그럼 화면과 같은 텍스트가 게임 화면에 부착된다.
// 점수 텍스트 생성
this.scoreText = this.add.text(16, 16, "SCORE : 0", {
fontSize: "32px",
color: "#333",
});
그리고 collectStar()
안에 별을 모을 때마다 점수를 부여해준다. 이렇게 하면 별을 수집할 때 마다 10씩 점수를 획득하게 된다.
this.score += 10;
this.scoreText?.setText("SCORE : " + this.score);
이제 마지막으로 폭탄을 만들 것이다. 폭탄과 플레이어간 충돌을 처리해보려고 한다. 아래 코드를 create()
안에 작성하여 폭탄을 생성한다.
여기서 주요 코드는 collider
라는 메서드인데 인자로 받은 두 요소 간의 충돌을 발생시킨다.
// 장애물 생성
const bombs = this.physics.add.group();
this.data.set("bombs", bombs);
this.physics.add.collider(bombs, platforms);
this.physics.add.collider(player, bombs, this.hitBomb, undefined, this);
그리고 GameScene
클래스 안에 부딪혔을 때 함수를 추가해준다.
// 플레이어 및 폭탄 충돌 처리
hitBomb(
_player:
| Phaser.Types.Physics.Arcade.GameObjectWithBody
| Phaser.Tilemaps.Tile
) {
this.physics.pause();
const player = _player as Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
player.setTint(0xff0000);
player.anims.play("turn");
}