게임 프레임워크 : Phaser3
Phaser3이란?
RPG 만들기(일명 알만툴) 이라고 아시나요? 게임 만들기에 관심이 있었다면 한번쯤 알만툴이나 스타크래프트 유즈맵 편집기를 통해 게임을 제작해 본 적이 있을 것입니다. Phaser 또한 이러한 툴과 비슷한 자바스크립트 기반 게임 프레임워크 입니다. 자바스크립트를 기반으로 하기때문에 웹브라우저 환경에서 게임실행이 가능하도록 합니다.
기본적으로 씬(Scene)
과 게임 오브젝트(Game Object)
로 구성되며, 물리엔진을 포함하고 있어서 손쉽게 게임을 제작할 수 있습니다.
공식문서의 튜토리얼을 통해 간단한 게임을 제작해보도록 하겠습니다.
완성 프로젝트 : phaser3_example.zip
프로젝트 설정
우분투 20.04 LTS 환경을 기준으로 프로젝트를 생성해보겠습니다. 웹브라우저 환경에서 실행되므로 웹서버를 필요로 합니다. 간단하게 nodejs를 통해 웹서버를 만들도록 합시다.
$ mkdir phaser3 && cd phaser3 && mkdir public && npm i http-server
웹서버를 실행하면 public 폴더가 루트폴더로 설정됩니다.
$ npx http-server
타입스크립트를 통해 프로젝트를 만들도록 하겠습니다. 새로운 터미널을 열고 npm을 통해 typescript와 phaser를 설치합니다.
$ npm i typescript phaser
타입스크립트 설정파일(tsconfig.json)을 생성하고 다음과 같이 작성해줍니다.
$ npx tsc --init && nano tsconfig.json
tsconfig.json
{
"compilerOptions": {
"target": "ES2015",
"module": "commonJS",
"allowJs": true,
"checkJs": true,
"jsx": "preserve",
"outDir": "./public/assets/js/",
"removeComments": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true
}
}
모듈을 합치고 호환성을 위해 웹팩을 사용하도록 하겠습니다.
$ npm i webpack webpack-cli ts-loader
웹팩 설정파일을 작성합니다.
$ touch webpack.config.js && nano webpack.config.js
webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/main.ts",
mode: "none",
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
output: {
filename: "main.js",
path: path.resolve(__dirname, "public/assets/js"),
},
};
웹팩을 watch mode
로 실행하면 코드가 수정될 때마다 자동으로 컴파일 됩니다.
$ npx webpack -w
게임 생성
우선 메인 문서를 생성합시다. http-server는 index.html
를 메인 문서로 인식합니다.
$ touch public/index.html
다음과 같이 컴파일된 자바스크립트 파일을 추가해주세요.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="/assets/js/main.js"></script>
</head>
<body>
</body>
</html>
게임 인스턴스는 new Phaser.Game()
을 호출하여 생성할 수 있습니다. 타입스크립트 파일을 작성하겠습니다.
$ touch /src/main.ts && nano /src/main.ts
main.ts
import Phaser from 'phaser';
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: 0xfff,
};
new Phaser.Game(config);
Game
인스턴스는 매개변수로 GameConfig
객체를 받습니다. GameConfig 객체는 게임에 필요한 여러 설정값을 프로퍼티로 가지고 있습니다.
type
프로퍼티는 그래픽의 렌더링 방식을 설정합니다. 캔버스를 사용하는 Phaser.CANVAS
, WebGL을 사용하는 Phaser.WEBGL
, 자동설정인 Phaser.AUTO
중 하나를 입력합니다.
width
와 height
프로퍼티는 화면 크기를 설정합니다. number 타입이며, 픽셀 단위로 입력합니다.
backgroundColor
는 화면의 색상을 설정합니다. number 타입의 hex 색상코드 또는 string 타입의 rgb 색상코드를 입력합니다.
다양한 프로퍼티를 알고 싶다면 공식문서를 참고하세요.
씬(Scene) 생성
게임 객체의 동적인 작용을 위해서는 씬 객체를 생성해야 합니다. 씬 객체의 라이프 사이클은 다음과 같습니다.
mainScene.ts
class MainScene extends Phaser.Scene {
constructor() {
super({ key: 'main', active: true })
}
preload(): void {}
create(): void {}
update(time: number, delta: number): void {}
destroy(): void {}
}
Scene 클래스를 상속받아 각각 오버라이드 하면 됩니다.
preload()
메소드는 게임에 필요한 이미지나 오디오 파일 등의 리소스를 로드할 때 사용합니다. 게임에 필요한 리소스를 Asset
이라고 합니다. asset을 로드할 때 Scene 객체의 load
프로퍼티를 사용합니다. 일단 asset이 로드되면 Loader
객체는 키값을 통해 asset에 접근할 수 있도록 해줍니다.
첨부파일의 이미지를 로드하기위해 다음 코드를 작성하세요.
preload(): void {
this.load.image('sky', 'assets/image/sky.png');
this.load.image('ground', 'assets/image/platform.png');
this.load.image('star', 'assets/image/star.png');
this.load.image('bomb', 'assets/image/bomb.png');
this.load.spritesheet('dude', 'assets/image/dude.png', { frameWidth: 32, frameHeight: 48 });
}
하나의 이미지를 로드하려면 Sene.load.image
메소드를 호출합니다. 첫번째 파라미터로 키값을, 두번째 파라미터로 asset 경로를 입력합니다.
spriteSheet의 경우 Sene.load.spritesheet
메소드를 호출하여 불러올 수 있습니다. 역시 첫번째 파라미터로 키값을, 두번째 파라미터로 asset 경로를 받으며, 3번째 파라미터로 sprite 설정 객체를 받습니다. 하나의 프레임에 대한 넓이와 높이를 설정해줍니다.
로드한 asset은 create()
메소드에 Scene 객체의 add 프로퍼티를 통해 Scene에 추가할 수 있습니다.
create(): void {
this.add.image(400, 300, 'sky');
this.add.image(400, 300, 'star');
}
첫번째 파라미터는 x축 좌표, 두번째 파라미터는 y축 좌표, 세번째 파라미터는 asset의 키값에 해당합니다.
씬 코드를 작성했다면 게임 설정 객체의 scene 프로퍼티에 배열로 넣어주면 됩니다. 다음과 같이 작성하고 컴파일 후 localhost:8080
으로 들어가봅시다.
main.ts
import Phaser from 'phaser';
import { MainScene } from './mainScene';
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: 0xfff,
scene: [MainScene]
};
new Phaser.Game(config);
mainScene.ts
export class MainScene extends Phaser.Scene {
constructor() {
super({ key: 'main', active: true });
}
preload(): void {
this.load.image('sky', 'assets/image/sky.png');
this.load.image('ground', 'assets/image/platform.png');
this.load.image('star', 'assets/image/star.png');
this.load.image('bomb', 'assets/image/bomb.png');
this.load.spritesheet('dude', 'assets/image/dude.png', { frameWidth: 32, frameHeight: 48 });
}
create(): void {
this.add.image(400, 300, 'sky');
this.add.image(400, 300, 'star');
}
}
다음과 같이 배경과 별 이미지가 출력된 것을 확인할 수 있습니다.
물리객체 생성
대부분의 게임들은 물리 효과가 적용되어 있습니다. 예를 들어 캐릭터가 점프를 하면 중력의 영향을 받아 낙하 하거나 충돌시 작용 반작용 효과를 받습니다. 이런 효과를 구현하려면 많은 노력이 필요하겠지만 Phaser는 2가지 물리엔진 arcade
와 matter
를 포함하고 있어 손쉽게 물리 효과를 부여할 수 있습니다.
arcade
는 Phaser 개발자가 만든 간단한 물리 엔진입니다. 간단히 사용하기에 편하지만 복잡한 게임을 만들기에는 조금 부족합니다.
matter
는 상용화 된 물리 엔진입니다. 앵그리버드 물리 엔진으로 유명한 Box2d에 이어 자바스크립트에서 사용되는 양대 산맥의 물리 엔진으로 알려져 있습니다.
Phaser 튜토리얼에 따라 arcade 엔진으로 물리 객체를 생성해보겠습니다. 우선 게임 설정 객체에 physics 프로퍼티를 설정해주세요.
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: 0xfff,
scene: [MainScene],
physics: {
default: 'arcade',
arcade: {
gravity: { y: 300 },
debug: false
}
}
};
씬 객체의 create 메소드에 다음과 같이 물리 객체를 생성하는 코드를 작성해 주세요.
create(): void {
this.add.image(400, 300, 'sky');
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');
}
씬 객체에서 physics
프로퍼티는 arcade 물리 엔진을 의미합니다. 물리 객체에는 dynamic
객체와 static
객체가 있습니다. dynamic 객체는 중력이나 가속도의 영향을 받아 상호작용 할 수 있는 객체이고, static 객체는 벽과 같이 중력의 영향도 받지 않을 뿐더러 부딪쳐도 움직이지 않는 객체입니다.
위에 코드에서는 우선 지형을 static 객체로 만들어보았습니다. static 그룹 객체를 만들고 4개의 static 객체를 추가했습니다. create 메소드에서 첫번째 파라미터는 x축 좌표, 두번째 파라미터는 y축 좌표, 세번째 파라미터는 asset의 키값입니다.
캐릭터 생성
이제 지형 위를 움직일 캐릭터를 생성해 보겠습니다.
다음 코드를 create 메소드에 추가해주세요.
const player = this.physics.add.sprite(100, 450, 'dude').setName("player");
player.setBounce(0.2);
player.setCollideWorldBounds(true);
this.anims.create({
key: 'left',
frames: this.anims.generateFrameNumbers('dude', { start: 0, end: 3 }),
frameRate: 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
});
this.physics.add.sprite()
메소드를 통해 캐릭터의 sprite를 추가했습니다. sprite는 2d 이미지 합성을 통해 애니메이션 효과를 내는 기술입니다. 움직임이 있는 이미지라고 보시면 됩니다. setName
함수를 통해 나중에 호출할 수 있도록 이름을 설정합니다.
Physics Game Object Factory
에 의해 생성된 sprite는 기본적으로 dynamic 객체가 됩니다. 플레이어 객체에 setBounce()
를 통해 바운스를 설정하면 충돌시 작용 반작용 효과를 나타낼 수 있습니다. 또한 setCollideWorldBounds()
를 통해 WoldBounds를 true로 설정하면 화면 경계에서 충돌 효과가 나타납니다. setGravityY()
를 통해서는 중력의 크기를 설정할 수 있습니다.
Scene 객체의 anims.create()
메소드를 통해서는 애니메이션 객체를 만들 수 있습니다. key
는 애니메이션 객체의 키가 되고 frames
는 프레임을, frameRate
는 fps를 나타냅니다. repeat
는 애니메이션의 반복수를 나타내는데 -1은 무한을 의미합니다.
지형 객체는 static 객체이기 때문에 dinamic 객체인 캐릭터와 상호작용하기 위해서는 collider
객체로 충돌을 설정해야 합니다. 다음 코드를 씬 객체의 create 메소드에 추가해주세요.
this.physics.add.collider(player, platforms);
캐릭터 이동
이제 키보드를 통해 캐릭터를 이동할 수 있도록 해보겠습니다.
update 메소드에 다음 코드를 작성합니다. update 메소드는 씬 객체가 존재하는 동안 주기적으로 실행되는 메소드 입니다.
update(_time: number, _delta: number): void {
const cursors = this.input.keyboard.createCursorKeys();
const player = this.children.getByName("player") as Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
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.setVelocityX(0);
player.anims.play('turn');
}
if (cursors.up.isDown && player.body.touching.down)
{
player.setVelocityY(-330);
}
}
의미는 알기 쉬울 것입니다. 주기적으로 해당 키보드를 누르고 있는지 체크하여 속도를 설정하고 키에 해당하는 애니메이션을 실행합니다.
별 모으기
이제 별을 모으는 코드를 작성해보겠습니다. 우선 별을 static 객체로 생성합니다. 다음 코드를 create 메소드에 추가합니다.
let stars = this.physics.add.group({
key: 'star',
repeat: 11,
setXY: { x: 12, y: 0, stepX: 70 }
});
this.data.set("stars", stars);
stars.children.iterate(function (child) {
(child as Phaser.Physics.Arcade.Image).setBounceY(Phaser.Math.FloatBetween(0.4, 0.8));
});
static 그룹 객체를 만들고 12개의 별 객체를 만듭니다. 그룹 객체를 만들때 처음 1개는 자동으로 생성되므로 11번 반복하여 12개의 별 객체가 생성됩니다. x축 좌표는 12픽셀부터 시작하여 70픽셀 간격으로 생성됩니다. iterate 메소드를 통해 순차적으로 Y축 바운스를 설정해줍니다.
static 객체인 지형과 별 그룹 객체가 상호작용 하도록 다음 코드를 create 메소드에 추가합니다.
this.physics.add.collider(stars, platforms);
캐릭터가 별을 수집하면 별 객체가 사라지도록 설정해줍시다. 이것은 arcade 물리 객체의 overlap 메소드를 통해 가능합니다. 다음 코드를 create 메소드에 추가해 주세요.
this.physics.add.overlap(player, stars, this.collectStar, undefined, this);
player 객체가 stars 객체와 overlap 되면 collectStar 메소드를 호출합니다. 해당 메소드를 작성해주세요.
collectStar (_player: Phaser.Types.Physics.Arcade.GameObjectWithBody, star: Phaser.Types.Physics.Arcade.GameObjectWithBody): void {
(star as Phaser.Physics.Arcade.Image).disableBody(true, true);
}
star 객체의 disableBody 함수를 호출하면 해당 객체가 사라집니다.
점수 설정
별을 모으면 점수가 증가 하도록 하겠습니다. 우선 create 메소드에 점수 데이터와 텍스트 asset을 추가해주세요.
this.data.set("score", 0);
this.add.text(16, 16, 'score: 0', { fontSize: '32px', backgroundColor: '#000' }).setName("scoreText");
다음으로 위에서 생성한 collectStar 메소드에 다음 코드를 추가합니다.
this.data.set("score", this.data.get("score") as number + 10);
(this.children.getByName("scoreText") as Phaser.GameObjects.Text).setText('Score: ' + (this.data.get("score") as number));
장애물 설정
마지막으로 장애물을 설정해 보겠습니다. 별을 모두 수집하면 폭탄이 하나 나오고 새로운 별 그룹 객체가 생성됩니다. create 메소드에 다음 코드를 추가합니다.
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);
폭탄의 그룹인 dynamic 그룹 객체를 생성하여 collider 객체로 충돌을 설정합니다. 또한 플레이어가 폭탄에 닿으면 hitBomb 메소드를 호출합니다.
hitBomb 메소드는 다음과 같이 작성합니다.
hitBomb (player: Phaser.Types.Physics.Arcade.GameObjectWithBody, _bomb: Phaser.Types.Physics.Arcade.GameObjectWithBody): void {
this.physics.pause();
(player as Phaser.Types.Physics.Arcade.SpriteWithDynamicBody).setTint(0xff0000);
(player as Phaser.Types.Physics.Arcade.SpriteWithDynamicBody).anims.play('turn');
}
폭탄에 닿으면 물리 객체의 상호작용이 정지되고 플레이어 캐릭터의 색이 빨같게 됩니다.
마지막으로 collectStar 메소드에 다음 코드를 추가합니다.
const stars = this.data.get("stars") as Phaser.Physics.Arcade.Group;
if (stars.countActive(true) === 0) {
stars.children.iterate(function (child) {
(child as Phaser.Physics.Arcade.Image).enableBody(true, (child as Phaser.Physics.Arcade.Image).x, 0, true, true);
});
const x = ((player as Phaser.Types.Physics.Arcade.SpriteWithDynamicBody).x < 400) ? Phaser.Math.Between(400, 800) : Phaser.Math.Between(0, 400);
const bombs = this.data.get("bombs") as Phaser.Physics.Arcade.Group;
const bomb = bombs.create(x, 16, 'bomb');
bomb.setBounce(1);
bomb.setCollideWorldBounds(true);
bomb.setVelocity(Phaser.Math.Between(-200, 200), 20);
}
별 객체가 하나도 없다면 새로운 별 객체 그룹을 생성하고 폭탄을 하나 생성하도록 합니다. 폭탄에 각각 바운스와 월드 바운스, 속도를 설정합니다.