Performance trouble with Containers and displayLists

My game board renders correctly. But when I start using timers, it starts to look like there has been a massive performance problem all along: a 2-second timer takes 17 seconds to fire the first time.

The visible scene renders completely within the first paint. Subsequent events fire at the correct 2-second interval.

I’m on a beefy M2 Macbook with graphics and RAM upgrades, and so far my “game” has zero interactivity or behavior (it just renders the game board from a data struct and color data), so I think the problem is not the expected performance cost of Containers, but that I’m setting up my constructs such that Phaser cannot possibly cope.


My game is played on a grid, like a crossword puzzle. It is assembled of a shallow hierarchy of Container, Rectangle, and Text instances, like so:

  • Scene (“Gameboard”) x1
    • Container (“Tile”) x 800
      • Rectangle x1
      • Text x1

Right now I’m trying to render about 800 Tiles. They are static now, but eventually each will need to be updateable semi-frequently during gameplay.

A Tile is just a colored square with a number printed on it; here are a few:

several Tile instances

All of this renders exactly how I want, helped in part by the local coordinate space created by each Container.

I’m using Phaser’s GameObject factory tech, so when the scene wants to create a Tile, this is the sequence of steps:

  1. Scene adds a Tile: scene.add.Tile(...)
  2. GOFactory makes new Tile; the Tile constructor does the following:
    1. invokes Container super
    2. makes new Phaser.GameObjects.Rectangle
    3. adds the rect to self: tileContainer.add(rect)
    4. makes new Phaser.GameObjects.Text
    5. adds the text to self: tileContainer.add(text)
  3. GOFactory explicitly adds the new Tile instance to the scene’s displayList and updateList

Again, this all renders exactly the way I want, and the code reads cleanly. There’s no place where it looks like I’m fighting the framework or cutting against the grain.

Finally, in the scene’s create method, after all the Tiles are generated, I create a repeating 2-second timer that does console.log:

scene.time.addEvent({
  delay: 2000,
  loop: true,
  callback: () => console.log('timer @ ' + msDeltaSinceStart)
})

It always takes about 17 seconds for that first log statement to appear.

