PlayCanvas cant find model components even though they exist

My code uses the following line to find a model entity:

this.hotspot_plane_mesh = this.cutaway_hotspots[x].findByName(“Plane”).model ;

This works fine but the second time the scene is loaded I get the error: Uncaught TypeError: Cannot read property ‘model’ of null

I figured that it must be because the entity no longer existed but I ran the debug tools and the entity IS there on the second run. So why isnt it being found this time? See image…

I should add that if I add the line:
this.hotspot_entity = this.cutaway_hotspots[x].findByName(“Plane”);
this gets found every time, so its the model and not the entity thats not found.

Following on from this I printed the objects out that ONLY contained models … they seem to be there…
image

Without seeing the code, my guess is that you have an old reference to an entity in the previous scene that has it’s components destroyed/removed. A repro project would be great.

When I switch scenes the whole hierarchy gets destroyed.There should be no references left no?

If you have a global variable or maybe an event callback with a variable that hasn’t been cleaned up that is referencing an entity object, while the internals of the entity are destroyed, JS will still keep the object in memory as there is still a reference to it.

Again, without a repro or seeing more code, it is hard to tell what could be the issue.

Where and when are this.cutaway_hotspots[x] initialised? Is it in an initialise function? Then chances are it is the same issue as this: Hotspots not drawn - #14 by yaustar

The script does have mouse event callbacks. Maybe that’s whats keeping the reference? How would I completely destroy everything?

Script…PS. The findByName(“Plane”); is at the bottom of the script

var Hotspot = pc.createScript('hotspot');

//Hotspot.attributes.add("cameraEntity", {type: "entity", title: "Camera Entity"});//OLD from demo
Hotspot.attributes.add("id", {type: "number", title: "ID"});

Hotspot.attributes.add("radius", {type: "number", title: "Radius"});
Hotspot.attributes.add("fadeDropOff", {
    type: "number", 
    default: 0.4, 
    title: "Fade Drop Off", 
    description: "When to start fading out hotspot relative to the camera direction. 1 for when hotspot is directly inline with the camera. 0 for never."
});


// initialize code called once per entity
Hotspot.prototype.initialize = function() {
    
    //
    
    hotspot_highlightMaterial = this.app.assets.find("Hotspot_Highlight");
    
     this.cameraEntity=this.app.root.findByName("Camera_MV");
    
     //get the app manager script
    this.appManagerScript= this.app.root.findByName("AppManager").script.appManager;
    
    if( this.cameraEntity===null)
        {console.log("HOTSPOT CANT FIND CAMERA");}
    else
        {console.log("HOTSPOT FOUND CAMERA: "+this.cameraEntity.name);} //Returns Camera correctly
        
    
    // Create a hit area using a bounding sphere
    this.hitArea = new pc.BoundingSphere(this.entity.getPosition(), this.radius);
    // More information about pc.ray: http://developer.playcanvas.com/en/api/pc.Ray.html
    this.ray = new pc.Ray();
    
    this.defaultForwardDirection = this.entity.forward.clone();
    
    this.directionToCamera = new pc.Vec3();
    this.sprite = this.entity.children[0];
    
    
    //set the text on this hotspot to match the id
    this.hotspotText=this.entity.findByName("Text").element;//you can only reference objects
    this.hotspotText.text=this.id+1;
    
    // Register the mouse down and touch start event so we know when the user has clicked
    this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
    
    if (this.app.touch) {
        this.app.touch.on(pc.EVENT_TOUCHSTART, this.onTouchStart, this);
    }
    
    //reference to all other cutaway hotspots
    this.cutaway_hotspots=this.app.root.findByTag("cutaway_hotspots");
   // this.cutaway_hotspot_models=this.app.root.findByTag("cutaway_hotspots").model;
    console.log("INIT HOTSPOT");
   // this.hotspot_model = this.entity.findByName("Plane").model;
    this.hotspot_plane_mesh = this.entity.findByName("Plane").model;
};

