Scene change error

Hello!

I have a big problem when I try change scenes in my project. I use Phaser 3 with TypeScript.

I have a couple of scenes:

  • PreloadScene - in this scene loading all assets, checking device screen orientation, loading script wich I used in all scenes at my project
  • LobbyScene - in this scene I have some filter, to find current match
  • RoomWndow - this is preload scene of my game, here a list of users
  • GameScene - main scene of my game

So, when my project starts - PreloadScene loading first, and then I do this.scene.launch('LobbyScene',{gameScript: this.gameScript});. My PreloadScene is still alive, and I have LobbyScene in parallel.

In LobbyScene, I have playButton:

playButton.button.on('pointerdown',()=>{
            this.gameScript.createOrJoinMatch()
                .then(r  =>(console.log('good')));
        },this);

In gameScript

public async createOrJoinMatch(){
   ...
   this.scene.pause('LobbyScene').launch('RoomWindow', {gameScript: this});
}

And my RoomWindow launched successfully. In this scene I have close button, that should close RoomWindow and continue LobbyScene.

this.cancelButton.button.on('pointerdown', async () => {
            this.gameScript.leaveMatch().then(r => (
                this.scene.stop('RoomWindow').launch('LobbyScene')
            ))
        }, this);

BUT after this code I have error:

image