This problem goes away when I stop manually add-ing Phaser constructs to the display hierarchy:

  • if GOFactory stops explicitly adding the new Tile to the scene’s displayList:
    • the timer fires after 2 seconds (on time), but none of the Tiles are visible
  • if I drop tileContainer.add(rect):
    • the timer fires after 3 seconds (late), but the colored regions are rendered at world 0,0
  • if I drop `tileContainer.add(text):
    • the timer fires after 16 seconds (very late), but then the text is rendered at world 0,0

My efforts to figure this out have failed so far.

It makes no difference whether I use the bare constructors or convenience scene factories to create the Rectangle and Text instances.

If I stop manually connecting the display hierarchy, I don’t get any benefits from using containers, in particular the local coordinate system.

The docs for Container say:

When a Game Object is added to a Container, the Container becomes responsible for the rendering of it. By default it will be removed from the Display List and instead added to the Container’s own internal list.

I’m doing this already, and it appears to lead directly to the abysmal performance.

I read samme’s wiki page on Containers, but did not spot my mistake.


A couple of general notes:

I’m still trying to figure out what is a workable pattern within Phaser for constructing “composite” GameObjects. Container seems like the obvious fit. I also want to make my custom GOFactory do some useful housekeeping across the board, so I’m trying to figure out the right division of responsibilities between a generic Container-based GO and the scene’s factory.

I thought I had finally hammered out a workable pattern for all that, until I started playing with timers this week and noticed that performance was so bad as to be unplayable.


Here are the relevant code snippets. I hope I’ve captured all and only what you need to see:

// scenes/gameboard.js
create() {
  let scene = this
  let tileCount = 800 // example only
  
  // generate Tiles for the play area
  for(let t = 0; t < tileCount; t++) {
    // omitted: position and color math from data source
    scene.add.Tile(
      thisTileLeftX,
      thisTileTopY,
      { rgb, cellIndex: t }
    )
  }

  // set up demo timer
  let timerStart = new Date().getTime()
  scene.time.addEvent({
    delay: 2000,
    loop: true,
    callback: () => { console.log('timer @ ' + (new Date().getTime() - timerStart)) }
  })
}
// custom-game-objects/tpr-scene/my-game-object-factory
// note that `esClass.name` will be "Tile" at runtime
Phaser.GameObjects.GameObjectFactory.register(esClass.name, ( ...objectConstructorArgs ) => {
  let instance = new esClass(scene, ...objectConstructorArgs)

  // omitted: useful housekeeping, e.g. give every object a unique string ID and add to global registry

  this.displayList.add(instance)
  this.updateList.add(instance)

  return instance
})
// custom-game-objects/tile.js
class Tile extends Phaser.GameObjects.Container {

  constructor( scene, x, y, tileOpts = {} ) {
    super(scene, x, y)
    
    // STEP 1: create colored square
    this.gTile = scene.add.rectangle(0, 0, TILE_WIDTH_PX, TILE_WIDTH_PX, 0x000000, 0)
    this.add(this.gTile) // NOTE: omit this, and the Rectangle uses world coordinates

    // STEP 2: create visible text label
    let labelText = '13'
    this.gLabel = scene.add.text(0, 0, labelText, TextStyles.Tile.Label)
    this.add(this.gLabel) // NOTE: omit this, and the Text uses world coordinates
  }
  
  // NOTE: omit this, and scene crashes
  preUpdate() {}

}

I ran this and got a 4s lag before anything rendered. It’s from the creation of 800 Text objects.

You could create a bitmap font and use BitmapText instead, or you could create 1 multiframe CanvasTexture and draw all the tiles on that.

2 Likes

Thanks! I think you may be right that special textures are the way to go.

Upon further reflection, I think my experiments showed that the performance hit comes mostly from the Rectangles. I know they have a cost, and I’ve been told to use them sparingly for that reason (possibly by you!).

So, since each tile will be a solid color anyway, I’m going to try creating a texture in RAM whose width & height match the width-in-tiles & height-in-tiles of the gameboard, treat each pixel as one tile, and render that texture at 24x scale. I’m hoping that:

  • Using a 1px-per-tile scale will make writing new colors to the texture as efficient as possible.
  • A straight 24x enlargement will be computationally trivial for Phaser and underlying tech.
  • The enlargement will not be blurry, but I’m not optimistic. If it is, I’ll try other ratios, approaching 1:1.

I’ll post a follow-up once the verdict is in. Thanks again for taking a look!


I will add: I’m reading raw pixel data out of raster images for some stuff in another Phaser game, and setting it up was kind of awkward: one can’t use the texture as loaded by the image preloader, one has to create a special CanvasTexture and copy the pixel data from the preloaded texture into it, and then read (or write) to that. So now usable texture is an extra thing I have to manage, and if there will be a dynamic number of those textures, I’ll need to design a system for managing them. I avoided that before because it felt like over-engineering, and I’m trying to avoid rabbit-holes. If this experiment works, it will be time to cross that Rubicon.

If these kinds of challenges keeps cropping up, I will start to feel like a real game dev instead of a biz-app dev in a hat & trenchcoat.

Here are the details of what I did to solve the performance problem, although I’ll admit this is incomplete (more on that at the bottom):

  1. Create one new CanvasTexture for the entire grid area, and display it as a Sprite.
  2. Use regular HTML5 canvas drawing methods to efficiently draw colored rectangles on that CanvasTexture.
  3. To capture mouse events on tiles, the canvas sprite listens for pointer events on itself, and uses the mouse’s pixel coordinates to figure out which tile is the target for the benefit of downstream code.

Like so:

// step 1
const tilesCanvasTexture = scene.textures.createCanvas(
  'canvas/tiles',
  playAreaRect.widthPx, playAreaRect.heightPx
)
const tilesSprite = scene.add.sprite(
  playAreaRect.x, playAreaRect.y,
  tilesCanvasTexture
)

// step 2
function fillTile( tileCanvasTexture, tileQ, tileR, rgba ) {
  const canvasContext = tilesCanvasTexture.getContext('2d')

  // apply scaling factor; my tiles are 24px square
  let pixelX = tileQ * TILE_SIZE_PX
  let pixelY = tileR * TILE_SIZE_PX
  
  // boring but efficient HTML5 canvas drawing
  canvasContext.fillStyle = `rgb(${rgba.join(',')})`
  canvasContext.fillRect(pixelX, pixelY, TILE_SIZE_PX, TILE_SIZE_PX)
  
  tileCanvasTexture.refresh() // TODO: batching
}

// step 3
tilesSprite.on(
  'pointerdown',
  wrapHandlerWithTileCoords(onTilePointerDown)
)

This reliably runs for all 800+ tiles in less than 50 ms, and the scene’s 2-second birthday timer always fires on-schedule.


That’s not all of what I was trying to accomplish. I still want a Phaser Container for each tile so I can easily draw stuff “at” desired tiles, such as the Text in my original post. I want both the local coordinate system of Containers, and the benefits of a game-object API to help with all of that stuff.

I think I’ll generate tile Containers on-demand, whenever something happens “at” a tile, possibly using Phaser’s object pool system if there is lots of expensive thrashing in practice. I have not attempted any of that yet, but it seems like it would be straightforward.

I am pretty sure I will end up encapsulating all of this in some kind of “Gameboard” subclass that talks in terms just of tile data and grid-space coordinates.

1 Like