Not always getting Scene Context when using a Class

I made a dialog box appear when the player move over a GameObject with a physics body (which I call a ‘location’)

I wanted the dialog box stay visible for 2 seconds even when the player move away from the ‘location’

The uses can click the text in the dialogbox to confirm entering the ‘location’

As functions in the scene it all worked. But now that I moved it to a Class I get a small error, “Uncaught TypeError: Cannot read properties of undefined (reading ‘add’)”

I only get the error once when loading the scene, the Dialogbox code works, but the error does interfere with the execution of other code in the scene.

This is my Class:

import ManageSession from "../ManageSession"

class LocationDialogbox {
    constructor(scene) {
        this.locationGameObject
        this.scene = scene
        this.show = false
        this.player
    }

    create(scene, locationObject, locationName, mainWidth, mainHeight) {

        scene.add.existing(this)
        //scene.physics.add.existing(this)

        this.locationGameObject = locationObject
        this.scene = scene
        console.log(scene)
        this.player = scene.player

        locationObject.setData("entered", false)
        locationObject.setName(locationName)

        //create variable for the text of the dialog box, set the text after
        let nameText = "scene." + locationObject.name + "DialogBox"
        nameText = scene.add.text(mainWidth - 60, mainHeight - 30, locationName, { fill: '#000' })

        //create variable to hold dialogbox graphics
        let nameBox = "scene." + locationObject.name + "DialogBox"

        //background panel for dialogbox
        nameBox = scene.add.graphics();
        nameBox.fillStyle(0xfffff00, 0.4)
        nameBox.fillRoundedRect(0, 0, mainWidth, mainHeight, 32)
        nameBox.setVisible(false)

        //create variable for texture that holds the graphics and the clickable area for the dialogbox
        let nameTexture = "scene." + locationObject.name + "Texture"

        nameTexture = scene.add.renderTexture(0, 0, mainWidth, mainHeight);
        nameTexture.draw(nameBox);
        nameTexture.setInteractive(new Phaser.Geom.Rectangle(0, 0, mainWidth, mainWidth), Phaser.Geom.Rectangle.Contains)
        nameTexture.on('pointerdown', () => { this.enterLocationScene(scene, locationObject.name) });

        //create container that holds all of the dialogbox: can be moved and hidden
        let nameContainer = "scene." + locationObject.name + "DialogBoxContainer"

        // nameContainer = scene.add.container(locationObject.x - (mainWidth / 2), locationObject.y - (mainHeight / 2), [nameTexture, nameText]).setDepth(900)
        nameContainer = scene.add.container(locationObject.body.x + (locationObject.body.width / 4), locationObject.body.y + (locationObject.body.height / 4), [nameTexture, nameText]).setDepth(900)

        nameContainer.setVisible(false)
        nameContainer.setName(locationObject.name)

        //add everything to the container
        scene.locationDialogBoxContainersGroup.add(nameContainer);

        //call overlap between player and the location, set the callback function and scope
        // more info about passing arguments to callback function https://phaser.discourse.group/t/passing-argments-into-functions/4411/2
        scene.physics.add.overlap(scene.player, locationObject, this.confirmEnterLocation, null, this)


        scene.add.existing(this)
        scene.physics.add.existing(this)
    }

    confirmEnterLocation(player, locationObject) {
        //console.log(locationObject)
        // locationObject = this.locationGameObject
        let show

        if (!locationObject.getData("entered")) {
            //start event
            show = false
            //   scene.time.addEvent({ delay: 2000, callback: this.enterLocationDialogBox, args: [player, location, show], callbackScope: this, loop: false })
            setTimeout(this.enterLocationDialogBox, 2000, locationObject, show)

            //show the box
            show = true
            this.enterLocationDialogBox(locationObject, show)
            locationObject.setData("entered", true)
        }
    }

    enterLocationDialogBox(locationObject, show) {
        //console.log(locationObject)
        let scene = locationObject.scene || this.scene

        scene.add.existing(this)
        //scene.physics.add.existing(this)

        let nameContainer = locationObject.name

        let container = Phaser.Actions.GetFirst(scene.locationDialogBoxContainersGroup.getChildren(), { name: nameContainer })
        //console.log(container)
        // console.log(container)

        if (show) {
            container.setVisible(show)
        } else {
            container.setVisible(show)
            locationObject.setData("entered", show)
        }
    }

    enterLocationScene(scene, location) {
        //const locationScene = location + "_Scene"

        ManageSession.previousLocation = scene.scene.key
        ManageSession.currentLocation = location

        scene.physics.pause()
        scene.player.setTint(0xff0000)

        //player has to explicitly leave the stream it was in!
        console.log("leave, scene.location")
        console.log(scene.location)
        ManageSession.socket.rpc("leave", scene.location)

        scene.player.location = location
        console.log("scene.player.location:")
        console.log(location)

        setTimeout(() => {
            ManageSession.location = location
            ManageSession.createPlayer = true
            ManageSession.getStreamUsers("join", location)
            scene.scene.stop(scene.scene.key)
            scene.scene.start(location)
        }, 500)

    }
}

export default new LocationDialogbox()

I thought it had to do with the scene context not being passed down the class, so I added a global this.scene that gets set in the beginning but that doesn’t help.

