Type-safe Phaser Scene changes and some updates about my game's backend

Type-safe Phaser Scene changes and some updates about my game's backend

Amir Eldor's photo
Amir Eldor

Published on Aug 2, 2021

4 min read

What's up? In this post I'll write how I use some TypeScript hints for writing the Phaser client for my online web persistent-universe single/co-op player game. Also, I'll mention some substantial architecture changes I had to make to the backend when I realized I was doing things that are too complicated and brittle and should KISS.

Migrating from Excalibur js, which proved to not be stable enough for me currently, both documentation-wise and API-wise, I decided to switch to Phaser. The switch was planned rather early in the project timeline and only took a few Pomodoros to make the whole change. I was up and running again rather quickly. I must say that I am very impressed with Excalibur, they are doing a great job. Will definitely follow that project in the future.

One thing that I did with Excalibur which I wanted to keep with my Phaser adventures was typed scene name changes. From past project I learned that it's always good to have a BaseScene of your own extending the Phaser scene, so with that I could add some TypeScript magic to help me. First, I declared my scenes in a type and a variable, which looks like this:

// Data a scene might get in `init`
export type sceneDataDefs = {
  loader: undefined;
  galaxy: undefined;
  solarSystem: { starId: string };
};

// Scene name to actual class implementation mapping
export const sceneDefs: Record<keyof sceneDataDefs, typeof Phaser.Scene> = {
  loader: LoaderScene,
  galaxy: GalaxyScene,
  solarSystem: SolarSystemScene,
};

Here I defined both my available scenes as well as any parameters (data definitions) they might be getting on their init, like the SolarSystemScene which needs to know which star it shows. A nice bonus I get from declaring my scenes this way is in my Phaser gameConfig:

// I can declare all my phaser scenes in my gameConfig easily
scene: Object.values(sceneDefs),

And now, in my base scene, I did something like this. I should've however overridden the existing Phaser methods, I think I'll try that soon and let you know how that worked for me.

// In my BaseScene, I declare this typed method:
goToScene<T extends keyof sceneDataDefs>(
  scene: T,
  data?: sceneDataDefs[T]
): void {
  this.scene.start(scene, data ?? undefined);
}

We get type-safe scene changes:

gotoscene.png

As I mentioned, it would probably be wiser to override existing Phaser scene-change methods. Let's try this together.

class TypeSafeSceneManager extends Phaser.Scenes.ScenePlugin {
  start<T extends keyof sceneDataDefs>(
    scene: T,
    data?: sceneDataDefs[T]
  ): this {
    return super.start(scene, data);
  }

  switch<T extends keyof sceneDataDefs>(scene: T): this {
    return super.switch(scene);
  }

  // I might be missing some methods, but the idea stays the same
}

Re-type the scene object in the BaseScene:

export class BaseScene extends Scene {
  scene!: TypeSafeSceneManager;
  ...

Oh my, that works! How nice is it writing a dev-blog and discovering new stuff. We get scene name completion and data type-safety.

scenestarttyped.png

banana.png

apples.png

🙂

Some updates to how things work in the backend

Previously, for building ships in the backend, I wanted to create a delayed Redis or some other queue task. As I approached implementing that, I realized that from a gameplay perspective the player might cancel a build, or research or improve their production capabilities, making the colony ship building faster. I would have had to cancel the delayed task and create a new one. That doesn't sound fun, right? It's not. It would have been a hell to test and debug and it would be a host of many, many bugs that I could foresee. Plans cancelled.

KISS. Eventually I decided that I should go the game.tick() way and just implement a method that updates the whole world. Meh. It's ugly and I wanted to avoid that, but it seems to be the simplest solution and maybe even the right solution at the moment. I hacked up a game tick manager which on startup creates a promise-queue as well as add new games to the queue when these are created. It was not thought of very thoroughly but I didn't want to have all my games updated at the same time for premature optimization reasons. I actually don't feel like writing about that at the moment, but in general I create a promise that calls game.tick, waits a bit, and then re-inserts itself to the back of the queue.

I'm excited, I feel the first POC version of the game is getting in shape slowly. I got my ClickUp Gantt set up and I follow that successfully at the moment. That version will probably not be public yet at the beginning, but drop me a line if you're interested to check it up.

 
Share this