Why does Phaser3 use init instead of the constructor?

With Typescript, this doesn’t work:

class Foo extends Phaser.Scene {
  player: Player
  init(args: { player: Player }) {
    this.player = args.player;
  }
}

The compiler helpfully reminds me player might be undefined, so I have to scatter player? all over my nice clean code.

Why can’t I just do this?

class Foo extends Phaser.Scene {
  player: Player
  constructor(args: { player: Player }) {
    super();
    this.player = args.player;
  }
}

Am I crazy? The syntax for starting a new scene doesn’t make a whole lot of sense either -
this.game.scene.start('Foo', { player: p });
I have everything nicely typed, why not:
this.game.scene.start(new Foo(p));?

1 Like

init() will be called every time the scene restarts, the constructor not.

You can set "strictPropertyInitialization": false in your tsconfig.json file.

1 Like

init() is not like a constructor. It’s a method hook, like create().

You can use that constructor function. You may want to pass arguments to super().

Starting a scene is different from instantiating a scene. Scenes are instantiated when added, then started any number of times.

1 Like

But why is starting a scene different from instantiating a scene? In usual OOP parlance, if you want persistent data on a class across instances you use static variables. Why isn’t it “the scene class is added but not instantiated, then instantiated any number of times?” I extend scene, using an OOP design pattern, then there are magic methods on the extended class that fight against the design pattern.

Starting a scene is different than instantiating a scene so you can re-use scenes without recreating the entire scene object. “Restarting” a scene is the Phaser / Scene Manager way to reload a scene, and you can keep values of data you set in the constructor.

There’s nothing stopping you from instantiating your own scene, however, and injecting it yourself into the scene manager at your discretion. Phaser’s Scene Manager functions (the hooks @samme is referencing) is different than the actual scene TypeScript object that is instantiated with the constructor when the game begins (or whenever you want to instantiate it).

An example is something I did for work, we had a mini-game where a user had to answer a sum questions 3 separate times as a math activity. The “Sum” scene had functions like so

// within Sum scene
constructor(){
  //super();
  this.numberOfSubmissions = 0;
}

create(){
   //set up onSubmit function
   //create is run every time the scene is restarted
}

onSubmit(){
   this.numberOfSubmissions++;
   if (this.numberOfSubmissions === 3) {
      console.log('user wins');
  }
}

We were able to use scene restarting with this, because the this.numberOfSubmissions variable is not overridden (the actual scene object is not being re-created / destroyed). This is a useful pattern for my use case and I think part of the intention of the scene manager.

In my case, my scene was “started” repeatedly for different questions until the user submitted a correct answer three times.

1 Like

I mean a scene is instantiated only when added to the game (specifically, to the Scene Manager). The Scene Manager holds scene instances only. When it adds a scene instance, it can start the instance, or not. A scene instance may never start, or it can start a hundred times.

The simplest approach is usually to add all your scene classes upfront, then start/switch them as needed.

Can you share how you want to use your scene and how it uses player? Maybe we can frame this in more practical terms.

There are a few non-ideal cases like this when using TypeScript and while I also would prefer to have everything a class needs set in the constructor I don’t think it is possible in a world where we don’t want to create/destroy Scenes for every start/stop.

I generally handle the player might be undefined problem by using the non-null assertion operator when defining the class property, eg: player!: Player I am just going to trust that Phaser will call init and that my player will be set.

It is not the most ideal but it is better than using the optional-chaining operator everywhere or having to check if player exists first.

3 Likes

In Phaser, scene instantiation is not a very significant event. It means the scene was added to the Manager, that’s all. An extended scene class doesn’t even need a constructor.

The init() hook is meant as The scene started, now you can do your setup.

2 Likes

This seems like a good approach compared to turning off strict checks everywhere. I think the worry about creating/destroying scenes for every start/stop is unfounded though. Object creation in Javascript is pretty cheap, and there’s not much of a price to be paid by calling the constructor function versus calling an init function, I think constructor will assign some prototype properties, and might traverse the inheritance tree which should be shallow. It’s still a bit weird to me that Phaser3 would commit to Typescript and ES6 and then ignore some basic language features for a custom implementation.

There is no custom implementation. The object life cycle is not the scene life cycle.

Also, nothing is stopping you from doing

this.scene.add('foo', new Foo({ player: Player }));

You can pass anything you want to the scene constructor.

There is no custom implementation. The object life cycle is not the scene life cycle.

I kind of need to disagree with this, the fact that the object life cycle is divorced from the scene life cycle is the “custom” part. Just to compare with React:

class Foo extends React.Component {
  constructor(props) {
    super(props);
    this.state = <some state>;
  }

  componentDidMount() { ... }

It’s true, there’s componentDidMount for post-constructor like actions, but the constructor provides a new Component, just like it says it will. The assignment to a state variable as opposed to using internal object properties is a case of React not really conforming to OOP design patterns, there it’s for different reasons (tracking if state has changed). React has a good reason for not conforming (Object.prototype.watch is expensive), I’m questioning if the logic of a custom lifecycle separate from the instance lifecycle is necessary. But this is getting pretty academic, and just telling the compiler on a case by cases basis “it’s ok, this variable is managed by the custom lifecycle” is a decent solution.

1 Like

I just had a bug because of this constructor-init divorce.

My class had a list of sprites

export class ManyExplosions implements Stuff {
  private sprites: Phaser.GameObjects.Sprite[] = [];

  create(scene: Phaser.Scene) {
    for (let i = 0; i < 10; i++) {
      const sprite = scene.add.sprite(0, 0, keys.animBoom);
      this.sprites.push(sprite);
    }
  }

And the scene class was being reused, so I didn’t realize I was collecting garbage sprites which eventually crashed. The fix in this case is to treat create like the constructor and use ! to denote there is no need for an initializer.

  private sprites!: Phaser.GameObjects.Sprite[];

  create(scene: Phaser.Scene) {
    this.sprites = [];
    for (let i = 0; i < 10; i++) {
      const sprite = scene.add.sprite(0, 0, keys.animBoom);
      this.sprites.push(sprite);
    }
  }

These are complications resulting from this design choice. @samme said:

The object life cycle is not the scene life cycle.

That is surprising when the object is literally a scene object. The only thing I can imagine that this is valuable for is if there are expensive initialization operations, they can happen just once in the constructor. But I’m not sure that feature is worth the mental baggage, because you can always make a class/object that is not part of the scene for that purpose.