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”) x1Container
(“Tile”) x 800Rectangle
x1Text
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:
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:
- Scene adds a Tile:
scene.add.Tile(...)
- GOFactory makes
new Tile
; theTile
constructor does the following:- invokes
Container super
- makes
new Phaser.GameObjects.Rectangle
- adds the rect to self:
tileContainer.add(rect)
- makes
new Phaser.GameObjects.Text
- adds the text to self:
tileContainer.add(text)
- invokes
- GOFactory explicitly adds the new
Tile
instance to the scene’sdisplayList
andupdateList
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() {}
}