Please help me, I already broke my brain(((

What’s in LobbyScene create?

In lobby scene I create filter, which player can use that find a similar match. I have text, sprites, images, and containers.

Code under create function below:

create() {
        this.bg = this.add.graphics().fillStyle(RummyConstants.BACKGROUND_COLOR, 1)
            .fillRect(0, 0, ResponsiveHelper.currentWidth, ResponsiveHelper.currentHeight);
        //region Panel body
        let panelHeader = this.add.graphics().fillStyle(RummyConstants.LOBBY_PANEL_HEADER_COLOR, 1)
            .fillRoundedRect(0, 0,
                ResponsiveHelper.currentWidth - (ResponsiveHelper.getX(26) * 2),
                ResponsiveHelper.currentHeight - (ResponsiveHelper.getY(17) * 2), 10);
        let headerText = this.make.text({
            x: ResponsiveHelper.getX(24),
            y: ResponsiveHelper.getY(6),
            text: 'Lobby',
            origin: {x: 0, y: 0},
            style: {
                font: 'bold 24px Roboto',
            }
        }).setScale(ResponsiveHelper.ratioX);
        this.balanceText = this.make.text({
            x: ResponsiveHelper.getX(550),
            y: ResponsiveHelper.getY(9),
            text: 'â‚ą 10000 INR',
            origin: {x: 0, y: 0},
            style: {
                font: 'bold 16px Roboto',
            }
        }).setScale(ResponsiveHelper.ratioX);
        let lobbyButtons = this.buttonsHelper.createLobbyButtons();
        lobbyButtons.setPosition(ResponsiveHelper.getX(650), ResponsiveHelper.getY(8))
            .setScale(ResponsiveHelper.ratioX);

        let panelBody = this.add.graphics().fillStyle(RummyConstants.LOBBY_PANEL_BODY_COLOR, 1)
            .fillRoundedRect(ResponsiveHelper.getX(1),ResponsiveHelper.getY(41),
                ResponsiveHelper.currentWidth - (ResponsiveHelper.getX(27) * 2),
                ResponsiveHelper.currentHeight - (ResponsiveHelper.getY(58 + 18) ),
                {
                    tl: 0,
                    tr: 0,
                    bl: 10,
                    br: 10
                });

        this.add.container(ResponsiveHelper.getX(26), ResponsiveHelper.getY(17),
            [panelHeader, headerText, this.balanceText, lobbyButtons, panelBody]);
        //endregion

        //region left section container
        let playersCountButtons = this.buttonsHelper.createPlayersCountLobbyButtons();
        let playersCountButtonsContainer = this.add.container(ResponsiveHelper.getX(52), ResponsiveHelper.getY(78),
            playersCountButtons);

        let pointsValueSection = this.setPointsValueSection();
        let pointsValueSectionContainer = this.add.container(ResponsiveHelper.getX(52), ResponsiveHelper.getY(167),
            pointsValueSection);

        this.entryFee = this.buttonsHelper.createStrokeButton(0, 0,
            188,42,false,'ENTRY FEE: â‚ą ');
        this.setPointsValueText();
        let playButton = this.buttonsHelper.createGradientButton(198, 0,
            136,42,
            RummyConstants.BLUE_BUTTON_TOP_GRADIENT_COLOR,RummyConstants.BLUE_BUTTON_BOTTOM_GRADIENT_COLOR,
            'Play Now');
        playButton.button.on('pointerdown',()=>{
            this.gameScript.createOrJoinMatch(this.playersCount, 800, 10)
                .then(r  =>(console.log('good')));
        },this);
        let playSectionContainer = this.add.container(ResponsiveHelper.getX(52), ResponsiveHelper.getY(291),
            [this.entryFee.button, this.entryFee.disableStroke, this.entryFee.enableStroke, this.entryFee.buttonText,
                playButton.button, playButton.buttonText]);
        playButton.mask.setPosition(playButton.button.x+playSectionContainer.x, playButton.button.y+playSectionContainer.y)

        let separatingLine = this.add.image(ResponsiveHelper.getX(414),ResponsiveHelper.getY(79),
            'separating_line').setOrigin(0,0).setScale(ResponsiveHelper.ratioY);

        // let leftSectionContainer = this.add.container(ResponsiveHelper.getX(0), ResponsiveHelper.getY(0),
        //     [playersCountButtonsContainer,pointsValueSectionContainer,playSectionContainer,separatingLine]);
        //endregion

        //region right section container
        let hotTableImage = this.add.image(0,0, 'hot')
            .setOrigin(0,0).setTint(0xFE0000).setScale(ResponsiveHelper.ratioX);
        let hotTableText = this.make.text({
            x: ResponsiveHelper.getX(20),
            y: 2,
            text: 'Join Our Hottest Tables',
            origin: {x: 0, y: 0},
            style: {
                font: '14px Roboto',
            }
        }).setScale(ResponsiveHelper.ratioX);
        //TODO: hot tables pagination
        let headerHotTableSectionContainer = this.add.container(ResponsiveHelper.getX(449), ResponsiveHelper.getY(84),
            [hotTableImage,hotTableText]);

        //endregion

    }

    setPointsValueSection(){
        let line1 = this.add.sprite(0,0,'line').setOrigin(0,0).setScale(ResponsiveHelper.ratioX,-ResponsiveHelper.ratioX);
        let line2 = this.add.sprite(0,ResponsiveHelper.getY(63),'line').setOrigin(0,0).setScale(ResponsiveHelper.ratioX);

        for (let i = 0; i <5; i++) {
            this.pointsValueList.push(
                this.make.text({
                    x: ResponsiveHelper.getX(31+24+55*i),
                    y: ResponsiveHelper.getY(20),
                    text: '0.0',
                    origin: {x: 0.5, y: 0},
                    style: {
                        font: '22px Roboto',
                    }
                }).setScale(ResponsiveHelper.ratioX)
            )
        }

        let arrow1 = this.add.sprite(ResponsiveHelper.getX(3), ResponsiveHelper.getY(20), 'arrow').setOrigin(0, 0).setInteractive()
            .on('pointerdown', () => {
                this.pointsValueText.unshift(...this.pointsValueText.splice(this.pointsValueText.length-1,1));
                this.setPointsValueText();
                this.buttonsHelper.clickSound.play();
            }, this).setScale(ResponsiveHelper.ratioX);
        let arrow2 = this.add.sprite(ResponsiveHelper.getX(321), ResponsiveHelper.getY(20), 'arrow').setOrigin(0, 0).setInteractive()
            .on('pointerdown', () => {
                this.pointsValueText.unshift(...this.pointsValueText.splice(1,this.pointsValueText.length-1));
                this.setPointsValueText();
                this.buttonsHelper.clickSound.play();
            }, this).setScale(-ResponsiveHelper.ratioX,ResponsiveHelper.ratioX);

        let text = this.make.text({
            x: ResponsiveHelper.getX(130),
            y: ResponsiveHelper.getY(75),
            text: 'POINTS VALUE',
            origin: {x: 0, y: 0},
            style: {
                font: '10px Roboto',
            }
        }).setTint(0x0AA9DB).setScale(ResponsiveHelper.ratioX);



        return [line1,line2,arrow1,arrow2,text,this.pointsValueList[0],this.pointsValueList[1],this.pointsValueList[2],
            this.pointsValueList[3],this.pointsValueList[4]]
    }

    setPointsValueText(){
        for (let i = 0; i <5; i++) {
            this.pointsValueList[i].text = this.pointsValueText[i].toFixed(1);
            this.pointsValueList[i].alpha = 0.3;
        }
        this.pointsValueList[2].alpha = 1;
        this.entryFee.buttonText.text = 'ENTRY FEE: â‚ą '+ (Number(this.pointsValueList[2].text) * 80);
    }

In my ButtonsHelper I have function, that helps me create a buttons:

createGradientButton(x, y, width, height, topGradient, bottomGradient, text) {
        let button = this.scene.make.graphics({x: 0, y: 0, add: true})
            .fillGradientStyle(topGradient, topGradient, bottomGradient, bottomGradient, 1)
            .fillRect(x, y, width, height)
            .setInteractive({
                // useHandCursor: true,
                hitArea: new Phaser.Geom.Rectangle(x, y, width, height),
                hitAreaCallback: Phaser.Geom.Rectangle.Contains,
            }).on('pointerdown', () => {
                this.clickSound.play();
            }, this)
            .setScale(ResponsiveHelper.ratioX);
        let mask = this.scene.make.graphics({x: 0, y: 0, add: true})
            .fillStyle(0xFFFFFF, 0)
            .fillRoundedRect(x, y, width, height, 4)
            .setScale(ResponsiveHelper.ratioX).setName("mask");
        button.setMask(mask.createGeometryMask());

        let buttonText = this.scene.make.text({
            x: (x + width / 2) * ResponsiveHelper.ratioX,
            y: (y + height / 2) * ResponsiveHelper.ratioX,
            text: text,
            origin: {x: 0.5, y: 0.5},
            style: {
                font: '14px Roboto',
            }
        }).setScale(ResponsiveHelper.ratioX);
        return {button, mask, buttonText}
    }

There was a bug in Phaser v3.55.2 which gave an error like this, but it involves destroying a container or its children, and I don’t see that happening here.

I have a feeling this issue is the same one. You don’t have to actively destroy the containers for it to occur, leaving the scene causes the same problem (because the scene will destroy the containers for you).

The “easiest” way to check for the bug is when you are about to leave the scene, cycle through all objects in the scene (this.children) and check to see if any of the scene properties are null/undefined - the error occurs because scene is undefined and it wants the sys property on it. If a child is a container, loop through all of its children as well, and check their scene properties (and do that recursively).

Alternatively, you can look through your code and see if you are adding a game object to a container, and that game object already belongs to a container (if you are adding it to the same container I don’t think the bug will occur).

A way to fix this issue (if you can’t wait for 3.60 or won’t be able to update to it), is to remove the game object from its parent container before adding it to another container. I will usually just throw if(myObj.parentContainer != null) myObj.parentContainer.remove(myObj) to the code anywhere I am adding a game object to a container and I suspect that it might sometimes already be in a container (I won’t bother doing this when I create my game object and initially add it to a container for example, but I might do it inside of a click callback where I can’t be sure if it’s in a container or not).

If you step through the relevant code in your browser’s debugger you may get some clues.

I try Phaser version 3.60 beta 10, and I still get this exception.

So my other suspicion is with the passing around of the gameScript instance. What is a gameScript instance? is that a GameObject, or just a regular class (does it extend Phaser.GameObjects.GameObject/Image/Container/etc)? How does this.scene get set inside of that class? When you leave a scene, that game object is going to get “cleaned up” if it’s in the scene’s display list, which will end up nulling out this.scene. There are ways to pass a game object from one scene to another, but it requires a bit of work. Even if it’s not a game object, if the scene it is holding is being destroyed (I destroy my Preloader scene’s since they are never used again for example, vs just “shutting down” the scene), it may cause this.scene to have issues as well. Might need more code to review to really be able to nail down the issue, and it may or may not be a Phaser issue at this point.

gameScript it’s just a regular class, that doesn’t extends anything. this.scene gets by preloadScene, access to other scenes in this script I have by this.scene.get('sceneName'). preloadScene doesn’t shutting down.

Not sure there is much else to do other than try and figure out where the error is happening, what object it is happening on, and then try to back-trace why it might be happening. I feel like this is a bug that is related to the container bug, but it is a bit different (the error stack trace is different than what I see when replicating the container bug, but it’s very similar). Without basically all of the code so that I can run it, not sure there is much else I can do to help pinpoint it. If you can provide the code, I can try doing a deep dive on it.

I find a solution for my problem! But it’s didn’t happen with containers…
In my project, I use compound objects that I can combine into an array, and then add to the container. My compound objects for example:

panel: {
   avatar: Phaser.GameObjects.Image
   name: Phaser.GameObjects.Text
}

In create() of my RoomWindow, I have an Array of Players Panels, which I filled in the following way:

for (let i = 0; i < 5; i++) {
            this.playersPanels.push({
                avatar: this.add.sprite(10*i, 10, 'loader_profile'),
                name: this.make.text({
                    x: 10*i,
                    y: 20,
                    text: '...',
                    origin: {x: 0.5, y: 0},
                    style: {
                        font: '12px Roboto',
                    }
                }),
            })
        }

I’m not entirely sure what the error was, but the solution to this problem was the following code:

for (let i = 0; i < 5; i++) {
            this.playersPanel[i] = {
                avatar ...

Interesting. I’m wondering if the issue was the array isn’t being cleared out if you start the scene again? Where do you define the this.playersPanels array? If you don’t clear it out when you leave the scene (this.playersPanels = []) or set it back to [] in init or create, it will still have the values in it from the previous time the scene was ran. Not sure if you were seeing this error on the first time you left the scene, or only on the second time. Might at least explain why your fix works, which wouldn’t really do much if the array was empty anyway.

1 Like

The first time the scene opened normally, the scene closed well too. The error always occurred only on the second opening of the scene. An array of user panels is declared in RoomWindow. I did not clear this array on purpose, I thought that this should happen automatically when .stop() is called

Properties on the scene object aren’t cleared/reset unless that is done explicitly in init/create or your leave function (you can listen for the scene stopping for example and do a cleanup). Phaser GameObjects are destroyed, but any properites holding references to them are not cleared, so the 2nd time through a scene if you expect a property to be null/undefined it won’t be if it happened to be set the 1st time through, but the object in the property might be a destroyed Phaser GameObject, which will cause errors.

The scene object itself is never destroyed unless you do this.scene.remove(mySceneKey). Stopping a scene only stops it (which causes the Phaser GameObjects to be destroyed), but it doesn’t destroy the scene, so your constructor (if you have one) for the scene is only ever called once. Not sure if you are declaring the initial array in your constructor. I would try adding this.playersPlanels = [] either to your “leave” function (if you have one) or to the init/create function, and see if it still fixes it without your change.

https://github.com/samme/phaser3-faq/wiki/Scenes#example-invalid-game-objects-after-restart

Yes, you’re right!
This error occurred because I don’t clear my objects and variables…
I thought that .stop() clear all objects and variables in my scene, and the error in debug console made me think in a different direction.
Thank you all for your help, I’ll be more careful in the future)))