Hotspot.prototype.postInitialize = function() {
   
};

// update code called every frame
Hotspot.prototype.update = function(dt) {
    
        var cameraPosition = this.cameraEntity.getPosition();

        // Always face the camera
        this.entity.lookAt(cameraPosition);

        // Get the current direction to the camera
        this.directionToCamera.sub2(cameraPosition, this.entity.getPosition());
        this.directionToCamera.normalize();

        // Get the dot product of the direction to the camera and the original direction of the hotspot
        // which is relative to the angle between the two vectors
        // Start fading out the hotspot if the dot product is below fadeDropOff
        var dot = this.directionToCamera.dot(this.defaultForwardDirection);
        if (dot < 0) {
            if (this.sprite.enabled) {
                this.sprite.enabled = false;
            }
        } else {
            if (!this.sprite.enabled) {
                this.sprite.enabled = true;
            }

            var meshInstances = this.sprite.model.meshInstances; 
            var alpha = pc.math.clamp(dot / this.fadeDropOff, 0, 1);
            for(var i = 0; i < meshInstances.length; ++i) {
                meshInstances[i].setParameter("material_opacity", alpha);
            }
        }

};

Hotspot.prototype.doRayCast = function (screenPosition) {
    // Only do the raycast if the sprite is showing
    if (this.sprite.enabled) { 
        
        
        
//          if( this.cameraEntity===null)
//         {console.log("HOTSPOT CANT FIND CAMERA");}
//     else
//         {console.log("HOTSPOT FOUND CAMERA: "+this.cameraEntity.name);}//Returns fine
        
//          if( this.cameraEntity.camera===null)
//         {console.log("NO COMPONENT ON CAMERA ENTITY");}
//          else
//         {console.log("COMPONENT ON CAMERA FOUND : "+this.cameraEntity.camera);}//Returns but its called Undefined
        
        
        this.cameraEntity=this.app.root.findByName("Camera_MV");//This works if a I put it here. But maybe its slow. Its probably finding the cam from previous scene so maybe quick fix is to rename camera
        
        if(this.cameraEntity!==null)
        {
                // Initialise the ray and work out the direction of the ray from the a screen position
                this.cameraEntity.camera.screenToWorld(screenPosition.x, screenPosition.y, this.cameraEntity.camera.farClip, this.ray.direction); 

                this.ray.origin.copy(this.cameraEntity.getPosition());
                this.ray.direction.sub(this.ray.origin).normalize();

                // If the hotspot is clicked on, then send a event to start the 'pulse' effect
                if (this.hitArea.intersectsRay(this.ray)) 
                {
                    this.entity.fire("pulse:start");
                    this.show_hotspot_information();
                }
        }
    }
};

Hotspot.prototype.onMouseDown = function(event) {
    if (event.button == pc.MOUSEBUTTON_LEFT) {
       this.doRayCast(event);
        
    }
};

Hotspot.prototype.onTouchStart = function (event) {
    // On perform the raycast logic if the user has one finger on the screen
    if (event.touches.length == 1) {
        this.doRayCast(event.touches[0]);
        
        // Prevent the default mouse down event from triggering
        // https://www.w3.org/TR/touch-events/#h3_list-of-touchevent-types
        event.event.preventDefault();
    }    
};