Here is the code I use in the scene:

  this.locationDialogBoxContainersGroup = this.add.group();
 
    this.location1 = this.add.isotriangle(300,300, 150, 150, false, 0x8dcb0e, 0x3f8403, 0x63a505);
    
    this.physics.add.existing(this.location1);
    this.location1.body.setSize(this.location1.width, this.location1.height)
    this.location1.body.setOffset(0, -(this.location1.height / 4))
   
    LocationDialogbox.create(this, this.location1, "Location1", 200, 150)

What am I doing wrong? Thanks :slight_smile:

Export the class:

export default LocationDialogbox;

Then instantiate in the scene:

const locationDialogBox = new LocationDialogbox(this);

Thanks Samme for the reply. Declaring an object from my class didn’t solve my problem, but it did get me thinking about the class more deeply, and I wrote it now from the point of view of the constructor (and not a method).

I rewrote it and now it works :slight_smile:

Maybe it was the setTimeout I was using that was throwing the error: the scope of this in there is tricky. I replaced that with a scene.time.addEvent

This is the class as an extension of container I ended up writing:

import ManageSession from "../ManageSession"

export default class GenerateLocation extends Phaser.GameObjects.Container {

    constructor(config) {

        super(config.scene, config.x, config.y)

        this.scene = config.scene
        this.x = config.x
        this.y = config.y
        this.locationImage = config.locationImage
        this.backButtonImage = config.backButtonImage
        this.locationText = config.locationText
        this.fontColor = config.fontColor
        this.locationDestination = config.locationDestination
        this.backButton
        this.showing = false
        this.color1 = config.color1
        this.color2 = config.color2
        this.color3 = config.color3
        this.userHome = config.userHome
        this.location

        //TODO don't make a location in a container, the depth order seems to be shared across the contianer, so we can't make the enter button appear above the player, and the location below the player
        //TODO rewrite with out the container, just using sprite, containers are a bit more cpu intensive

        //display width of the location image/ triangle/ isoBox
        const width = 128

        // the content of the container is created at 0,0
        // then the container is set at a position

        // image for the location, physical body for collision with the player
        //setOrigin(0.5) in the middle

            this.scene.textures.exists(this.locationImage)
            this.location = this.scene.physics.add.image(0, 0, this.locationImage).setOrigin(0.5, 0.5).setDepth(50)

            //set the location to a fixed size, also scales the physics body
            this.location.displayWidth = width
            this.location.scaleY = this.location.scaleX
      

        //place the description under the location image (for devving only)
        const locationDescription = this.scene.add.text(0, width / 2 - 30, this.locationText, { fill: this.fontColor }).setOrigin(0.5, 0.5).setDepth(51)

        // back button that appears 
        this.backButton = this.scene.add.image(0, -(width / 2) - 60,
            this.backButtonImage).setInteractive().setVisible(false).setOrigin(0.5, 0.5).setDepth(200)

        this.scene.tweens.add({
            targets: this.backButton,
            y: -90,
            alpha: 0.0,
            duration: 1000,
            ease: 'Sine.easeInOut',
            repeat: -1,
            yoyo: true
        })

        //the container is created at the this.x and this.y
        //this.setSize(width, width)
        this.add(this.location)
        this.add(locationDescription)
        this.add(this.backButton)

        this.setSize(width, width, false)

   

        this.backButton.on('pointerdown', () => {

        })

        this.backButton.on('pointerup', () => {
            // on entering another location we want to keep a record for "back button"
            ManageSession.previousLocation = this.scene.key;

            this.scene.physics.pause()
            this.scene.player.setTint(0xff0000)

            //player has to explicitly leave the stream it was in!
            console.log("leave, this.location")
            console.log(this.scene.location)
            ManageSession.socket.rpc("leave", this.scene.location)

            this.scene.player.location = this.locationDestination
            console.log("this.player.location:")
            console.log(this.locationDestination)

            this.scene.time.addEvent({ delay: 500, callback: this.switchScenes, callbackScope: this, loop: false })
        })

        this.scene.physics.add.overlap(this.scene.player, this.location, this.confirmEnterLocation, null, this)

        this.scene.add.existing(this)
        this.scene.physics.add.existing(this)
    }



    confirmEnterLocation(player, location) {
        this.confirmConfirm()
        this.backButton.setVisible(true)
    }

    hideEnterButton() {
        this.showing = false
        this.backButton.setVisible(this.showing)
    }

    confirmConfirm() {
        if (this.showing) {
        } else {
            this.showing = true
            this.scene.time.addEvent({ delay: 2000, callback: this.hideEnterButton, callbackScope: this, loop: false })
        }
    }

    switchScenes() {
        ManageSession.location = this.locationDestination
        ManageSession.createPlayer = true
        ManageSession.getStreamUsers("join", this.locationDestination)
        this.scene.scene.stop(this.scene.scene.key)
        //check if it is a userHome, pass data to the userHome (user_id)
        if (this.userHome) {
            this.scene.scene.start(this.locationDestination, { user_id: this.userHome })
            console.log("UserHome defined: ", this.userHome)
        } else {
            this.scene.scene.start(this.locationDestination)
        }
    }
}