ourcade - How to Really Make a Phaser Game from Scratch!을 보고 정리한 글
Phaser란?
- Phaser는 HTML5 게임 프레임워크 기술이다.
- 웹 기술에 의존하며, 웹 게임의 특성상 데스크톱이나 모바일 웹 브라우저에서 자유롭게 제작이 가능하다.
- Phaser를 통해 제작된 대표적인 게임으로는 Vampire Survivors(~2023년 2월), PokéRogue 등이 있다.
Parcel 설치
Phaser를 사용하기 위해서는 Local Web Server 환경을 설정해야 한다. 웹 어플리케이션은 여러 리소스를 로딩하고, HTTPS 환경에서 작동하기 때문이다. Local Web Server를 만드는 가장 쉬운 방법은 Node.js의 패키지들을 이용하는 것이다.
Parcel은 모듈 번들러이다. 모듈 번들러로 가장 유명한 것은 Webpack이지만, Parcel은 Webpack에 필요한 여러가지 설정이 필요없는 zero configuration을 지향하며, 성능도 Webpack에 비해 빠르다.
Parcel을 이용하여, Node.js의 다양한 패키지를 이용해 Local Web Server를 구축하고, Phaser 어플리케이션을 만들고, 이를 Webpack에 비해 빠르게 테스트할 수 있다.
- 프로젝트 폴더를 열고 터미널에
npm init
을 입력하여 npm으로 폴더를 관리한다.
{
"name": "phaser-game",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
-
터미널에
npm install parcel-bundler
를 입력하여 Parcel을 설치한다. -
엔트리 파일을 만든다.
./src/index.html
을 생성하여 이를 엔트리파일로 사용한다.
- Emmet이 설정되었다면 빈 파일에 !를 치고 탭을 누르면 아래와 같이 생성된다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
</html>
- pacakage.json에 스크립트를 추가한다.
{
"name": "phaser-game",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "parcel src/index.html", //추가됨
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"parcel-bundler": "^1.12.5"
}
}
npm start
를 입력하면 http://localhost:1234에서 로컬 서버가 실행된다.
- start script를
"start": "parcel src/index.html -p 8000",
와 같이 바꾸면 8000번 포트에서 실행된다. Parcel CLI 공식문서
phaser 설치
-
터미널에
npm install phaser
를 통해 설치한다. ("^3.80.1" 설치됨) -
src 폴더에 phaser의 엔트리 파일이 될 main.js 파일을 생성한다.
-
아래와 같이 game 인스턴스를 생성한다.
// src/main.js
import Phaser from "phaser";
const game = new Phaser.Game();
- Game 클래스는 인스턴스를 생성하기 위한 GameConfig를 인자로 넘겨받을 수 있다. Phaser 3 API Docs - Phaser.Game
- config 옵션에서 type은 Canvas, WebGL를 사용할지 등을 결정한다. Phaser.AUTO는 기본으로 WebGL 사용을 시도하고, 브라우저가 WebGL을 지원하지 않으면 Canvas로 실행된다.
import Phaser from "phaser";
const config = {
width: 800,
height: 500,
type: Phaser.AUTO,
};
const game = new Phaser.Game(config);
- 해당 엔트리파일을 index.html에 script 태그로 삽입해준다.
...
<body>
<script src="./main.js"></script>
</body>
...
게임인스턴스가 화면에 표시되는 것을 확인할 수 있다.
Title Screen
Scene은 게임의 오브젝트들이 모여 하나의 장면을 이룬다. 일반적으로 하나의 레벨(맵)이나 배경을 기준으로 한다.
타이틀 화면을 만들도록 한다.
src폴더 하위에 scenes 폴더를 만들고, TitleScreen.js 파일을 생성한다.
// src\scenes\TitleScreen.js
import Phaser from "phaser";
export default class TitleScreen extends Phaser.Scene {
preload(){
}
create(){
}
}
위와 같이 Phaser.Scene 클래스를 상속받는다.
그리고 모든 장면 클래스는 preload와 create 메서드를 갖는다.
해당 클래스가 작동하는지 알아보기 위해 아래와 같이 create 메서드를 작성한다.
create() {
this.add.text(400, 250, "Hello, World!");
}
해당 장면 객체를 main.js에서 실행시켜보자.
//src\main.js
import Phaser from "phaser";
// 1. TitleScreen을 import한다.
import TitleScreen from "./scenes/TitleScreen";
const config = {
width: 800,
height: 500,
type: Phaser.AUTO,
};
const game = new Phaser.Game(config);
// 2. scene.add(고유한 Key, Scene 객체)로 scene을 추가한다.
game.scene.add("titlescreen", TitleScreen);
// 3. scene.start(Key)로 scene을 실행한다.
game.scene.start("titlescreen");
1. 물리 엔진 추가하기
핑퐁 게임을 만들어보자. 먼저 공 객체를 추가하고 물리엔진을 적용한다.
먼저 src\main.js의 config에 physics 객체를 추가하자. physics와 관련된 속성은 Phaser.Types.Core.PhysicsConfig에서 확인할 수 있다.
const config = {
width: 800,
height: 400,
type: Phaser.AUTO,
physics: {
default: "arcade",
arcade: {
gravity: { y: 0 },
},
},
};
공 객체 추가하기
src\scenes\Game.js
import Phaser from "phaser";
class Game extends Phaser.Scene {
preload() {}
create() {
// 공 객체를 하나 생성한다.
const ball = this.add.circle(400, 250, 10, 0xffffff, 1);
// 공 객체를 물리엔진에 추가한다.
this.physics.add.existing(ball);
}
}
export default Game;
circle 객체 클래스에 추가한다.
그리고 해당 메서드에서 반환된 객체를 물리엔진에도 똑같이 넘겨준다.
해당 화면을 불러와보자
import * as Phaser from "phaser";
import TitleScreen from "./scenes/TitleScreen";
import Game from "./scenes/Game";
const game = new Phaser.Game({
width: 800,
height: 400,
type: Phaser.AUTO,
physics: {
default: "arcade",
arcade: {
gravity: {
y: 0,
},
},
},
});
game.scene.add("titlescreen", TitleScreen);
game.scene.add("game", Game); //추가됨
game.scene.start("game"); //추가됨
공이 생성된 것을 볼 수 있다. gravity.y가 0으로 설정되어 있기 때문에 공은 움직이지 않고 허공에 떠있다.
Game 씬의 코드에 아래와 같이 추가해주면 공이 움직인다.
import Phaser from "phaser";
class Game extends Phaser.Scene {
preload() {}
create() {
const ball = this.add.circle(400, 250, 10, 0xffffff, 1);
this.physics.add.existing(ball);
// 1. World의 경계에서 물체가 충돌하도록 설정한다.
ball.body.setCollideWorldBounds(true, 1, 1);
// 2. 물체의 속도를 정한다. Y, X모두 1초당 200픽셀 이동하도록 한다.
ball.body.setVelocity(200, 200);
}
}
export default Game;
물리엔진에 추가된 객체는 충돌을 감지할 수 있는 body가 생성된다.
setCollideWorldBounds 메서드를 통해 World의 경계에서 튕기도록 하고,
setVelocity를 통해 공이 이동하도록 설정한다.
이렇게 설정한 후 게임을 실행하면, 공이 경계에서 튕기며 이동하는 것을 확인할 수 있다.
패들 객체 추가하기
이번에는 핑퐁에서 공을 튕겨내는 패들 객체를 만들어보자.
const paddleLeft = this.add.rectangle(30, 250, 30, 100, 0xffffff, 1);
this.physics.add.existing(paddleLeft);
this.physics.add.collider(ball, paddleLeft);
collider를 통해 충돌체에 ball과 paddle을 추가하여 둘이 서로 충돌하도록 만든다.
이를 실행하면 ball이 paddle과 충돌할 때에 ball이 paddle을 쭉 밀고 나가는 것을 확인할 수 있다. 움직이는 물체인 ball의 에너지가 더 커서 그렇다.
paddle이 움직이지 않도록 질량을 추가해보자.
paddleLeft.body.setMass(1000);
paddleLeft.body.setBounce(1, 1);
vscode의 인텔리센스를 활용하고 싶은 경우 아래와 같이 JSDoc을 이용하여 활용하는 방법도 있다.
/** @type {Phaser.Physics.Arcade.Body} */
const paddleLeftBody = paddleLeft.body;
paddleLeftBody.setMass(1000);
paddleLeftBody.setBounce(1, 1);
이렇게 질량을 1000까지 늘렸는데도 패들이 튕겨져 나간다.
패들이 튕겨져나가지 않도록 정적 몸체로 바꿔줄 필요가 있다.
// this.physics.add.existing(paddleLeft);
this.physics.add.existing(paddleLeft, true);
static한 객체의 body는 여러 메서드가 비활성화 된다.
paddle대신 ball에 메서드를 추가하도록 변경한다.
// paddleLeft.body.setMass(1000);
// paddleLeft.body.setBounce(1, 1);
ball.body.setBounce(1, 1);
이제 공이 패들을 만나면 튕겨져 나간다.
import Phaser from "phaser";
class Game extends Phaser.Scene {
preload() {}
create() {
const ball = this.add.circle(400, 250, 10, 0xffffff, 1);
this.physics.add.existing(ball);
ball.body.setCollideWorldBounds(true, 1, 1);
ball.body.setVelocity(-200, 0);
ball.body.setBounce(1, 1);
const paddleLeft = this.add.rectangle(30, 250, 30, 100, 0xffffff, 1);
this.physics.add.existing(paddleLeft, true);
this.physics.add.collider(ball, paddleLeft);
}
}
export default Game;
2. 키 입력
키 입력을 통해 패들이 움직이도록 구현해보자
import Phaser from "phaser";
class Game extends Phaser.Scene {
preload() {}
create() {
...
this.cursors = this.input.keyboard.createCursorKeys()
}
}
export default Game;
createCursorKeys는 키보드 방향키 상, 하, 좌, 우와 스페이스바, 쉬프트, 총 6개의 키 객체를 반환한다.
import Phaser from "phaser";
class Game extends Phaser.Scene {
preload() {}
create() {
...
// const paddleLeft = this.add.rectangle(30, 250, 30, 100, 0xffffff, 1);
this.paddleLeft = this.add.rectangle(30, 250, 30, 100, 0xffffff, 1);
// this.physics.add.existing(paddleLeft, true);
this.physics.add.existing(this.paddleLeft, true);
// this.physics.add.collider(ball, paddleLeft);
this.physics.add.collider(ball, this.paddleLeft);
...
}
update(){
}
}
export default Game;
앞서 추가한 paddleLeft를 클래스의 프로퍼티로 만들기 위해 this.paddleLeft로 변경해주었다.
그리고 class 객체에 update()메서드를 추가해준다. update 메서드는 해당 Scene이 실행되고 있을 때, 매 프레임마다 호출되어 실행된다.
이제 업데이트 함수를 아래와 같이 작성해준다.
update() {
if (this.cursors.up.isDown) {
// this.paddleLeft.setVelocityY(-10)
this.paddleLeft.y -= 10;
} else if (this.cursors.down.isDown) {
this.paddleLeft.y += 10;
}
}
방향키의 up이 눌리고 있는 경우(isDown) paddleLeft의 y좌표에서 10을 뺀다.
참고로 setVelocity 메서드는 static이 아닌 body객체에서만 사용할 수 있다.
이렇게 작성을 하고 실행을 해보면 공이 기존의 패들 위치에서 튕겨져 나가는 것을 볼 수 있다.
// src\main.js
const game = new Phaser.Game({
width: 800,
height: 400,
type: Phaser.AUTO,
physics: {
default: "arcade",
arcade: {
gravity: {
y: 0,
},
debug: true, // 디버깅을 위해 추가해준다.
},
},
});
게임 객체는 this.paddleLeft는 물리엔진에서 Static Body로 설정되어 있다. 해당 게임 객체의 좌표는 이동했지만, 물리 객체 Body는 이동하지 않고 있는 것이다.
update() {
if (this.cursors.up.isDown) {
this.paddleLeft.y -= 10;
this.paddleLeft.body.updateFromGameObject();
} else if (this.cursors.down.isDown) {
this.paddleLeft.y += 10;
this.paddleLeft.body.updateFromGameObject();
}
}
updateFromGameObject 메서드를 호출하여 게임 객체의 위치와 Body의 위치를 동기화시킨다.
3. 오른쪽 패들
아래와 같이 코드를 추가하여 공과 충돌하는 오른쪽 패들을 만든다. 한편 공의 움직임에 상하 움직임도 추가한다. 오른쪽 패들이 공의 상하 움직임을 따라가도록 만들 생각이다.
import Phaser from "phaser";
class Game extends Phaser.Scene {
preload() {}
create() {
const ball = this.add.circle(400, 250, 10, 0xffffff, 1);
this.physics.add.existing(ball);
ball.body.setCollideWorldBounds(true, 1, 1);
ball.body.setVelocity(-200, -200); // 수정됨
ball.body.setBounce(1, 1);
this.paddleLeft = this.add.rectangle(50, 250, 30, 100, 0xffffff, 1);
this.physics.add.existing(this.paddleLeft, true);
this.paddleRight = this.add.rectangle(750, 250, 30, 100, 0xffffff, 1); // 추가됨
this.physics.add.existing(this.paddleRight, true); // 추가됨
this.physics.add.collider(ball, this.paddleLeft);
this.physics.add.collider(ball, this.paddleRight); // 추가됨
this.cursors = this.input.keyboard.createCursorKeys();
}
update() {
if (this.cursors.up.isDown) {
this.paddleLeft.y -= 10;
this.paddleLeft.body.updateFromGameObject();
} else if (this.cursors.down.isDown) {
this.paddleLeft.y += 10;
this.paddleLeft.body.updateFromGameObject();
}
}
}
export default Game;
공의 좌표를 파악하기 위해 공을 로컬 속성에 추가해준다.
import Phaser from "phaser";
class Game extends Phaser.Scene {
preload() {}
create() {
this.ball = this.add.circle(400, 250, 10, 0xffffff, 1); // 수정됨
this.physics.add.existing(this.ball); // 수정됨
this.ball.body.setCollideWorldBounds(true, 1, 1); // 수정됨
this.ball.body.setVelocity(-200, -200); // 수정됨
this.ball.body.setBounce(1, 1); // 수정됨
this.paddleLeft = this.add.rectangle(50, 250, 30, 100, 0xffffff, 1);
this.physics.add.existing(this.paddleLeft, true);
this.paddleRight = this.add.rectangle(750, 250, 30, 100, 0xffffff, 1);
this.physics.add.existing(this.paddleRight, true);
this.physics.add.collider(this.ball, this.paddleLeft); // 수정됨
this.physics.add.collider(this.ball, this.paddleRight); // 수정됨
this.cursors = this.input.keyboard.createCursorKeys();
}
update() {
if (this.cursors.up.isDown) {
this.paddleLeft.y -= 10;
this.paddleLeft.body.updateFromGameObject();
} else if (this.cursors.down.isDown) {
this.paddleLeft.y += 10;
this.paddleLeft.body.updateFromGameObject();
}
}
}
export default Game;
업데이트에 아래와 같이 추가하여 공의 y좌표에 따라 오른쪽 패들이 위, 아래로 움직이도록 한다.
update() {
if (this.cursors.up.isDown) {
this.paddleLeft.y -= 10;
this.paddleLeft.body.updateFromGameObject();
} else if (this.cursors.down.isDown) {
this.paddleLeft.y += 10;
this.paddleLeft.body.updateFromGameObject();
}
const diff = this.ball.y - this.paddleRight.y;
if (diff < 0) {
this.paddleRight.y -= 10;
this.paddleRight.body.updateFromGameObject();
} else if (diff > 0) {
this.paddleRight.y += 10;
this.paddleRight.body.updateFromGameObject();
}
}
조금 더 자연스러운 움직임을 위해 가속도를 추가해보자.
init() 메서드를 통해 paddleRightVelocity라는 벡터값을 생성한다.
class Game extends Phaser.Scene {
init() {
this.paddleRightVelocity = new Phaser.Math.Vector2(0, 0);
}
...
그리고 아래와 같이 값을 넣는다
update() {
...
const diff = this.ball.y - this.paddleRight.y;
const aiSpeed = 0.5;
if (diff < 0) {
this.paddleRightVelocity.y -= aiSpeed;
if (this.paddleRightVelocity.y < -10) {
this.paddleRightVelocity.y = -10;
}
} else if (diff > 0) {
this.paddleRightVelocity.y += aiSpeed;
if (this.paddleRightVelocity.y > 10) {
this.paddleRightVelocity.y = 10;
}
}
this.paddleRight.y += this.paddleRightVelocity.y;
this.paddleRight.body.updateFromGameObject();
}
이제 오른쪽 패들이 공위 위치에 따라서 점진적으로 가속하며, 최대 속도는 10을 넘지 않는다.
aiSpeed의 값이 가속력을 조정하여, aiSpeed가 높을수록 공을 더 빠르고 정확하게 찾아간다. aiSpeed의 값이 낮을수록 오른쪽 패들의 기민함이 떨어져 게임의 난이도가 낮아진다.
4.
각도를 벡터로
이제 공에 움직임을 넣어주자.
Ourcade - Convert Angle to Vector with Arcade Physics 를 참고하여 각도와 속도를 고려하여 벡터값이 변경되도록 수정해줄 수 있다.
// this.ball.body.setVelocity(-200, -200);
const angle = Phaser.Math.Between(0, 360);
const vec = this.physics.velocityFromAngle(angle, 200);
this.ball.body.setVelocity(vec.x, vec.y);
점수 계산
먼저 물리 세계를 넓혀주자
this.physics.world.setBounds(-100, 0, 1000, 500);
월드의 크기가 800, 500이었던 점을 고려하면, 이제 양 옆으로 100씩 여백이 생겼다
해당 여백 안으로 공이 들어가면 다시 화면의 중앙으로 공이 오도록 해보자.
update() {
...
if (this.ball.x < -30) {
this.resetBall();
} else if (this.ball.x > 830) {
this.resetBall();
}
}
resetBall() {
this.ball.setPosition(400, 250);
const angle = Phaser.Math.Between(0, 360);
const vec = this.physics.velocityFromAngle(angle, 200);
this.ball.body.setVelocity(vec.x, vec.y);
}
resetBall()이라는 메서드를 만들고, 화면의 끝에 공이 닿으면 공이 정 중앙으로 오도록 했다.
이제 점수를 계산할 수 있는 변수 두 개를 만들자
init() {
...
this.leftScore = 0;
this.rightScore = 0;
}
init에서 변수를 선언하면 다른 메서드에서도 사용할 수 있다.
create 메서드에서 텍스트 객체를 추가해주자
create() {
...
this.leftScoreLabel = this.add
.text(300, 125, String(this.leftScore), { fontSize: 48 })
.setOrigin(0.5, 0.5); // 원점이 중앙에 있음
this.rightScoreLabel = this.add
.text(500, 375, String(this.rightScore), { fontSize: 48 })
.setOrigin(0.5, 0.5);
...
}
해당 텍스트 객체를 증가, 감소 시키는 메서드를 만든다.
incrementLeftScore() {
this.leftScore += 1;
this.leftScoreLabel.text = String(this.leftScore);
}
incrementRightScore() {
this.rightScore += 1;
this.rightScoreLabel.text = String(this.rightScore);
}
이제 업데이트 메서드에 있던 resetBall을 아래와 같이 수정해준다.
update() {
...
if (this.ball.x < -30) {
this.resetBall();
this.incrementRightScore(); // 추가됨
} else if (this.ball.x > 830) {
this.resetBall();
this.incrementLeftScore(); // 추가됨
}
}
import Phaser from "phaser";
class Game extends Phaser.Scene {
init() {
this.paddleRightVelocity = new Phaser.Math.Vector2(0, 0);
this.leftScore = 0;
this.rightScore = 0;
}
preload() {}
create() {
this.physics.world.setBounds(-100, 0, 1000, 500);
this.ball = this.add.circle(400, 250, 10, 0xffffff, 1);
this.physics.add.existing(this.ball);
this.ball.body.setBounce(1, 1);
this.ball.body.setCollideWorldBounds(true, 1, 1);
const angle = Phaser.Math.Between(0, 360);
const vec = this.physics.velocityFromAngle(angle, 200);
this.ball.body.setVelocity(vec.x, vec.y);
this.paddleLeft = this.add.rectangle(50, 250, 30, 100, 0xffffff, 1);
this.physics.add.existing(this.paddleLeft, true);
this.paddleRight = this.add.rectangle(750, 250, 30, 100, 0xffffff, 1);
this.physics.add.existing(this.paddleRight, true);
this.physics.add.collider(this.ball, this.paddleLeft);
this.physics.add.collider(this.ball, this.paddleRight);
this.leftScoreLabel = this.add
.text(300, 125, String(this.leftScore), { fontSize: 48 })
.setOrigin(0.5, 0.5); // 원점이 중앙에 있음
this.rightScoreLabel = this.add
.text(500, 375, String(this.rightScore), { fontSize: 48 })
.setOrigin(0.5, 0.5);
this.cursors = this.input.keyboard.createCursorKeys();
}
update() {
if (this.cursors.up.isDown) {
this.paddleLeft.y -= 10;
this.paddleLeft.body.updateFromGameObject();
} else if (this.cursors.down.isDown) {
this.paddleLeft.y += 10;
this.paddleLeft.body.updateFromGameObject();
}
const diff = this.ball.y - this.paddleRight.y;
const aiSpeed = 0.5;
if (diff < 0) {
this.paddleRightVelocity.y -= aiSpeed;
if (this.paddleRightVelocity.y < -10) {
this.paddleRightVelocity.y = -10;
}
} else if (diff > 0) {
this.paddleRightVelocity.y += aiSpeed;
if (this.paddleRightVelocity.y > 10) {
this.paddleRightVelocity.y = 10;
}
}
this.paddleRight.y += this.paddleRightVelocity.y;
this.paddleRight.body.updateFromGameObject();
if (this.ball.x < -30) {
this.resetBall();
this.incrementRightScore();
} else if (this.ball.x > 830) {
this.resetBall();
this.incrementLeftScore();
}
}
resetBall() {
this.ball.setPosition(400, 250);
const angle = Phaser.Math.Between(0, 360);
const vec = this.physics.velocityFromAngle(angle, 200);
this.ball.body.setVelocity(vec.x, vec.y);
}
incrementLeftScore() {
this.leftScore += 1;
this.leftScoreLabel.text = String(this.leftScore);
}
incrementRightScore() {
this.rightScore += 1;
this.rightScoreLabel.text = String(this.rightScore);
}
}
export default Game;
8. Title Screen
// src\scenes\TitleScreen.js
import Phaser from "phaser";
export default class TitleScreen extends Phaser.Scene {
preload() {}
create() {
const title = this.add.text(400, 200, "Old School Tennis", {
fontSize: 50,
});
title.setOrigin(0.5, 0.5);
this.add.text(400, 300, "Press Space to Start").setOrigin(0.5);
}
}
타이틀 스크린의 텍스트를 위와 같이 변경하여준다.
...
// game.scene.start("game");
game.scene.start("titlescreen");
main.js에서 위와 같이 titlescreen을 실행하도록 변경해준다.
키보드 이벤트
이제 스페이스바를 입력하면 게임이 시작되도록 이벤트를 넣어보자.
Phaser.Input.Keyboard. KeyboardPlugin
// src\scenes\TitleScreen.js
import Phaser from "phaser";
export default class TitleScreen extends Phaser.Scene {
preload() {}
create() {
...
this.input.keyboard.once("keydown-SPACE", () => {
this.scene.start("game");
});
}
keyboard 플러그인에서 on은 키 입력을 감지한다. 중복 입력도 감지함으로, once를 통해 한 번만 감지하여 실행하도록 한다.
event명은 "event명-대문자키"로 구성된다.
9. 게임 종료
먼저 게임 종료 화면을 만들어보자.
main.js에서 아래와 같이 게임 종료 씬을 추가해준다.
import * as Phaser from "phaser";
...
import GameOver from "./scenes/Gameover";
...
game.scene.add("titlescreen", TitleScreen);
game.scene.add("game", Game);
game.scene.add("gameover", GameOver); // 추가됨
...
이제 Game.js의 init 메서드에서 gameState라는 문자열 변수를 하나 만든다.
init() {
this.paddleRightVelocity = new Phaser.Math.Vector2(0, 0);
this.gameState = "running"; // 추가됨
this.leftScore = 0;
this.rightScore = 0;
}
15점이 넘으면 해당 변수가 player-won이나 ai-won으로 변경되도록 로직을 추가해준다.
update() {
...
if (this.leftScore >= 15) {
this.gameState = "player-won";
} else if (this.rightScore >= 15) {
this.gameState = "ai-won";
}
if (this.gameState !== "running") {
this.scene.start("gameover", {
gameState: this.gameState,
});
}
}
scene.start메서드의 첫번째 인자는 scene의 key값이고, 두번째 인자는 데이터이다. 해당 데이터를 받아 scene에서 사용할 수 있다.
아래와 같이 GameOver Scene을 작성해준다.
//src\scenes\Gameover.js
export default class GameOver extends Phaser.Scene {
create(data) {
// 데이터의 gameState를 받아 텍스트를 변경한다.
let titleText = "You Lose";
if (data.gameState === "player-won") {
titleText = "You Win!";
}
// 텍스트를 표시해주고, spacebar를 누르면 게임이 다시 시작되도록 한다.
this.add.text(400, 250, titleText, { fontSize: 38 }).setOrigin(0.5);
this.add.text(400, 300, "Press Space to Restart").setOrigin(0.5);
this.input.keyboard.once("keydown-SPACE", () => {
this.scene.start("game");
});
}
}
10 소리 넣기
opengameart - 3-ping-pong-sounds-8-bit-style에서 파일을 다운받고 public/assets 폴더에 넣는다.
해당 파일을 인식시키기 위해서는
npm install --save-dev parcel-plugin-static-files-copy
로 플러그인을 설치해주어야 한다.- staticFiles의 경로를 package.json에 추가해주어야 한다.
{
"name": "phaser-game",
"version": "1.0.0",
"description": "",
"main": "index.js",
...
"devDependencies": {
"parcel-plugin-static-files-copy": "^2.6.0"
},
"staticFiles": {
"staticPath": "public",
"watcherGlob": "**"
}
}
staticPath는 정적 파일이 저장된 폴더명이고,
watcherGlob는 파일변경을 감시할 때 사용할 패턴이다. **
은 모든 파일을 의미한다.
해당 퍼블릭 폴더의 파일을 미리 불러오는 장면을 만든다.
//src\scenes\Payload.js
export default class Preload extends Phaser.Scene {
preload() {
this.load.audio("beeep", "assets/ping_pong_8bit_beeep.ogg");
this.load.audio("plop", "assets/ping_pong_8bit_plop.ogg");
}
create() {
this.scene.start("titlescreen");
}
}
// src\main.js
...
game.scene.add("preload", Preload); // 추가됨
game.scene.add("titlescreen", TitleScreen);
game.scene.add("game", Game);
game.scene.add("gameover", GameOver);
game.scene.start("preload"); // 수정됨
이제 패들이 공을 때릴 때 소리가 나도록 수정해보자.
기존 Game.js에서 collider를 추가하는 코드는 아래와 같았다.
import Phaser from "phaser";
class Game extends Phaser.Scene {
...
create() {
...
this.physics.add.collider(this.ball, this.paddleLeft);
this.physics.add.collider(this.ball, this.paddleRight);
collider의 3번째 파라미터부터는 충돌이 발생하였을 때 실행시킬 콜백 함수를 넣을 수 있다.
아래와 같이 수정하여 충돌이 발생하면 소리를 재생하고, 공의 가속도를 증가시키도록 수정해보자.
this.physics.add.collider(this.ball, this.paddleLeft, () => {
this.sound.play("beeep"),
this.ball.body.setVelocity(
this.ball.body.velocity.x * 1.1,
this.ball.body.velocity.y
);
});
this.physics.add.collider(this.ball, this.paddleRight, () => {
this.sound.play("plop");
this.ball.body.setVelocity(
this.ball.body.velocity.x * 1.1,
this.ball.body.velocity.y + this.paddleRightVelocity.y * 20
);
});
위에서 오른쪽 패들의 경우 가속도가 있기 때문에 해당 가속도를 공에도 적용하도록 수정하였다. 이제 공이 더욱 예측 불가능하게 움직인다.
이렇게 핑퐁 게임을 만들어보았다.