A screenshot of a side-scrolling platform game with the Phaser logo superimposed

Learning Phaser with Astro


I’ve been toying with the idea of making a videogame for a long time, but was lacking motivation. I recently came across a thread by Brian Bucklew who documented his work in porting his Caves of Qud from Unity to Godot. The thread is a great read and I highly recommend it.

Screenshot of Brian Bucklew's tweet about migrating to Godot

After reading the thread, it seems I finally found that motivation as I jumped into learning Godot. I very quickly realized I didn’t want to learn a GDScript, so before investing more effort into Godot, I decided to try out Phaser, a JavaScript game engine that runs on the web. I began going through the official tutorial and the first steps include setting up a web server. I thought to myself, “I can do that in Astro!1” So I did.

The first Astro-related issue I ran into is that the content folder for the blog has to be in Markdown or JSON. Fortunately, Astro supports MDX, which is Markdown that can render JSX inline. I can even import components from the project and use them in my Markdown. So, I create an MDX file in my content directory and immediately realize there is no syntax highlighting for this filetype in Neovim.

I search for a Treesitter grammar with no luck. I search online and find nothing, at most there are recommendations to set filetype to JSX, but this would mean no language server features. I decided to write the MDX without syntax highlighting and develop the Phaser tutorial in a component file to import into the MDX file.

components/Phaser/HelloWorld.astro

<div class="wrapper">
  <button data-button>Click Me!</button>
</div>

<script>
  // Find our component DOM on the page.
  const button = document.querySelector("[data-button]");

  // Add event listeners to send alert when a button is clicked.
  button.addEventListener("click", () => alert("You clicked the button 🎉"));
</script>

MDX

import HelloWorld from "../../components/Phaser/HelloWorld.astro";

<HelloWorld />;

By creating an .astro file in my components/ directory, I am now able to load it into my MDX file and render it. I can even add styles and arbitrary HTML by using the Astro component syntax!

On to using Phaser in MDX. The first issue I ran into was that the preload function was fetching from the Phaser asset library and couldn’t be loaded due to CORS restrictions. I decided to host the assets locally by downloading them to the /public/ folder. I’m not sure if there is a better way to do this, but it works for now.

A second issue I encountered was that there was a console error when using the tutorial code as-is. It was a simple fix, thankfully the error was self-documenting. The particle emitter was changed since the tutorial was published.

// Old
const particles = this.add.particles("red");
const emitter = particles.createEmitter({
  speed: 100,
  scale: { start: 1, end: 0 },
  blendMode: "ADD",
});

// New: https://github.com/photonstorm/phaser/issues/6476#issue-1669398672
const emitter = this.add.particles(0, 0, "red", {
  speed: 100,
  scale: { start: 1, end: 0 },
  blendMode: "ADD",
});

After addressing these issues, I was able to get the Phaser game to load inline successfully. I also tried to an Astro client directive to load the scripts only when the client is idle, but this embeds the script as-is and I wouldn’t be able to use TypeScript, so I skipped it.

All in all, I converted the tutorial to TypeScript, loaded the assets locally, fixed the particle emitter, loaded Phaser into a <div> tag and added some styles so it would fit on the page and not be too tall. The final source2 for the tutorial:

components/Phaser/LoadPhaser.astro

<script>
  import Phaser, { Types } from "phaser";

  const gameConfig: Types.Core.GameConfig = {
    type: Phaser.AUTO,
    width: "100%",
    height: 300,
    parent: "phaser-1",
    physics: {
      default: "arcade",
      arcade: {
        gravity: { y: 200 },
      },
    },
    scene: {
      preload: preload,
      create: create,
    },
  };

  function preload() {
    this.load.image("sky", "/phaser/assets/space3.png");
    this.load.image("logo", "/phaser/assets/phaser3-logo.png");
    this.load.image("red", "/phaser/assets/red.png");
  }

  function create() {
    this.add.image(400, 300, "sky");

    const emitter = this.add.particles(0, 0, "red", {
      speed: 100,
      scale: { start: 1, end: 0 },
      blendMode: "ADD",
    });

    const logo = this.physics.add.image(400, 100, "logo");

    logo.setVelocity(100, 200);
    logo.setBounce(1, 1);
    logo.setCollideWorldBounds(true);

    emitter.startFollow(logo);
  }

  const game = new Phaser.Game(gameConfig);
</script>

MDX

import LoadPhaser from "../../components/Phaser/LoadPhaser.astro";

<LoadPhaser />;

I’m looking forward to learning more about Phaser and continuing to document my progress on my blog. I also hope to find a follow-up tutorial that isn’t from 2018 and uses the latest Phaser API. I’m not sure what I’ll build with Phaser, but I’m sure I’ll find something fun to make.


FootnotesSection titled %5Bobject%20Undefined%5D %5Bobject%20Undefined%5D

  1. This blog is built with Astro, which was created by the folks behind Snowpack. Since Snowpack is now deprecated, Astro uses Vite under the hood.

  2. The source for the entire blog is available on GitHub.