Phaser, ES2015 and Breakout

So I'm entering the GitHub Game Off 2017 to level up my game development skills in a short space of time. As I'm immersed in JavaScript, I'm sticking with Phaser for development - an awesome HTML5 game framework. Phaser, including the upcoming version 3 due later this year, is built with ES5. I'm trying to standardize my workflow with ES2015+ for all projects, so to test how that'll work with Phaser I followed the Mozilla Development Network's breakout tutorial while translating the code to ES2015. For those absolutely new to Phaser, I definitely recommend the tutorial but using ES2015+ isn't necessary at all. If you choose, you can look at my code below for reference:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import Phaser from 'phaser';
class Game extends Phaser.Game {
constructor (width, height, mode, domElement='content', arg=null) {
super(width, height, mode, domElement, arg);
this.state.add('Game', GameState, true);
}
}
class GameState extends Phaser.State {
constructor() {
super();
this.ball = null;
this.paddle = null;
this.bricks = null;
this.brickInfo = null;
this.score = 0;
this.scoreText = null;
this.lives = 3;
this.livesText = null;
this.lifeLostText = null;
this.textStyle = {
font: '18px Helvetica',
fill: '#0095DD'
};
this.playing = false;
this.startButton = null;
this.ballHitPaddle = this.ballHitPaddle.bind(this);
this.initBricks = this.initBricks.bind(this);
this.ballHitBrick = this.ballHitBrick.bind(this);
this.ballLeaveScreen = this.ballLeaveScreen.bind(this);
this.startGame = this.startGame.bind(this);
}
preload() {
this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
this.scale.pageAlignHorizontally = true;
this.scale.pageAlignVertically = true;
this.stage.backgroundColor = '#eeeeee';
this.load.image('ball', 'assets/sprites/ball.png');
this.load.image('paddle', 'assets/sprites/paddle.png');
this.load.image('brick', 'assets/sprites/brick.png');
this.load.spritesheet('ball', 'assets/sprites/wobble.png', 20, 20);
this.load.spritesheet('button', 'assets/sprites/button.png', 120, 40);
}
create() {
this.physics.startSystem(Phaser.Physics.ARCADE);
this.physics.arcade.checkCollision.down = false;
this.ball = this.add.sprite(this.world.width * 0.5, this.world.height - 50,
'ball');
this.ball.animations.add('wobble', [0, 1, 0, 2, 0, 1, 0, 2, 0], 24);
this.physics.enable(this.ball, Phaser.Physics.ARCADE);
this.ball.body.collideWorldBounds = true;
this.ball.checkWorldBounds = true;
this.ball.events.onOutOfBounds.add(this.ballLeaveScreen, this);
this.ball.body.bounce.set(1);
this.paddle = this.add.sprite(this.world.width * 0.5,
this.world.height - 10, 'paddle');
this.paddle.scale.setTo(1.5, 1);
this.paddle.anchor.setTo(0.5, 1);
this.physics.enable(this.paddle, Phaser.Physics.ARCADE);
this.paddle.body.collideWorldBounds = true;
this.paddle.body.immovable = true;
this.initBricks();
this.bricks = game.add.group();
// Add bricks
for (let c = 0; c < this.brickInfo.count.col; c++) {
for (let r = 0; r < this.brickInfo.count.row; r++) {
let brickX = (r * (this.brickInfo.width + this.brickInfo.padding))
+ this.brickInfo.offset.left;
let brickY = (c * (this.brickInfo.height + this.brickInfo.padding))
+ this.brickInfo.offset.top;
let newBrick = this.add.sprite(brickX, brickY, 'brick');
this.physics.enable(newBrick, Phaser.Physics.ARCADE);
newBrick.body.immovable = true;
newBrick.scale.setTo(2, 1.2);
newBrick.anchor.set(0.5);
this.bricks.add(newBrick);
}
}
this.scoreText = this.add.text(5, 5, `Points: ${this.score}`, this.textStyle);
this.livesText = this.add.text(this.game.world.width - 5, 5,
`Lives: ${this.lives}`, this.textStyle);
this.livesText.anchor.set(1,0);
this.lifeLostText = this.add.text(this.world.width * 0.5,
this.world.height * 0.5, 'Life lost, click to continue', this.textStyle);
this.lifeLostText.anchor.set(0.5);
this.lifeLostText.visible = false;
this.startButton = this.add.button(this.world.width * 0.5,
this.world.height * 0.5, 'button', this.startGame, this, 1, 0, 2);
this.startButton.anchor.set(0.5);
}
update() {
this.physics.arcade.collide(this.ball, this.paddle, this.ballHitPaddle);
this.physics.arcade.collide(this.ball, this.bricks, this.ballHitBrick);
if (this.playing) {
this.paddle.x = this.input.x || (this.world.width * 0.5);
}
}
initBricks() {
this.brickInfo = {
width: 100,
height: 30,
count: {
row: 9,
col: 5
},
offset: {
top: 100,
left: 200
},
padding: 10
};
}
ballHitPaddle(ball, paddle) {
ball.animations.play('wobble');
ball.body.velocity.x = -1 * 5 * (paddle.x - ball.x);
}
ballHitBrick(ball, brick) {
let killTween = this.add.tween(brick.scale);
killTween.to({x: 0, y: 0}, 200, Phaser.Easing.Linear.None);
killTween.onComplete.addOnce(() => {
brick.kill();
}, this);
killTween.start();
this.score += 10;
this.scoreText.setText(`Points: ${this.score}`);
let countAlive = this.bricks.filter((brick) => {
return brick.alive;
}).length;
if (countAlive === 0) {
alert('You\'ve won the game, congrats!');
}
}
ballLeaveScreen() {
this.lives -= 1;
if (this.lives) {
this.livesText.setText(`Lives: ${this.lives}`);
this.lifeLostText.visible = true;
this.ball.reset(this.world.width * 0.5, this.world.height - 50);
this.paddle.reset(this.world.width * 0.5, this.world.height - 10);
this.input.onDown.addOnce(() => {
this.lifeLostText.visible = false;
this.ball.body.velocity.set(150, -150);
}, this);
} else {
alert('You lost, game over!');
location.reload();
}
}
startGame() {
this.startButton.destroy();
this.ball.body.velocity.set(150, -150);
this.playing = true;
}
}
const game = new Game('100', '100', Phaser.AUTO);

Works just like the original! Phaser Breakout

A major difference with my code is the use of a state, more than likely unnecessary but I just love using states even for small projects. I used this repo to setup webpack to transpile the code. You probably noticed from the GameState constructor that the repo does not allow me to use later ESnext features - I had to bind the object's functions to preserve the context of this instead of defining the function as a class property. It's my first time using that repository for ES2015 but I'd prefer a tool that includes ESnext out of the box and makes less assumptions about how my code is structured. Also, as we have to clone the repo we have to readjust the git remotes if we use version control for everything (and we really should!). Someone made a Phaser CLI to fill that need but only one developer is working on it and it's not updated frequently so I'm not basing my future work on it in this current state.

Still finalizing my workflow but so far it's promising with the Phaser-ES6-Webpack setup. Best solution might be to fork and modify for my requirements. I won't stop making games because of this. ES2015 modules greatly help structure your application and remove the need for a global game variable to be referenced all over the codebase. Modules alone are good enough reason to use ES2015+ with Phaser.

Happy hacking!