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…
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.
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;
}
}
}
};
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…
And the script is a component of each of the Hotspot entities
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));
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.
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.
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.
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.
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.