Hotspot.prototype.show_hotspot_information = function () 
{
    console.log("Showing Hotspot Info");
    //populate left hand window with info from the JSON data file
    
    //hide some text
    this.modelNameTextTitle=this.app.root.findByName("Text_Cutaway_Title").element;//you can only reference objects
    this.modelNameTextTitle.text="";
    
    //Hotspot Name
    this.modelNameText=this.app.root.findByName("Text_Cutaway_SubTitle").element;//you can only reference objects
    this.modelNameText.text=this.appManagerScript.mattressData.brands[this.appManagerScript.currentBrand].products[this.appManagerScript.currentMattress].hotspots.cutaway[this.id].info_1;
    
    this.modelNameText2=this.app.root.findByName("Text_Cutaway_Body").element;//you can only reference objects
    this.modelNameText2.text=this.appManagerScript.mattressData.brands[this.appManagerScript.currentBrand].products[this.appManagerScript.currentMattress].hotspots.cutaway[this.id].info_2;
    
    
    //change the hotspot image..
    this.hotspot_image_1=this.app.root.findByName("Mattress_Main_Image").element;//you can only reference objects
    this.hotspot_image_1_name = this.appManagerScript.mattressData.brands[this.appManagerScript.currentBrand].products[this.appManagerScript.currentMattress].hotspots.cutaway[this.id].image;
    var hotspot_imageAsset = this.app.assets.find(this.hotspot_image_1_name);
    
    console.log ("Trying to load "+this.hotspot_image_1_name);
    if( hotspot_imageAsset && hotspot_imageAsset.loaded)
    {
       this.hotspot_image_1.texture= hotspot_imageAsset.resource;
    } 
    
    
    //highlight this hotspot and unhighlight others
    var hotspot_normalMaterial= this.app.assets.find("Hotspot_Normal");
    //var hotspot_highlightMaterial = this.app.assets.find("Hotspot_Highlight");
    
    console.log("This hospot id is:"+this.id);
     console.log("this.cutaway_hotspots.length is :"+this.cutaway_hotspots.length );
    
     if(hotspot_highlightMaterial.loaded)
    {
        for(x=0;x<this.cutaway_hotspots.length > 0;x++)
        { 
            //set texture to normal
             this.hotspot_plane_mesh = this.cutaway_hotspots[x].findByName("Plane");
            this.hotspot_plane_mesh = this.cutaway_hotspots[x].findByName("Plane").model;// when the scene gets reloaded it cant find this mesh..why not???
            
            this.hotspot_plane_mesh.material = hotspot_normalMaterial.resource;

            console.log("SETTING DEFAULT MAT ON id "+x);
        }
        
        for(x=0;x<this.cutaway_hotspots.length > 0;x++)
        { 
            //set texture to normal
            
          
            //but set this hotspot the highlighted
            if(x===this.id)
                {
                     console.log("CHANGING MATERIAL on "+this.id);
                    this.hotspot_plane_mesh = this.cutaway_hotspots[x].findByName("Plane").model;
                    this.hotspot_plane_mesh.material= hotspot_highlightMaterial.resource;
                    //this.hotspot_model.material= hotspot_highlightMaterial.resource;
                }

        }
    }
    
    
};
    this.cutaway_hotspots=this.app.root.findByTag("cutaway_hotspots");

Does this have to be on the root object?

Where are these entities in relation to the entity that this script is on? Are they children of this entity or elsewhere?

Can you show a screenshot of the hierarchy and show where the entities tagged cutaway_hotspots are and also the entity where this script is attached to?

The plane is on a template (Hotspot) which is inside a parent template(Test_Cutaway). This ‘parent template’ is instantiated into the Root>Scene> node of the new scene. Here is the Test_Cutaway template…
image
And the script is a component of each of the Hotspot entities

Which entities are tagged ‘cutaway_hotspots’?

And the ‘Hotspot’ script is on Hotspot 0/1/2 etc ?

Hotspot 0,Hotspot 1, Hotpot 2, Hotspot 3 etc are the ones with the script AND the tag.

In which case, what I would do is change the scene root entity to something unique to the whole project:
image

Eg: ‘Scene1’

And change the code to:

    this.cutaway_hotspots=this.app.root.findByName('Scene1').findByTag("cutaway_hotspots");

The name of the entity (‘Scene1’) could be a script attribute on the hotspot script.

The issue here is the same reason here: Hotspots not drawn - #14 by yaustar

