Best practice for "composite" game objects?

I’m making an airline tycoon game. I’m trying to draw an 8-bit airplane with a tail number beneath it, and then I want to make it fly from A to B over the course of a couple of minutes.

I have a version working that uses setVelocity on a physics sprite, but the tail number text does not move with the plane. And I’m realizing I don’t know what is the best practice for assembling complex visualizations out of the basic displayable Phaser constructs. So that’s my real question, but I’ll use Plane as my example.

Plane is a custom class. Until I needed Planes to move, Plane extended Sprite:

class Plane extends Phaser.GameObjects.Sprite {
  constructor( scene, x, y, tailNumber ) {
    super(scene, x, y, 'airplane')
    
    this.vLabel = scene.add.text(x, y + 15, tailNumber, TextStyle.PlaneName)
    this.setInteractive()
  }
}

So, this is the airplane Sprite, and this.vLabel holds a reference to a Text GameObject.

Now I need to make Planes move, so I added no-gravity physics to the game and modified Plane to be the physics version of Sprite so I could setVelocity to make it move:

class Plane extends Phaser.Physics.Arcade.Sprite {
  constructor( scene, x, y, tailNumber ) {
    super(scene, x, y, 'airplane')
    scene.physics.world.enableBody(this, Phaser.Physics.Arcade.DYNAMIC_BODY)

    this.vLabel = scene.add.text(x, y + 15, tailNumber, TextStyle.PlaneName)
    this.setInteractive()
  }

  fly = () => {
    this.setVelocity(/*...*/)
  }
}

That airplane moves, but the tail number doesn’t.

It has always seemed kind of hokey to me that my custom game items are each built around some kind of “primary” renderable construct (such as a sprite), with a bunch of additional object references dangling off them. For one thing, it means that click handlers only fire when the gameobjectup event occurs over the “primary” construct: if someone clicks the tail number text, it won’t fire as a click on the Plane instance. (I have this same problem with Airport objects, which have a visible icon representing the airport, plus text of the city name, and some crude adjacent shapes to represent the condition of the airport. Clicking on the icon counts as a click on the city, because the icon is the “primary” element of the City object, but clicking the label text or the shapes does not count.)

I was looking at the Container class, but there’s a scary warning about performance costs, which is not the kind of thing I would expect to see on something we’re supposed to use as our bread-and-butter. And I don’t require the nicety of local coordinates for my purposes.

I tried converting Plane to extend Phaser.GameObjects.GameObject, and then make the constructor create the physics sprite and the piece of text, but it crashes at runtime:

class Plane extends Phaser.GameObjects.GameObject {
  constructor( scene, x, y, tailNumber ) {
    super(scene)
    // blows up immediately
  }
}

// Uncaught TypeError: gameObject.preUpdate is undefined

How can I make the tail number text move with the plane graphic? What would the constructor look like for a custom game object that needs a physics sprite, a non-physics image, some text, and maybe some additional shapes? What is a good general pattern here?

class Plane extends Phaser.Physics.Arcade.Sprite {
  constructor(scene, x, y, tailNumber) {
    super(scene, x, y, 'airplane');
    scene.physics.world.enableBody(this);

    this.vLabel = scene.add.text(x, y + 15, tailNumber, TextStyle.PlaneName);
    this.setInteractive();
  }

  fly() {
    this.setVelocity(/*...*/);
  }

  // update() isn't called automatically
  update() {
    const { x, y } = this.body.center;

    this.vLabel.setPosition(x, y + 15);
  }
}

I noticed that custom classes’ update don’t get called automatically. (Which makes me wonder what is the purpose of the scene’s updateList. But I digress.)

It looks like some folks do use Container.

I’m also thinking I might try using startFollow, although that also seems a little hokey.

I wonder if there are any other ideas out there.

1 Like

Container is best if you want all children transformed by the parent. I wasn’t sure how you want the label to appear.

This is pretty frustrating.

Container is not adequate because you can’t setVelocity on a Container. I can still setVelocity on an individual physics sprite within the Container, but then only the physics sprite moves (i.e. the label text does not), which is exactly the problem I’m trying to solve.

I think it’s not correct to make this a physics Group, because I don’t want any of the other items in this collection to participate in physics. (The docs also recommend that all the items in a Group be of the same kind, and I want very precisely to do the opposite, and it seems like a bad idea to disregard advice from the framework authors.)

It seems like cameras are the only things that have startFollow, so I can’t use that to make the other items follow the collection’s “primary” item.

Seriously, how do people do this?

This appears to do exactly what I want:

  1. custom Plane class extends Container; its constructor creates all the little sub-pieces: physics sprites, text, shapes, etc.
  2. when you extend Container, Phaser seems to require that you also define a preUpdate(time, delta) method on your subclass; in that method, I explicitly update the positions of each of the sub-pieces by re-assigning into their x & y coords

it-flies-rightward

Like so:

export default class Plane extends Phaser.GameObjects.Container {

  constructor( scene, x, y, name, size ) {
    super(scene)

    this.vIcon = scene.physics.add.sprite(x, y, 'airplane')
    this.vLabel = scene.add.text(x, y + 15, name, TextStyle.PlaneName)
    this.vLabel.setOrigin(0.5)

    scene.add.existing(this)
  }


  fly() {
    this.vIcon.setVelocity(10, 0)
  }

  preUpdate = (time, delta) => {
    this.vLabel.x = this.vIcon.x
    this.vLabel.y = this.vIcon.y + 15
  }

}

A couple notes:

  • Updating the coords in preUpdate is simply a matter of re-running the same calculation that the constructor uses to position the item in the first place. If it really bothers you to copy/paste that math in two places, you could refactor the position calculations into private functions within the same module. Or you could probably not bother to set the position in the constructor at all, and just rely on math in preUpdate to set their positions on the next frame draw. That might result in a brief initial flash of glitchy layout, depending on how the lifecycle plays out.

  • I’ve seen elsewhere that folks call super.preUpdate within their custom class’s preUpdate method, but it throws when I try that (Phaser v3.54). So I just do the position assignment.

Frankly, I think it’s great. I feel like I’m making Phaser do almost all the work here.