본 포스트에서는 간단히 Phaser 3 의 기초를 학습하기 위하여 벽돌깨기 게임을 만들어 보도록 하겠습니다. 기본이 되는 내용은 2D breakout game using Phaser 을 바탕으로 진행하였으며, 원본의 코드는 Phaser 2 (CE) 버전을 기반으로 하고 있어, 여기서는 Phaser 3 에서 동작할 수 있도록 소스코드를 수정하였습니다.
사전작업
- Phaser 를 사용할 수 있도록 환경을 구성합니다.
- 혹은 다음의 Stackblitz 링크에서 시작 합니다.
프레임 워크 초기화
게임의 기능을 삽입하기 전에 우선 전체적인 구조를 잡아주어야 합니다. Phaser 는 장면(scene) 단위로 게임을 관리합니다. 때문에 우선 장면(scene) 클래스를 생성한 후 이를 Angular 에 추가 및 실행 하겠습니다.
▼ 장면(scene) 클래스
export class MyScene extends Phaser.Scene {
constructor() {
super({
key: 'Scene',
});
}
public preload() {
}
public create() {
}
public update() {
}
}
▼ 새로운 장면(scene) phaser 에 추가하기
ngOnInit() {
this.game = new Phaser.Game(this.gameConfig);
this.game.scene.add('main', new MyScene(), true);
}
Scene 클래스의 preload
는 애셋(assets)을 사전에 로딩하는데 사용하며, create
는 모든 애셋을 로드하고 게임을 실행할 준비가 완료되었을 때 한번 실행됩니다. 마지막으로 update
는 매 프레임마다 실행될 내용을 작성하게 됩니다. 이들은 phaser 가 자동으로 호출을 해주기 때문에 우리는 이들 함수의 성격에 따라서 내용을 작성해주면 됩니다.
지금까지 작성한 전체 코드는 다음과 같습니다.
▼ src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
export class MyScene extends Phaser.Scene {
constructor() {
super({
key: 'Scene',
});
}
public preload() {
}
public create() {
}
public update() {
}
}
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
name = 'Angular';
public game: Phaser.Game;
public readonly gameConfig: GameConfig = {
title: "Phaser Running Test @ Angular",
version: "0.0.1",
type: Phaser.AUTO,
width: 480,
height: 320,
backgroundColor : '#eee',
physics: {
default : 'arcade',
arcade: {
debug: false,
setBounds: false
}
},
parent: null,
callbacks: {
postBoot: function(game) {
game.canvas.style.width = '100%';
game.canvas.style.height = '100%';
}
}
}
ngOnInit() {
this.game = new Phaser.Game(this.gameConfig);
this.game.scene.add('main', new MyScene(), true);
}
}
애셋(assets) 불러오고 화면에 출력하기
게임 애셋은 게임내에 삽입하여 게임엔진을 통해 사용할 모든 요소를 가르키는 용어입니다. 우선 간단히 공 이미지를 Phaser 로 불러들여와 이를 화면에 출력해보도록 하겠습니다.
preload()
함수내에 load.image()
메서드를 추가합니다. 하지만 그에 앞서 우리는 로컬 서버에서 해당 파일을 로드하는 것이 아니므로 load.baseURL
을 이용하여 경로를 이미지 파일의 기본 주소를 변경해줍니다.
▼ 이미지 파일 로드하기
... 생략 ...
ball:Phaser.GameObjects.Sprite;
public preload() {
this.load.crossOrigin = 'anonymous';
this.load.baseURL = 'https://end3r.github.io/Gamedev-Phaser-Content-Kit/demos/';
this.load.image('ball', 'img/ball.png');
}
... 생략 ...
다음은 로딩한 이미지를 화면에 출력합니다. 이 때는 add.sprite()
메서드를 사용합니다. 이 메서드의 첫 두 개의 파라메터는 x, y 좌표를 의미하여, 다음 파라메터는 앞서 이미지 에셋 로딩시 해당 이미지에 부여하였던 이름입니다.
▼ 이미지 화면에 출력하기
public create() {
this.ball = this.add.sprite(50, 50, 'ball');
}
이제 화면에 하늘색 공이 출력되는 것을 확인할 수 있습니다.
공 움직이기
앞선 단계에서 이미지를 로딩하여 화면에 출력해 보았습니다. 이번에는 공을 움직여보도록 하겠습니다. 앞서 작성하였던 update()
함수는 Phaser 가 자동으로 매 프레임(Frame) 마다 호출하는 함수로 여기에 프레임별로 게임내에서 반복 수행할 작업을 작성하면 됩니다. 이곳에서 공의 x, y 좌표를 각각 1씩 증가시켰습니다.
▼ 공의 위치 변경하기
public update() {
this.ball.x += 1;
this.ball.y += 1;
}
웹브라우저를 다시 불러들여 게임을 실행하면 공의 위치가 매 프레임마다 오른쪽(x좌표) 아래(y좌표)로 변경되어 화면에 표시되며, 이를 통해 결국 공이 이동하는 것처럼 보이는 것을 확인 할 수 있습니다.
물리법칙
앞서 로딩한 공 이미지에 Phaser
에서 제공하는 물리법칙이 적용되도록 설정하겠습니다. 앞서 작성하였더코드를 다음과 같이 변경합니다. 우선 ball
변수의 type 을 물리법칙이 적용되도록 변경합니다.
▼ 물리법칙 객체로 변경하기
// ▼ 수정
// ball:Phaser.GameObjects.Sprites;
ball:Phaser.Physics.Arcade.Sprite;
이제 물리법칙이 적용된 sprite 로 다시 화면에 추가합니다.
public create() {
// ▼ 수정
// this.ball = this.add.sprite(50, 50, 'ball');
this.ball = this.physics.add.sprite(50, 50, 'ball');
}
이 때 앞서 update()
함수에 작성하였던 위치값을 변경하는 코드는 삭제하여 물리법칙이 우리의 상식(자유낙하)처럼 게임화면상에서 적용되도록 합니다.
public update() {
// ▼ 삭제
}
이제 공은 앞서 이미지로 추가했을 때와 달리 Phaser 에서 제공해주는 물리법칙에 따라 중력방향으로 낙하하게 됩니다. 이번에는 여기에 초기 속도값을 넣어서 공을 좀 더 빠르게 이동시켜 보도록 하겠습니다.
▼ 공에 초기속도 추가
public create() {
// this.ball = this.add.sprite(50, 50, 'ball');
this.ball = this.physics.add.sprite(50, 50, 'ball');
// ▼ 수정
this.ball.body.velocity.set(150, 150);
}
공에 오른쪽 아래 방향으로 초기 속도를 주었기 때문에 이번에는 공이 오른쪽 대각선으로 낙하하는 것을 확인할 수 있습니다.
벽에 공 튕기기
물리 법칙이 적용된 덕분에 게임 화면이 호출되면 공은 중력에 의해 낙하하기 시작합니다. 하지만 공이 우리 시야에서 벗어나 게임화면 밖으로 이동하게 되어 이후의 모습을 볼 수 없으므로, 공이 게임화면밖으로 빠져나가지 못하도록 설정하겠습니다.
▼ 화면에 경계 생성하기
public create() {
// this.ball = this.add.sprite(50, 50, 'ball');
this.ball = this.physics.add.sprite(50, 50, 'ball');
this.ball.body.velocity.set(150, 150);
// ▼ 추가
this.ball.body.collideWorldBounds = true;
}
이제 공이 화면밖으로벗어나지 않게 되었지만, 벽에 부딛힌 후 튕기지 않고 그대로 오른쪽 하단에 머무르게 되었습니다.
다음과 같이 공이 튕기도록 설정해보도록 하겠습니다. 여기서 입력파라메터 1
은 공이 튀면서 잃게 되는 속도의 비율을 의미합니다. 즉 이 경우는 벽에 부딛힌 후 속도가 줄지 않고 방향만 바뀌게 됩니다.
▼ 공이 화면과 충돌후 속도를 잃지 않도록 설정하기
public create() {
// this.ball = this.add.sprite(50, 50, 'ball');
this.ball = this.physics.add.sprite(50, 50, 'ball');
this.ball.body.velocity.set(150, 150);
this.ball.body.collideWorldBounds = true;
// ▼ 추가
this.ball.body.bounce.set(1);
}
프레이어 패들(Paddle)과 제어
현재까지 작성한 게임은 비록 공이 움직이고 있기는 하지만 플레이어와의 상호작용 요소가 없습니다. 이제부터는 사용자의 제어에 따라 움직이는 요소를 추가해보도록 하겠습니다.
앞서 공을 추가했던 것과 동일한 방법으로 플레이어 패들(Paddle)을 추가합니다. 이 패들(Paddle)은 플레이어의 조종에 따라 움직이며, 이후 낙하하는 공이 화면 밖으로 나가지 못하고 튕겨 올라갈 수 있는 발판 역할을 할 것입니다.
▼ 플레이어가 제어할 수 있는 패들 추가하기
... 생략 ...
// ▼ 추가
paddle:Phaser.Physics.Arcade.Sprite;
public preload() {
this.load.crossOrigin = 'anonymous';
this.load.baseURL = 'https://end3r.github.io/Gamedev-Phaser-Content-Kit/demos/';
this.load.image('ball', 'img/ball.png');
// ▼ 추가
this.load.image('paddle', 'img/paddle.png');
}
public create() {
// this.ball = this.add.sprite(50, 50, 'ball');
this.ball = this.physics.add.sprite(50, 50, 'ball');
this.ball.body.velocity.set(150, 150);
this.ball.body.collideWorldBounds = true;
this.ball.body.bounce.set(1);
// ▼ 추가
this.paddle = this.physics.add.sprite(this.cameras.main.width*0.5, this.cameras.main.height-5, 'paddle');
}
화면이 로드되면 패들이 게임화면 중앙(this.cameras.main.width*0.5
) 하단(this.cameras.main.height-5
)에 생성되는 것을 확인할 수 있습니다.
단, 공과 패들이 상호작용 없이 각각 낙하하는 것을 확인할 수 있습니다. 따라서 이번에는 공과 패들이 서로 부딛힐 수 있도록 설정하겠습니다.
▼ 공과 패들이 서로 충돌하게 설정하기
public create() {
... 생략 ...
this.physics.add.collider(this.ball, this.paddle);
}
이번에는 패들이 화면밖으로 사라지는 것을 수정해보도록 하겠습니다. 우리가 원하는 것은 공이 충돌하는 순간 패들은 고정되어 있으며 공만 튕기도록 하는 것이기 때문에 이를 설정해보도록 하겠습니다.
▼ 패들이 움직이지 않도록 설정하기
public create() {
... 생략 ...
this.physics.add.collider(this.ball, this.paddle);
// ▼ 추가
this.paddle.body.allowGravity = false;
this.paddle.body.immovable = true;
}
이제 패들이 화면밖으로 추락하지 않으므로 안심하고 플레이어의 입력에 따라 패들을 좌우로 이동시켜보도록 하겠습니다.
▼ 패들의 위치를 사용자의 입력에 따라 설정하기
function update() {
// ▼ 추가
this.paddle.x = this.game.input.activePointer.x;
}
이제 매 프레임마다 패들의 x
좌표가 입력장치(예를 들어 마우스)의 x
좌표에 맞추어 조정되는 것을 볼 수 있습니다. 하지만 우리가 게임을 시작할 때 이 패들의 위치가 의도와 달리 화면의 중앙에 위치하지 않는 것을 확인할 수 있습니다. 이것은 입력장치가 그 시점에서 정의되지 않았기 때문인데 이에 대해 기본값을 화면의 중앙으로 설정하여 문제를 고칠 수 있습니다.
앞서 작성한 코드를 다음과 같이 수정합니다.
▼ 패들의 위치 초기값 재설정하기
function update() {
this.physics.add.collider(this.ball, this.paddle);
// ▼ 수정
this.paddle.x = this.game.input.activePointer.x || this.cameras.main.width*0.5;
}
이제 우리가 예측한대로 게임 화면이 로딩되면 플레이어는 화면 중앙에 표시되고 이후 입력장치를 이동함에따라 플레이어가 입력장치를 따라다닙니다.
이번에는 플레이어의 초기위치를 화면 중앙으로 설정한 것처럼 공의 초기 위치도 화면 중앙으로 이동하겠습니다. 패들의 위치를 설정한 것과 동일한 방법으로 x, y 좌표와 함께 생성합니다.
public create() {
this.ball = game.add.sprite(this.cameras.main.width*0.5, this.cameras.main.height-25, 'ball');
}
이번에는 공의 속도값을 변경해보도록 하겠습니다. 앞서 설정하였던 코드에서 두번째 파라메터의 값을 변경합니다.
▼ 공의 속도 변경하기
public create() {
this.ball.body.velocity.set(150, -250);
}
이제 게임의 시작시 공은 시작시 화면의 중앙에서부터 오른쪽 위로 이동합니다. 즉, -
부호를 이용하여 방향을 설정할 수 있습니다.
게임 종료
현재는 게임 화면의 사방에 가상의 벽이 존재하여 공이 그 밖으로 이동할 수 없게 설정(this.ball.body.collideWorldBounds = true;
) 되어 있습니다. 게임을 종료하기 위하여 화면 하단과 공이 충돌하는 것을 검출하고 그 경우 게임을 종료해보도록 하겠습니다. create()
함수를 다음과 같이 수정합니다.
▼ 게임 종료 조건 설정하기
public create() {
... 생략 ...
// ▼ 추가
this.physics.world.checkCollision.down = true;
this.ball.body.onWorldBounds = true;
this.physics.world.on('worldbounds', function(body, blockedUp, blockedDown, blockedLeft, blockedRight) {
if (blockedDown == true) {
alert('Game over!');
location.reload();
}
}, this);
}
코드를 실행해보면, 공이 바닥에 부딛힐 때, 이벤트가 발생하며 게임이 재시작하는 것을 확인할 수 있습니다.
벽돌 추가하기
이제 공을 이용하여 없앨 수 있는 벽돌들을 추가해보도록 하겠습니다. 이 부분은 앞서 공이나 플레이어를 추가한것과는 달리 동일한 성격의 객체를 여러개 추가하는 작업으로 Phaser 의 그룹 기능을 사용하여 추가합니다.
비록 Phaser 에서 제공하는 기능을 사용하더라도 여러개의 벽돌을 동시에 생성하기 때문에 내용이 길어지게 되므로 별도의 함수 initBricks()
로 작성하고 이를 create()
함수에서 호출하여 사용합니다.
▼ 벽돌정보를 저장할 변수 추가
ball:Phaser.Physics.Arcade.Sprite;
paddle:Phaser.Physics.Arcade.Sprite;
// ▼ 추가
bricks:Phaser.Physics.Arcade.Group;
brickInfo:any;
▼ 벽돌 이미지 로딩
public preload() {
.. 생략 ..
this.load.image('brick', 'img/brick.png');
}
▼ 벽돌밭 생성
public create() {
.. 생략 ..
this.initBricks();
}
private initBricks() {
this.brickInfo = {
width: 50,
height: 20,
count: {
row: 3,
col: 7
},
offset: {
top: 50,
left: 60
},
padding: 10
};
this.bricks = this.physics.add.group({allowGravity: false,
immovable: true});
for (var c=0;c<this.brickInfo.count.col;c++) {
for (var r=0;r<this.brickInfo.count.row;r++) {
var brickX = (c*(this.brickInfo.width+this.brickInfo.padding))+this.brickInfo.offset.left;
var brickY = (r*(this.brickInfo.height+this.brickInfo.padding))+this.brickInfo.offset.top;
this.bricks.create(brickX, brickY, 'brick');
}
}
}
이제 21개의 벽돌이 게임화면상에 추가된 것을 확인할 수 있습니다.
충돌감지 추가하기
이번에는 앞서 추가한 벽돌들과 공사이의 충돌을 검출해보도록 하겠습니다. 다행이도 Phaser 는 공이나 패들과 같은 하나의 객체간의 충돌뿐만 아니라 여러개의 객체로 구성된 그룹과 다른 객체간의 충돌감지도 지원하기 때문에 앞서 수행하였던 공과 패들간의 충돌설정과 같이 간단히 구현할 수 있습니다.
앞서와 마찬가지로 create()
함수에 충돌을 감지할 두 대상을 입력 파라메터로 하여 설정합니다. 이를 통해 Phaser 는 매 프레임 마다 충돌의 발생을 확인하게 됩니다. 그리고 충돌이 검출되었을 때, 수행할 작업을 등록하기 위하여 initBricks()
함수 아래에 fnBallHitBrick()
함수를 추가합니다. 이 때 주의할 점은 두개의 입력인자로 삽입하는 객체가 생성된 후에 충돌대상으로 설정해야 하기 때문에 코드를 가장 하단에 넣어야 합니다.
▼ 공과 블록간의 충돌감지 설정하기
public create() {
.. 생략 ..
this.initBricks();
this.physics.add.collider(this.ball, this.paddle);
// ▼ 추가
this.physics.add.collider(this.ball, this.bricks, this.fnBallHitBrick);
}
.. 생략 ..
private initBricks() {
.. 생략 ..
}
private fnBallHitBrick(ball:Phaser.GameObjects.GameObject, brick:Phaser.GameObjects.GameObject) {
}
우리가 만들어보고 있는 게임의 목적은 공을 사용하여 블록과 충돌 시키고 이를 통해 모든 블록을 없애는 것입니다. 따라서 공과 블록간의 충돌이 발생하면 블록을 제거 합니다. Phaser 는 충돌이 발생한 두 개의 객체를 함수에서 사용할 수 있도록 인자로 제공해주기 때문에 이렇게 입력받은 인자 중, 블록을 없애기만 하면 간단히 원하는 결과를 얻을 수 있습니다.
▼ 공과 블록간의 충돌시 수행할 내용 (블록제거)
private fnBallHitBrick(ball:Phaser.GameObjects.GameObject, brick:Phaser.GameObjects.GameObject) {
// ▼ 추가
brick.destroy();
}
점수표시
블록을 제거하여 플레이어가 얻는 점수를 화면에 표시하겠습니다. 여기서는 두 개의 변수를 추가합니다. 점수를 화면에 표시할 문자로 저장한 변수와 계산을 위한 정수형 변수입니다.
▼ 점수표시를 위한 변수 추가
scoreText: Phaser.GameObjects.Text;
score:number = 0;
다음 코드를 create()
함수의 가장 하단에 추가 합니다.
글자표시하기
create() {
// .. 생략 ..
this.scoreText = this.add.text(5, 5, 'Points: 0', { font: '18px Arial', fill: '#0095DD' });
}
text()
메서드에 설정한 입력인자는 문장을 표시할 좌표(x
,y
), 표시할 문장, 그리고 스타일입니다. 따라서 ‘Points: 0’ 이라는 문장이 화면 좌측 상단(5,5)에 18포인트 크기의 파란색글씨로 추가된 것을 확인할 수 있습니다.
이제 공이 블록과 충돌하여 블록이 없어질 때, 이 문장을 갱신해보도록 하겠습니다. 앞서 공이 블록과 충돌할 때마다 fnBallHitBrick()
함수가 호출되는 것을 기억할 것입니다. 따라서 이 부분에서 점수를 갱신 합니다.
※ 앞서 작성하였던 fnBallHitBrick
함수의 형식이 화살표 형식으로 재작성된 것에 주의해야 합니다.
▼ 점수 증가하기
private fnBallHitBrick = (ball:Phaser.GameObjects.GameObject, brick:Phaser.GameObjects.GameObject) => {
// console.log(this.ball.body.velocity);
brick.destroy();
// ▼ 추가
this.score += 10;
this.scoreText.setText('Points: '+this.score);
}
이제 공이 블록과 충돌할 때마다 해당 함수가 호출되며, 이를 통해 블록이 파괴되고, 점수가 10점씩 증가하며 이것이 화면에 출력됩니다.
게임종료 (승리)
앞서 공이 바닥에 닿을 때 게임에서 패하게 되며 다시 재시작되었습니다. 이번에는 공을 이용하여 모든 블록을 파괴했을 때, 게임에서 승리하며 종료되도록 설정해보도록 하겠습니다. 앞서 점수를 계산했던 것과 마찬가지로 블록과 공이 충돌할 때마다 남은 블록의 갯수를 확인하고 더 이상 블록이 없을 때, 게임을 종료하겠습니다.
▼ 게임의 종료 - 승리
fnBallHitBrick = (ball:Phaser.GameObjects.GameObject, brick:Phaser.GameObjects.GameObject) => {
// console.log(this.ball.body.velocity);
brick.destroy();
this.score += 10;
this.scoreText.setText('Points: '+this.score);
// ▼ 추가
if (this.bricks.countActive(true) == 0) {
alert('You won the game, congratulations!');
location.reload();
}
}
추가 목숨
현재 게임은 공이 바닥에 닿으면 즉시 종료됩니다. 우리는 여기에 플레이어에게 2회의 추가기회를 제공하여 총 3번 공이 바닥에 닿으면 게임이 종료되도록 변경해겠습니다.
앞서 점수를 표시하던 것과 동일하게 플레이어의 게임 기회를 저장할 변수와 이를 화면에 표시할 수 있도록 2개의 변수를 추가 합니다. 그리고 기회를 읽게 되었을 때 표시할 문구를 저장할 변수를 1개 추가 합니다.
▼ 추가목숨을 저장할 변수
lives:number = 3;
livesText:Phaser.GameObjects.Text;
lifeLostText:Phaser.GameObjects.Text;
화면에 문자를 출력합니다. create()
함수 내부에 아래의 내용을 추가합니다.
▼ 화면에 문구 추가하기
this.livesText = this.add.text(this.cameras.main.width-50, 5, 'Lives: '+this.lives, { font: '18px Arial', fill: '#0095DD' });
this.livesText.setOrigin(1,0);
this.lifeLostText = this.add.text(this.cameras.main.width*0.5, this.cameras.main.height*0.5, 'Life lost, click to continue', { font: '18px Arial', fill: '#0095DD' });
this.lifeLostText.setOrigin(0.5, 0.5);
this.lifeLostText.visible = false;
이제 화면의 우측상단에 플레이어에게 주어진 기회가 표시되는 것을 확인 할 수 있습니다. 다음은 공이 화면의 하단에 충돌하였을 때, 게임종료가 발생하던 것을 주석처리한 후, 주어진 목숨의 여부에 따라 화면을 로딩할지 결정하는 함수를 호출하도록 변경 합니다.
▼ 공과 화면이 충돌할 때 함수 호출
this.physics.world.on('worldbounds', function(body, blockedUp, blockedDown, blockedLeft, blockedRight) {
if (blockedDown == true) {
this.fnBallLeaveScreen();
// alert('Game over!');
// location.reload();
}
}, this);
▼ 화면과 충돌시 여분의 기회에 따라 재시작 설정하기
private fnBallLeaveScreen = () => {
this.lives--;
if(this.lives) {
this.livesText.setText('Lives: '+this.lives);
this.lifeLostText.visible = true;
this.ball.setPosition(this.cameras.main.width*0.5, this.cameras.main.height-25);
this.paddle.setPosition(this.cameras.main.width*0.5, this.cameras.main.height-5);
this.ball.body.velocity.set(0, 0);
this.ball.body.allowGravity = false;
this.input.once('pointerdown', function (pointer) {
this.lifeLostText.visible = false;
this.ball.body.velocity.set(150, -250);
this.ball.body.allowGravity = true;
}, this);
} else {
alert('You lost, game over!');
location.reload();
}
}
에니메이션과 트윈(tweens)
이번에는 게임의 화면효과를 풍부하게 할 수 있는 에니메이션을 추가해보도록 합니다.
공이 패들 및 블록과 충돌할 때 공의 탄성이 느껴질 수 있도록 공에 에니메이션 효과를 추가 하겠습니다. 또한 블록이 파괴되기 전에 크기가 자연스럽게 줄어드는 애니메이션 효과도 추가해봅니다.
우선, 공의 이미지를 교체 하겠습니다. 공이 튕겼을 때 찌그러지는 모양을 포함하고 있는 이미지를 사용 합니다. 이때는 단순히 하나의 이미지가 아닌 하나의 이미지를 쪼개어 frame 으로 인식할 수 있도록 spritesheet()
메서드를 사용합니다. 이 때 image()
메서드와 달리 하나의 프레임의 너비와 높이를 표시하는 두 개의 추가 파라메터를 넣어 줍니다.
▼ 전체 이미지 로드
public preload() {
this.load.crossOrigin = 'anonymous';
this.load.baseURL = 'https://end3r.github.io/Gamedev-Phaser-Content-Kit/demos/';
// ▼ 제거
// this.load.image('ball', 'img/ball.png');
this.load.image('paddle', 'img/paddle.png');
this.load.image('brick', 'img/brick.png');
// ▼ 추가
this.load.spritesheet('ball', 'img/wobble.png', { frameWidth: 20, frameHeight: 20 });
}
다음은 create() 함수에 에니메이션 효과를 생성합니다. 또한 공과 패들 충돌 이벤트 발생시 에니메이션을 실행할 함수를 추가합니다.
▼ 이벤트 발생시 재생할 에니메이션 생성
public create() {
// this.ball = this.add.sprite(50, 50, 'ball');
this.ball = this.physics.add.sprite(50, 250, 'ball');
// ▼ 추가
this.anims.create({
key: 'wobble',
frames: this.anims.generateFrameNumbers('ball', { frames: [0,1,0,2,0,1,0,2,0]}),
frameRate : 24,
});
.. 생략 ..
// ▼ 수정
this.physics.add.collider(this.ball, this.paddle, this.fnBallHitPaddle);
이제 공이 패들과 충돌할 때 마다 fnBallHitPaddle()
이 호출됩니다. 여기에서 앞서 만들어 놓은 애니메이션 효과를 재생합니다.
▼ 공과 패들 충돌시 에니메이션 재생
private fnBallHitPaddle = (ball:Phaser.GameObjects.GameObject, paddle:Phaser.GameObjects.GameObject) => {
this.ball.anims.play('wobble');
}
마찬가지로 블럭과 공이 충돌할 때도 에니메이션 효과를 재생합니다.
▼ 공과 블록 충돌시 에니메이션 재생
fnBallHitBrick = (ball:Phaser.GameObjects.GameObject, brick:Phaser.GameObjects.GameObject) => {
// console.log(this.ball.body.velocity);
brick.destroy();
// ▼ 추가
this.ball.anims.play('wobble');
this.score += 10;
this.scoreText.setText('Points: '+this.score);
if (this.bricks.countActive(true).size == 0) {
alert('You won the game, congratulations!');
location.reload();
}
}
이제 공이 다른 물체들과 충돌할 때 마다 조금 찌그러졌다 원래대로 돌아오는 에니메이션이 게임에 추가되었습니다.
지금까지 추가한 에니메이션이 외부 이미지를 재생하는 것이었다면 트윈은 게임내의 객체의 속성을 자연스럽게 변화를 줍니다. 이를 이용하여 벽돌 공에 충돌이 발생하면 벽돌의 크기가 자연스럽게 줄어들면서 제거되도록 합니다.
▼ 벽돌 파괴 트윈 생성
fnBallHitBrick = (ball:Phaser.GameObjects.GameObject, brick:Phaser.GameObjects.GameObject) => {
// ▼ 제거
// console.log(this.ball.body.velocity);
// ▼ 추가
this.add.tween({
targets : brick,
scaleX: 0.5,
scaleY: 0.5,
ease: 'linear',
duration : 200,
repeat: 0,
yoyo: false,
onComplete: () => {
brick.destroy();
},
onCompleteScope: this
});
// brick.destroy();
this.ball.anims.play('wobble');
this.score += 10;
this.scoreText.setText('Points: '+this.score);
if (this.bricks.countActive(true) == 0) {
alert('You won the game, congratulations!');
location.reload();
}
}
버튼
화면에 게임이 로딩되면 바로 시작하는 것 보다는 플레이어가 준비가 되어 버튼을 누르면 게임을 시작하는 것이 조금더 친절한 방법일 것입니다. 이번에는 화면에 버튼을 누를 때 게임이 시작되도록 해보겠습니다.
게임이 현재 진행중인지를 나타낼 변수와 버튼을 화면에 표시할 것인지를 저장할 수 있는 변수 두개를 추가 합니다.
playing:boolean = false;
startButton:Phaser.GameObjects.Sprite;
버튼의 이미지 역시 사용자가 클릭하였을 때 이미지의 변화를 줄 수 있도록 spritesheet 형식으로 추가하도록 합니다. preload()
함수에서 다음과 같이 버튼이미지를 로딩합니다.
▼ 버튼 이미지 추가
public preload() {
this.load.crossOrigin = 'anonymous';
this.load.baseURL = 'https://end3r.github.io/Gamedev-Phaser-Content-Kit/demos/';
// this.load.image('ball', 'img/ball.png');
this.load.image('paddle', 'img/paddle.png');
this.load.image('brick', 'img/brick.png');
this.load.spritesheet('ball', 'img/wobble.png', { frameWidth: 20, frameHeight: 20 });
// ▼ 추가
this.load.spritesheet('button', 'img/button.png', { frameWidth: 120, frameHeight: 40 });
}
이제 화면에 버튼을 추가합니다.
▼ 버튼 생성 및 사용자가 조작할 수 있게 설정
public create() {
.. 생략 ..
this.startButton = this.add.sprite(this.cameras.main.width*0.5, this.cameras.main.height*0.5, 'button', ).setInteractive();
this.startButton.setOrigin(0.5,0.5);
}
다음은 버튼에 발생할 수 있는 이벤트로 사용자가 마우스를 버튼위로 올리거나 클릭하였을 때, 프레임을 교체합니다. 그리고 이제 게임이 로딩되면서 create()
함수내에서 공의 속도를 설정하는 부분(this.ball.body.velocity.set(150, -250)
)을 주석처리하고 대신 공이 버튼을 클릭하기 전까지는 낙하하지 않도록 설정(this.ball.body.allowGravity = false;
)합니다. 버튼을 클릭하면 해당버튼은 제거한 후, 공의 속도를 변경합니다.
▼ 버튼의 이벤트 추가
public create() {
.. 생략 ..
// this.ball.body.velocity.set(150, -250);
this.ball.body.allowGravity = false;
.. 생략 ..
this.startButton = this.add.sprite(this.cameras.main.width*0.5, this.cameras.main.height*0.5, 'button', ).setInteractive();
this.startButton.setOrigin(0.5,0.5);
this.startButton.on('pointerover', () => {
this.startButton.setFrame(1);
});
this.startButton.on('pointerout', () => {
this.startButton.setFrame(0);
});
this.startButton.once('pointerdown', () => {
this.startButton.setFrame(2);
this.startButton.destroy();
this.ball.body.velocity.set(150, -250);
this.playing = true;
this.ball.body.allowGravity = true;
})
}
게임플레이 변화주기
이제 게임이 모두 완성되었습니다. 하지만 자세히 살펴보면 공이 항상 동일한 각도로 튕겨서 항상 동일한 패턴으로 게임이 진행된다는 것을 알 수 있습니다.
즉, 항상 동일한 게임을 진행하게 된다는 점입니다. 이번에는 이 점을 수정하겠습니다.
간단히 패들과 공이 충돌하여 튕길때 공과 패들이 충돌하는 위치에 따라 공의 x
축 속도에 변화를 줍니다.
▼ 공의 바운스 변화 추가
private fnBallHitPaddle = (ball:Phaser.GameObjects.GameObject, paddle:Phaser.GameObjects.GameObject) => {
this.ball.setVelocityX((-1*5*(this.paddle.x-this.ball.x)));
this.ball.anims.play('wobble');
}
이제 공은 더이상 동일한 각도로 튕기지 않으며 매번 튕길 때마다 변화가 생기게 되었습니다.
결론
지금까지 2회에 걸쳐 Angular 프로젝트에 Phaser 를 추가하여 Typescript(Javascript)를 이용하여 HTML5 게임을 만들어 보았습니다. 지금까지의 진행 내용은 다음의 링크에서 동작 및 소스코드를 확인할 수 있습니다.
'모듈, 프레임웍 > Phaser' 카테고리의 다른 글
Angular - Phaser 통합 (0) | 2020.04.11 |
---|---|
Phaser3 의 기본기능 및 퍼즐게임 예시 (3) | 2020.03.25 |
Phaser 3 Preloading 화면 만들기 (0) | 2019.04.08 |
Angular 와 Phaser 함께 사용하기 (1/2) (0) | 2019.01.06 |