[SOLVED] Errors upon going between two scenes

The issue: When I go from scene A -> scene B -> scene A again, some references become undefined

Caveats / Apology: I cannot share the project due to NDA, I have not been able to make a smaller test version to illustrate the problem because I don’t know exactly what causes it, and I am also using 8thWall XR. I know this makes it harder to debug, I appreciate any insights folks have regardless.

About Scene Loading: I know that it’s additive and not really recommended, I felt I needed to use it as this (will be) a rather large project and I wanted a way to split out the parts. I am using a slightly modified version of the example scene loading project. Code below:

var SwitchScenesOnclick = pc.createScript('switchScenesOnclick');

SwitchScenesOnclick.attributes.add("sceneName", {type: "string", default: "", title: "Scene Name to Load"});

SwitchScenesOnclick.attributes.add("t_LoadingScreen", {type: "asset", assetType: "template", title: "Template for Loading Scene"});

SwitchScenesOnclick.attributes.add("b_TurnOffXR", {type: "boolean", title: "Turn off XR?"});

SwitchScenesOnclick.attributes.add("b_IsFace", {type: "boolean", title: "Turn off Face Effects?"});

SwitchScenesOnclick.attributes.add("s_EventName", {type: "string", default: "touchstart", title: "Name Of Event"});

SwitchScenesOnclick.prototype.initialize = function(dt) {
    
    this.entity.button.on(this.s_EventName, this.loadScene.bind(this));

};


SwitchScenesOnclick.prototype.loadScene = function (e) {
    
    e.stopPropagation();
    
    if(this.b_TurnOffXR){
        if(this.b_IsFace){
            XR8.PlayCanvas.stopFaceEffects();
        } else {
            XR8.PlayCanvas.stopXr();
        }
    }
    
    setTimeout(this.loadHelper.bind(this), 10);
    
};

SwitchScenesOnclick.prototype.loadHelper = function () {
    
    // Get a reference to the scene's root object
    var oldHierarchy = this.app.root.findByName ('Root');
    
    // Get the path to the scene
    var scene = this.app.scenes.find(this.sceneName);
    
    var self = this;
    
    // Load the scenes entity hierarchy
    this.app.scenes.loadScene(scene.url, function (err, scene) {
        if (!err) {
            oldHierarchy.destroy();
            pc.ComponentSystem.initialize(scene.root);
            pc.ComponentSystem.postInitialize(scene.root);
        } else {
            console.error(err);
        }
    });
    
};

Errors: here are the two errors I get depending on the route I go through the game. The first two “errors” on the first screenshot are from console.error to demonstrate which part of the line is undefined.

image

here is the relevant code snippet:

BackgroundManager.prototype.bindedEndGame = function(n_CoinsCollected, n_CoinsTotal, b_Won) {
    
    var self = this;
    
    if(typeof(n_CoinsCollected) !== "number"){
        console.error("Expected n_CoinsCollected to be a number but it was a " + typeof(n_CoinsCollected));
    }
    if(typeof(n_CoinsTotal) !== "number"){
        console.error("Expected n_CoinsTotal to be a number but it was a " + typeof(n_CoinsCollected));
    }
    if(typeof(b_Won) !== "boolean"){
        console.error("Expected b_Won to be a boolean but it was a " + typeof(b_Won));
    }
    
    if(b_Won){
        //you won
        self.e_WinBackground.enabled = true;
    } else {
        //you lost
        self.e_LoseBackground.enabled = true;
    }
    
    self.e_EmptyBackground.enabled = true;
    self.e_Blackground.enabled = true;
    
    console.error("self.e_CoinsCollectedText: " + self.e_CoinsCollectedText);
    console.error("self.e_CoinsCollectedText.element: " + self.e_CoinsCollectedText.element);
    console.error("self.e_CoinsCollectedText.element.text: " + self.e_CoinsCollectedText.element.text);
    
    self.e_CoinsCollectedText.element.text = n_CoinsCollected + "/" + n_CoinsTotal;
    
};

image

here is the relevant code snippet for this one, i’ve commented the line it’s erroring on:


