DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

Electron Adventures: Episode 71: CoffeeScript Phaser Game

Now that we have CoffeeScript 2 setup, let's create a simple game with Phaser 3.

js2.coffee

This is my first time writing new CoffeeScript in years, and I quickly discovered how painful lack of working js2.coffee is. The existing converter only handles pre-ES6 JavaScript, and even that often doesn't generate great code. Being able to convert between JavaScript and CoffeeScript easily was a huge part of CoffeeScript's appeal at the time, and it's now completely gone.

Not that there's anything too complicated about converting JavaScript to CoffeeScript manually, but it's pointless tedium in a language whose primary appeal is cutting on pointless tedium.

Asset files

I emptied preload.coffee as we won't need it.

I added star.png and coin.mp3 to public/. There's a lot of free assets on the Internet which you can use in your games.

We'll also need to npm install phaser

public/index.html

Here's the updated index.html file, just loading Phaser, and adding a placeholder div for game canvas to be placed at:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="app.css">
  </head>
  <body>
    <div id="game"></div>
    <script src="../node_modules/phaser/dist/phaser.js"></script>
    <script src="./build/app.js"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

public/app.css

To keep things simple I decided to just center the game canvas in the browser window, without any special styling:

body {
  background-color: #444;
  color: #fff;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
}

#game {
}
Enter fullscreen mode Exit fullscreen mode

Game source

Let's go through the game code. It's something I wrote a while ago, and just slightly adapted and converted to CoffeeScript for this episode.

Getters and setters

When CoffeeScript adapted to ES6, a few features were really difficult to add due to syntactic issues.

Dropping some features made sense, like the whole var/let/const mess. JavaScript would do just fine having one way to define variables - namely let. You might have noticed by now that I never use const - if variables declared consts were actually immutable, I might change my mind, but I find it both pointless extra thing to think about, and also intentionally misleading. Declaring mutable state with const, as is the standard React Hooks way (const [counter, setCounter] = useState(0)), looks like a vile abomination to me. So CoffeeScript never bothering with three variable types makes perfect sense.

Much more questionable is not having getters and setters. They can be emulated with calls to Object.defineProperty, but these are ugly and are in wrong place - in constructor instead of being part of class definition. Well, we'll just use what we have, so here's the getter helper:

get = (self, name, getter) ->
  Object.defineProperty self, name, {get: getter}
Enter fullscreen mode Exit fullscreen mode

Start game

We define constant size box and create a game using MainScene class.

size_x = 800
size_y = 600
game = new Phaser.Game
  backgroundColor: "#AAF"
  width: size_x
  height: size_y
  scene: MainScene
Enter fullscreen mode Exit fullscreen mode

StarEmitter

When a ball hits a brick, we want to do some fancy effects. An easy effect is bursting some stars, and it's so common Phaser already contains particle emitter system. Here's a class that sets up such emitter with some settings how those stars should fly.

class StarEmitter
  constructor: (scene) ->
    @particles = scene.add.particles("star")
    @emitter = @particles.createEmitter
      gravityY: -50
      on: false
      lifespan: 2000
      speedX: {min: -50, max: 50}
      speedY: {min: -50, max: 50}
      alpha: 0.2
      rotate: {min: -1000, max: 1000}

  burst_at: (x, y) ->
    @emitter.emitParticle(40, x, y)
Enter fullscreen mode Exit fullscreen mode

Brick

class Brick
  constructor: (scene, x, y) ->
    colors_by_row = {
      2: 0xFF0000
      3: 0xFF0080
      4: 0xFF00FF
      5: 0xFF80FF
      6: 0x8080FF
      7: 0x80FFFF
    }
    @destroyed = false
    @brick_x_size = size_x/18
    @brick_y_size = size_y/30
    @brick = scene.add.graphics()
    @brick.x = x*size_x/12
    @brick.y = y*size_y/20
    @brick.fillStyle(colors_by_row[y])
    @brick.fillRect(
      -@brick_x_size/2, -@brick_y_size/2,
      @brick_x_size, @brick_y_size
    )
    get @, "x",-> @brick.x
    get @, "y",-> @brick.y

  destroy: ->
    @brick.destroy()
    @destroyed = true
Enter fullscreen mode Exit fullscreen mode

Brick is a straightforward class wrapping Phaser brick object. You can see how one can do getters in CoffeeScript. It works, but it's a bit awkward.

The only method Brick has is destroy.

Ball

