Cleaning Event Handlers on Scene Transitions

Before reading this, it might be helpful to see this article:

And also, this is my project where I found some parts to suggest feedback.

Suggestion

Although a single developer can solve the problem that I will indicate, it is better to make an option for a developer to clean up event handlers when a scene changes. Currently, it seems the event handlers in an old hierarchy remain even though the app destroys the root entity.

Explanation

I'm currently developing a game that contains multiple scenes and may have a loop around those scenes in the next version.

The scenario of “Mound Simulator” consists of three phases:

A player, who enters the game, will face a Menu scene firstly. Then, when the player clicks a “Let’s Play” button, the scene called “Main” will start and initiate every script in entities. When a player completes an inning (since it is a baseball game), a Result page will appear and suggest two options: Returning to the Menu scene or playing another Main scene.

The entities in a hierarchy have successfully remove when I use the following script module.

const ChangeScene = pc.createScript('changeScene');

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

ChangeScene.prototype.initialize = function() {
    this.entity.on("load:scene", this.loadScene, this);
};

ChangeScene.prototype.loadScene = function () {
    // Get a reference to the scene's root object
    let oldHierarchy = this.app.root.findByName('Root');
    
    // Get the path to the scene
    let scene = this.app.scenes.find(this.sceneName);
    
    // 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);
        }
    });
};

However, some event handlers in other entities and script modules remain and stay open. Due to the leftovers, the new scene still reacts to the entities in the previous scene, even though it disappears.

For example, the following screenshot shows the error after a player enters the Main scene, returns to the Menu, and opens Settings UI. (In addition, the Main scene also has identical Settings UI entity with a few more functions and event handlers)

// initialize code called once per entity
PitcherUI.prototype.initialize = function() {
    this.app.inningMaster.on('pitch', this.onPitch, this);
    this.app.inningMaster.on('conclude', (ball) => this.showUmpireText(ball), this);
    this.app.inningMaster.on("reset", this.reset, this);
    
    this.app.on("change:settings", this.onChangeSettings, this);
};

PitcherUI.prototype.postInitialize = function() {
    this.onChangeSettings();
};

PitcherUI.prototype.onChangeSettings = function() {
    switch (parseInt(this.app.settings.pitchTraceDisplay)) {
        case 0:
            this.entity.script.arrowUI.fire("enable:inputUI", true);
            this.entity.script.mouseHistoryUI.fire("enable:inputUI", false);
            break;
        case 1: 
            this.entity.script.arrowUI.fire("enable:inputUI", false);
            this.entity.script.mouseHistoryUI.fire("enable:inputUI", true);
            break;
    }
};

The newly implanted event handler on “change:settings” in the Main Scene stays active while “this.entity.script.arrowUI” no longer exists. Therefore the game throws such an error.

So I add additional lines in the module ChangeScene:

ChangeScene.prototype.loadScene = function () {
    // Get a reference to the scene's root object
    let oldHierarchy = this.app.root.findByName('Root');
    
    // Get the path to the scene
    let scene = this.app.scenes.find(this.sceneName);
    
    // New Lines
    this.app.off();
    if (this.app.mouse) this.app.mouse.off();
    if (this.app.touch) this.app.touch.off();

    // 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);
        }
    });
};

And now, the application refreshes the obsolete event handlers every time a scene changes. Those old event handlers may re-initialize on each scene initialization while not unnecessarily duplicating itself.

Is it better to purge any event handler in the destroyed entity? I actually have no clue on the deeper part of the Playcanvas engine, so this question is not that rhetorical. Anyway and anyhow, a developer have to turn off the event handlers while loading another scene. Aside of my suggestion, is there any other elaborate method to manage the event handlers?

1 Like

Hi @Heein_Park,

Interesting game! To your question regarding events I think it’s a matter of best practices used by the developer. One way to approach this is similar to constructors/destructors in C/C++.

On every script that you attach events, make sure that on script disable or destroy you detach those events. Usually it’s just a matter of copy/paste and setting on → off.

Events attached on entities that will be destroyed don’t need to be detached, the destroy() method on the entity will remove those event handlers.

If you would like a more radical way to approach the problem, you can dive in the pc.Events class in the engine for app, mouse etc. and loop through the _callbacks property and remove all of your custom events. Just be extra careful to not remove any event handlers the engine uses internally (e.g. ‘frameend’).

I see your point.

Do those attached events handlers become detached only when an app destroys the exact associated entities? Or does the detachment also happen when the parent entity of such entities removes?

When an entity is destroyed all of its children are destroyed as well, and their event handlers are detached.

Here is the relevant part in the engine source code:

1 Like

I found the fundamental reason for the old event handlers that never remove despite the entity clean-up.

All the event handlers that listen to “this.app” remain because they aren’t attached to specific entities. Even if the system destroys the entities, the handlers survive. After the struggles with these disasters, making event handlers for “this.app” seems like a dangerous idea.

Meanwhile, I put this.app.off() in the code that handles scene transitions. It appears to work fine, except “this.app.renderLines” stops working. I guess the engine uses some communications to render lines on a graphic device directly. When I block it, however, the function gets a bug. Then the “this.app.off()” solution is also too radical to apply generally.

Oh yeah, I would strongly suggest not using this.app.off() since you will be removing internal events that the engine uses.

You can still use app events to fire app wide events, just make sure to pass a specific listener when calling off, or a specific event category for the system to detach.

Example removing a specific event listener:

MyScript.prototype.myListener = function(){
   // do things when the event fires
};

MyScript.prototype.initialize = function(){

   // attach events
   this.app.on('MyEvent', this.myListener, this);

   // let's detach this event listener when this script instance is destroyed
   this.on('destroy', function(){
      this.app.off('MyEvent', this.myListener, this);
   });
};

If you have more than one event listeners for ‘MyEvent’ you can remove all of them in one go like this:

this.app.off('MyEvent');
5 Likes