StarHolder.prototype.setElements = function(self, delta){
    
    self.numCollected += delta;
    if(self.numCollected < 0){
        self.numCollected = 0;
    }
    
    for(var i = 0; i < self.elements.length; i++){
        if(i < self.numCollected){
            self.elements[i].element.color = self.elements[i].script.pointNotch.onColor; //cannot read pointNotch of undefined
            self.elements[i].element.opacity = self.elements[i].script.pointNotch.onColor.a;
        } else {
            self.elements[i].element.color = self.elements[i].script.pointNotch.offColor;
            self.elements[i].element.opacity = self.elements[i].script.pointNotch.offColor.a;
        }
    }
};

Sorry for the long post, I wanted to make sure to be thorough.
Thanks for your help!

HI @Ezra_Szanton,

Would you mind putting your code samples in code blocks using three backquotes?

Like this?

It makes it easier to read.

Regarding your issue, do you have any events subscribed to app? As in:

this.app.on('foo', this.bar, this);

or

this.app.on('foo', function() {
 this.doStuff();
}, this);
3 Likes

Thanks so much for the tip! I was trying to format it before but this is waaay better :slight_smile:

I do have quite a few events subscribed to the app as you laid out (it’s how I’m doing almost all my communication between scripts)

Could you elaborate on the relationship between this pattern and the error?

1 Like

@Ezra_Szanton,

No problem!

So, every time you switch scenes in PlayCanvas, the app event is attempting to access the attributes of the object that was previously destroyed, which is why you’re getting the undefined errors. In Playcanvas’s mind, that object no longer exists. To avoid this, you can unsubscribe from those events each time the scene is destroyed. Like this:

ScriptExample.prototype.initialize = function() {
  this.on('destroy', this.onDestroy, this);
};

ScriptExample.prototype.onDestroy = function() {
  this.app.off('foo');
};

or

this.on('destroy', function() {
  this.app.off('foo');
}, this);

The event should resubscribe when the scene is loaded again assuming you’re subscribing to it in the initialize function.

4 Likes

ohhhhh that makes so much sense! I never considered that the error might be coming from a function on an object that doesn’t exist anymore.

I’m going to try implementing this fix tomorrow. You’re a life saver!

1 Like

It worked! Thank you so much

2 Likes

No problem!

One thing you might want to consider in the future is to fire and subscribe to events at the entity level. That way you reduce the number of global events that are fired, and when the entity is destroyed you won’t have to unsubscribe as the event will no longer be fired by the previous entity. To track the entity events you could create a ‘manager’ object with the script that listens to the entity events. Something like:

-Manager-

ObjectManager.attributes.add('entities', {
  type: 'entity',
  title: 'Entities',
  description: 'Entities to listen for events',
  array: true
});

ObjectManager.prototype.initialize = function() {

  this.entities.forEach(function(entity) {
    entity.on('foo', this.bar, this);
  }.bind(this));

};

ObjectManager.prototype.bar = function() {
doStuff();
};

-Object-

ObjectScript.prototype.initialize = function() {

this.entity.fire('foo');

};

Of course the above code requires ES6, but you get the idea. Keeping events at the entity level means you don’t have to worry about going back to unsubscribe from them in every script.

2 Likes

I appreciate it, at first blush my impression of this approach is that it would not be very performant. Is that intuition justified?

@yaustar or @Leonidas should absolutely correct me if I’m wrong,

But my understanding is that you can run into performance issues with app based events because the event is broadcasted to every object with a subscription to app. That means even if you do not fire the event that a specific object is listening to, it still has to read that event and determine that it does not have to do anything.

Global events like app can become more costly as you scale up the number of events being listened for since every object hears every other object’s event. Using events at the entity level means that is only listening for the events related to that object.

Again, don’t take my word for it. There is definitely a possibility that I’m wrong. Hopefully, some clarification might appear in this thread.

1 Like

I think what @eproasim says is correct, under the hood pc.Application, pc.Entity, pc.Asset etc all use the same class pc.Events for event handling.

Performance in that case comes down to how many event listeners are firing for each event. Limiting the scope by using entity bound events I’d say will definitely help with performance if that event would normally fire a dozen of app wide listeners.

I personally use entity events a lot in my projects, it helps organize things and target specific entities.

3 Likes

I’ll look into it more - thanks!