At the point that initialise is called during your loadScene logic, both scenes are loaded and the root.findByTag will find the scene you are switch from entities too.

However:

I would suggest using loadSceneData to load the JSON from the server, destroy the current scene hierarchy and then add the new scene hierarchy. This means that the JSON data can be loaded first with adding the new scene so you can destroy the old scene and add the new scene in the same frame.

An example of use load scene data in this context: PlayCanvas 3D HTML5 Game Engine (see console log for printing out of the model components on the tagged entities.

ChangingScenes.prototype.loadScene = function (sceneName) {
    // 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(sceneName);
    this.app.scenes.loadSceneData(scene, function (err, sceneItem) {
        if (err) {
            console.error(err);
        } else {
            oldHierarchy.destroy();
            this.app.scenes.loadSceneHierarchy(sceneItem, function(err, parent) {
                if (err) {
                    console.error(err);
                }
            });
        }
    }.bind(this));

Using the exact function above, but replacing:

var oldHierarchy = this.app.root.findByName ('Root');
with
var oldHierarchy = this.app.root.findByName ('Scene');

(because I don’t want to destroy everything at root level because that includes my app manager entity)

I get exactly the same error when returning to the scene.(Uncaught TypeError: Cannot read property ‘model’ of null) Although the entities themselves exist.

This looks like it would need a reproducible project to look at properly

I think you are already shared on it here…PlayCanvas 3D HTML5 Game Engine

To run the project you will need to run it from the init scene.

The hotspot script is in script/Misc/hotspot.js
and the opencene is in script/Misc/openScene.js

To repro in app choose Purple>Purple 4 > At this point choosing the cutaway option (bottom right) and clicking on hotspots will work fine.
Now go back to the previous scene with the back/up arrow…then return (Purple 4) and do the same again and you will see the error.

From the repro steps and a quick look at the code, it looks like the issue I mentioned earlier in the thread: PlayCanvas cant find model components even though they exist - #4 by yaustar

You need to unsubscribe from the touch and mouse events when the script is destroyed:

You have the following code in hotspot:

    // Register the mouse down and touch start event so we know when the user has clicked
    this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
    
    if (this.app.touch) {
        this.app.touch.on(pc.EVENT_TOUCHSTART, this.onTouchStart, this);
    }
    

But there isn’t anywhere where it unsubscribes.

So when you load the scene the second time, the old callback is still registered to the mouse and touch events and all the related data is still in memory due to the callback reference.

Add the following to the initialise function:

this.on('destroy', function()  {
    this.app.mouse.off(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
    
    if (this.app.touch) {
        this.app.touch.off(pc.EVENT_TOUCHSTART, this.onTouchStart, this);
    }

}, this);

OKay great, that works perfectly. I still don’t quite understand why the destroy function doesn’t do this by default on an entity when it gets destroyed (why would I want to keep events available for entities that don’t exist? Is there a reason?) but I guess I know now to bear this in mind.
Thanks very much.

The event system is generic, it’s not always a script instance that subscribes to the event so it has no way to know if the object that is subscribing to the event is PlayCanvas script instance or a generic JS object.

At the same time, the script has no idea what event’s it is subscribing to (as it’s an object outside the ownership of the script instance) or whether the developer may want to persist beyond the lifetime of the script instance.

The rule of thumb is that if it are subscribing to something, it is also responsible for unsubscribing.

1 Like

That said, we cooooould write a wrapper function on the script type API that does this or you could write your own :thinking:

Yes, do that :rofl:

oldHierarchy.destroy(andUnsucribeEvents=true);

To be honest though, I cant see any occasion when anyone would actually need to keep the events of an entity that no longer exists - especially if it just causes errors.

Don’t forget about engine only users or those that listen to these events in code that lives outside the lifetime of an Entity.

Eg: playcanvas.github.io/mouse.html at master · playcanvas/playcanvas.github.io · GitHub