class Ball
  constructor: (scene) ->
    @ball = scene.add.graphics()
    @ball.x = 0.5*size_x
    @ball.y = 0.8*size_y
    @ball.fillStyle(0x000000)
    @ball.fillRect(-10,-10,20,20)
    @dx = 300
    @dy = -300
    get @, "x", -> @ball.x
    get @, "y", -> @ball.y

  update: (dt) ->
    @ball.x += @dx*dt
    @ball.y += @dy*dt
    if @ball.x <= 10 && @dx < 0
      @dx = - @dx
    if @ball.x >= size_x-10 && @dx > 0
      @dx = - @dx
    if @ball.y <= 10 && @dy < 0
      @dy = - @dy
Enter fullscreen mode Exit fullscreen mode

The Ball has similar messy getter. The only method is update which is passed how much time passed since the last update, and it's responsible for ball bouncing off the walls, but not bouncing off paddle or bricks.

Paddle

class Paddle
  constructor: (scene) ->
    @paddle = scene.add.graphics()
    @paddle.x = 0.5*size_x
    @paddle.y = size_y-20
    @paddle.fillStyle(0x0000FF)
    @paddle.fillRect(-50, -10, 100, 20)
    get @, "x", -> @paddle.x

  update: (dt, direction) ->
    @paddle.x += dt * direction * 500
    @paddle.x = Phaser.Math.Clamp(@paddle.x, 55, size_x-55)
Enter fullscreen mode Exit fullscreen mode

Paddle follows the same pattern. Its direction is sent to the update method depending on which keys are pressed, and it moves left or right. Phaser.Math.Clamp prevents it from going outside the canvas.

MainScene

class MainScene extends Phaser.Scene
  preload: () ->
    @load.image("star", "star.png")
    @load.audio("coin", "coin.mp3")

  create: () ->
    @active = true
    @paddle = new Paddle(@)
    @ball = new Ball(@)
    @bricks = []
    for x from [1..11]
      for y from [2..7]
        @bricks.push(new Brick(@, x, y))
    @emitter = new StarEmitter(@)
    @coin = @sound.add("coin")
    @coin.volume = 0.2

  handle_brick_colission: (brick) ->
    return if brick.destroyed
    distance_x = Math.abs((brick.x - @ball.x) / (10 + brick.brick_x_size/2))
    distance_y = Math.abs((brick.y - @ball.y) / (10 + brick.brick_y_size/2))
    if distance_x <= 1.0 && distance_y <= 1.0
      brick.destroy()
      @emitter.burst_at(@ball.x, @ball.y)
      @coin.play()
      if distance_x < distance_y
        @ball_bounce_y = true
      else
        @ball_bounce_x = true

  is_game_won: () ->
    @bricks.every((b) => b.destroyed)

  update: (_, dts) ->
    return unless @active
    dt = dts / 1000.0
    @ball.update(dt)
    if @input.keyboard.addKey("RIGHT").isDown
      @paddle.update(dt, 1)
    else if @input.keyboard.addKey("LEFT").isDown
      @paddle.update(dt, -1)
    @ball_bounce_x = false
    @ball_bounce_y = false
    for brick from @bricks
      @handle_brick_colission(brick)
    @ball.dx = -@ball.dx if @ball_bounce_x
    @ball.dy = -@ball.dy if @ball_bounce_y

    paddle_distance = Math.abs(@paddle.x - @ball.x)
    bottom_distance = size_y - @ball.y

    if @ball.dy > 0
      if bottom_distance <= 30 && paddle_distance <= 60
        @ball.dy = -300
        @ball.dx = 7 * (@ball.x - @paddle.x)
      else if bottom_distance <= 10 && paddle_distance >= 60
        @cameras.main.setBackgroundColor("#FAA")
        @active = false
    if @is_game_won()
      @cameras.main.setBackgroundColor("#FFF")
      @active = false
Enter fullscreen mode Exit fullscreen mode

And finally the MainScene. preload, create, and update are Phaser methods. Everything else we just created ourselves.

I think everything should be fairly readable, as long as you remember that @foo means this.foo, so it's used for both instance variables and instance methods.

Is CoffeeScript dead?

While I feel nostalgia for it, the unfortunate answer is yes. I mentioned some historical background in previous episode, but ES6 adopted most of the features people used CoffeeScript for, and available tooling did not keep up with the times.

That's not to say the idea is dead. In particular Imba is a CoffeeScript-inspired language and framework that's absolutely worth checking out. It comes with an extremely expressive and performant framework. For some less extreme cases, Svelte, Vue, React, and so on all come with their own extended versions of JavaScript, so nobody really writes app in plain JavaScript anymore.

Results

Here's the results:

Episode 71 Screenshot

It's time to say goodbye to CoffeeScript, in the next episode we start another small project.

As usual, all the code for the episode is here.

Discussion (0)