Dynamic script loading not working (can't use preload)

Would like to dynamically load script assets and have them function as intended. When preload is off, the project does not work. My application is a web site that streams assets and entities to the client so i think I cannot preload.

https://playcanvas.com/project/676704/overview/video

https://playcanv.as/p/JCCqQWZV/

I see that without preload the scripts are not loaded at all. Why is this?

If you do not mark them to preload, then you need to load them manually. You can see this example how to load an asset:

Once the script is loaded, you can add it to your entity:

entity.addComponent('script');
entity.script.create('myScript', {
  attributes: { ... }
});

I was adding the component in my application but not the second part of creating the script. Thanks

It still doesn’t seem to be loading the attributes. Is there a way to load them manually as the document suggests?

You are probably doing something wrong then. Please, post your script loading code and how you are reading attributes, or make a small repro project.

the server sends ‘data’ via socket and enqueues it with the queueItem type ‘data’, as the queueItem resource:

// initialize code called once per entity
Network.prototype.initialize = function() {

    this.queue = new Queue();
    this.isQueueRunning = false;
    this.progress = 0;
    this.progressExpected = 0;

    var socket = io.connect(CYBERS_CAFE_SERVICE_URL);
    Network.socket = socket;

    socket.on ('addRecord', function (data) {
        self.queue.enqueue(new QueueItem('data', data));
        if (!self.isQueueRunning) {
            self.popQueue();
        }
    });

  //...

furthermore, the network.js file handles the loading of playcanvas entities and assets


//****
// Overall, the popQueue method is responsible for dequeueing items from the queue, 
// checking their types, and calling the appropriate methods. 
// It also updates the progress bar and handles recursive calls to the popQueue method (subroutines call popQueue() )
Network.prototype.popQueue = function() {
	if (!this.isQueueRunning) {
	  this.isQueueRunning = true;
	}

	var queueItem = this.queue.dequeue();
  if (queueItem) {
    console.log("Dequeuing " + queueItem.type + ", queue length: " + this.queue.getLength());
    if (queueItem.type == 'asset') {
        this.addAsset (queueItem.resource);
    } else if (queueItem.type == 'entity') {
        this.addEntity (queueItem.resource);
    } else if (queueItem.type == 'glbEntity') {
        this.addGlbEntity (queueItem.resource);
    } else if (queueItem.type == 'record') {
        this.addRecord (queueItem.resource);
    } else if (queueItem.type == 'data') {
        this.addAssetsAndEntitiesAndRecord (queueItem.resource);
    } else if (queueItem.type == 'delete') {
        this.deleteRecord (queueItem.resource);
    } else {
        console.log("Unknown QueueItem type: " + queueItem.type);
    }
  } else {
    console.log("Queue is empty");
  }

  if (this.queue.getLength() === 0) {
    this.isQueueRunning = false;
  }

};

//****
// this method gets called by popQueue() first,
// and calls addAsset() and addEntity() methods as needed (via the popQueue() method)
Network.prototype.addAssetsAndEntitiesAndRecord = async function(data) {
    const self = this;

    if (data.record.id in this.app.storage.records) {
        console.log('Record already added: ' + data.record.id);

        // if the record is present because it was loaded before
        // the entities might have been turned off from leaving the loation range 
        // so we still need to re-add the record

        // continue readding the record now
    }

    console.log('Add Record ' + data.record.id);
    if (!(data.record.id in self.app.storage.records)) {
        self.queue.enqueue(new QueueItem('record', data.record));
        if (!self.isQueueRunning) {
            self.popQueue();
        }
    }

    Object.keys(data.assets).forEach(function (assetId) {
        const asset = data.assets[assetId];
        console.log('Add Asset ' + asset.id);
        if (!app.assets.get(asset.id)) {
            self.queue.enqueue(new QueueItem('asset', asset));
            if (!self.isQueueRunning) {
                self.popQueue();
            }
        }
    });
    
    var rootEntity = null;
    if (data.record.glbAssetId != null) {
        // loads glb entities
        self.queue.enqueue(new QueueItem('glbEntity', {asset: data.assets[data.record.glbAssetId]}));
        if (!self.isQueueRunning) {
            self.popQueue();
        }
    } else {
        // loads classic zip entities
        rootEntity = data.entities[data.record.rootEntityId];
        // load entity (enqueue locally for loading)
        self.queue.enqueue(new QueueItem('entity', {entity: rootEntity, entities: data.entities}));
        if (!self.isQueueRunning) {
            self.popQueue();
        }
    }


    self.popQueue();

    // update the progress bar
    this.progress++;
    if (this.progressPercent == null) {
        this.progressPercent = 0;
        $('#progress-inner-div').attr('aria-valuenow', 0).css('width',0);
        $('#progress-div').css('visibility', 'visible');
    }
    if (this.progressExpected) {
        const percent = this.progress / this.progressExpected * 100;
        if (percent > this.progressPercent) {
            this.progressPercent = percent;
            $('#progress-inner-div').attr('aria-valuenow', this.progressPercent).css('width',this.progressPercent + '%');
        }
    }
    
    setTimeout(function(){
      if (self.progress >= self.progressExpected) {
        self.progress = 0;
        self.progressExpected = 0;
        self.progressPercent = null;
        $('#progress-inner-div').attr('aria-valuenow', 100).css('width','100%');
        setTimeout(function(){
          $('#progress-div').css('visibility', 'hidden');
          $('#progress-inner-div').attr('aria-valuenow', 0).css('width','0%');
        },3000);
      }     
    },5000);

}

// *****
// this method is called by addAssetsAndEntitiesAndRecord() via popQueue as needed
Network.prototype.addEntity = function(data) {
    var entityData = data.entity;
    var entities = data.entities;

    var entity = new pc.Entity();
    entity = this.prepareEntity(entity, entityData);

    this.app.root.addChild(entity);
    this.resizeImageEntity(entity);
    entity.added = true;
    this.addChildren(entity, entities);
    this.app.storage.entities[entity.id] = entity;

    console.log('Root entity ' + entity.id + ' added');
    console.log(entity);

    this.popQueue();

};

// ******
// this method is called by addEntity()
Network.prototype.prepareEntity = function(entity, data) {
    for (var componentName in data.components) {
    	if (componentName == "camera" || componentName == "script") {
    		continue;
    	}
        const component = entity.addComponent(componentName, data.components[componentName]);
        if (component == null) {
            throw new Error("Unable to add component " + componentName + " to entity " + entity.id);
        }
    }
//########################################################################################## script loading
    // scripts
    try {
        if (entity.script == null) {
            entity.addComponent("script");
        }
        for (var scriptName in data.components.script.scripts) {
            const script = data.components.script.scripts[scriptName];
            const attributes = script.attributes;
            entity.script.create(scriptName, {"attributes": attributes, "preloading": true});
        }
    } catch (err) {
        console.log(err);
    }


   // general properties
    entity.id = data.resource_id;
    entity.resource_id = data.resource_id;
    entity.record_id = data.record_id;
    entity.title = record.title;
    entity.name = data.name;

 // (positioning and so on)
 // ...

}

// *****
// this method is called by addAssetsAndEntitiesAndRecord() via popQueue() as needed
Network.prototype.addAsset = function(data) {
    // from playcanvas application.js: _parseAssets()
    var asset = new pc.Asset(data.name, data.type, data.file, data.data);
    asset.id = data.id;
    asset.preload = data.preload ? data.preload : false;
    asset.tags.add(data.tags);
    asset.revision = data.revision;

    try {
      this.app.assets.add(asset);
    } catch (err) {
    	console.log("Asset error: " + err);
	return;
    }
   
    var self = this;
    var onAssetLoad = function(asset) {
        self.app.storage.assets[asset.id] = asset;
        
        console.log('Asset ' + asset.id + ' added');
        console.log(asset);
    
        self.popQueue();
    };
    
    if (asset.resource) {
        // The asset has already been loaded 
        onAssetLoad(asset);
    } else {
        // Start async loading the asset
        asset.once('load', onAssetLoad);
        try {
          this.app.assets.load(asset);
        } catch (err) {
        	console.log("Asset error: " + err);
        }
    }
};

you can see the assets are being queued to be loaded first before the entities. i tried it the other way around and it wasnt working either.

this is from the video project mentioned. the materials and video are just null instead of having the set attributes

var Videotexture = pc.createScript('videotexture');

Videotexture.attributes.add('materials', {
    type: 'asset',
    assetType: 'material',
    array: true
});

Videotexture.attributes.add('video', {
    type: 'asset'
});

Videotexture.attributes.add('playEvent', {
    title: 'Play Event',
    description: 'Event that is fired as soon as the video texture is ready to play.',
    type: 'string',
    default: ''
});

// initialize code called once per entity
Videotexture.prototype.initialize = function() {
    var app = this.app;

    // Create a texture to hold the video frame data            
    var videoTexture = new pc.Texture(app.graphicsDevice, {
        format: pc.PIXELFORMAT_R5_G6_B5,
        autoMipmap: false
    });
    videoTexture.minFilter = pc.FILTER_LINEAR;
    videoTexture.magFilter = pc.FILTER_LINEAR;
    videoTexture.addressU = pc.ADDRESS_CLAMP_TO_EDGE;
    videoTexture.addressV = pc.ADDRESS_CLAMP_TO_EDGE;

    // Create HTML Video Element to play the video
    var video = document.createElement('video');
    video.addEventListener('canplay', function (e) {
        videoTexture.setSource(video);
    });
    video.src = this.video.getFileUrl();
    video.crossOrigin = 'anonymous';
    video.loop = false;
    video.volume = 1;
    video.muted = false; // This line allows video to play without prior interaction from the user
    video.autoplay = false;
    video.playsInline = true;

    //video.play();

    // Get the material that we want to play the video on and assign the new video
    // texture to it.
    for (var i = 0; i < this.materials.length; i++) {
        var material = this.materials[i].resource;
        material.emissiveMap = videoTexture;
        material.update();
    }

    this.videoTexture = videoTexture;
    this.videoDom = video;
    
    this.upload = true;
};


Videotexture.prototype.postInitialize = function() {    
    //this.app.fire('video:playing');
    
    // Add pause/play controls event listeners
    // this.app.on('video:play-pause-toggle', function () {
    //     if (this.videoDom.paused) {
    //         this.videoDom.play();
    //         this.app.fire('video:playing');
    //     } else {
    //         this.videoDom.pause();
    //         this.app.fire('video:paused');
    //     }
    // }, this);

    this.app.on('button:clicked:PlayButton',function (e) {
        this.videoDom.play();
//        this.app.fire('video:playing');
        this.app.fire(this.playEvent, this.videoTexture);
    }.bind(this));

    // this.app.on('button:clicked:PauseButton',function (e) {
    //     video.pause();
    //     video.currentTime = 0;
    // }.bind(this));

    // this.app.on('button:clicked:ReplayButton',function (e) {
    //     console.log(video.currentTime);
    //     video.currentTime = video.currentTime - 10;
    //     console.log(video.currentTime);
    // }.bind(this));
};


// update code called every frame
Videotexture.prototype.update = function(dt) {
    // Upload the video data to the texture every other frame
    // this.upload = !this.upload;
     if (this.upload) {
        this.videoTexture.upload();
     }
};

the error i get is as follows:

TypeError: Cannot read properties of null (reading 'getFileUrl')
    at Videotexture.initialize (videotexture.js:39:28)
    at i._scriptMethod (playcanvas-stable.min.js:6:1281526)
    at i.set (playcanvas-stable.min.js:6:1466705)
    at i._checkState (playcanvas-stable.min.js:6:1280896)
    at i.onEnable (playcanvas-stable.min.js:6:1279842)
    at i._onHierarchyStateChanged (playcanvas-stable.min.js:6:840256)
    at i._notifyHierarchyStateChanged (playcanvas-stable.min.js:6:839811)
    at i._onInsertChild (playcanvas-stable.min.js:6:495415)
    at i.addChild (playcanvas-stable.min.js:6:494695)
    at Network.addEntity (Network.js:534:19)
e._onFailure @ playcanvas-stable.min.js:6
(anonymous) @ playcanvas-stable.min.js:6
a @ playcanvas-stable.min.js:6
(anonymous) @ playcanvas-stable.min.js:6
Promise.then (async)
e._loadImageBitmapFromBlob @ playcanvas-stable.min.js:6
(anonymous) @ playcanvas-stable.min.js:6
e._onSuccess @ playcanvas-stable.min.js:6
e._onReadyStateChange @ playcanvas-stable.min.js:6
m.onreadystatechange @ playcanvas-stable.min.js:6
XMLHttpRequest.send (async)
e.request @ playcanvas-stable.min.js:6
e.get @ playcanvas-stable.min.js:6
e._loadImageBitmap @ playcanvas-stable.min.js:6
e.load @ playcanvas-stable.min.js:6
e.load @ playcanvas-stable.min.js:6
h @ playcanvas-stable.min.js:6
e.load @ playcanvas-stable.min.js:6
i.load @ playcanvas-stable.min.js:6
i.add @ playcanvas-stable.min.js:6
Network.addAsset @ Network.js:290
Network.popQueue @ Network.js:257
onAssetLoad @ Network.js:303
e.fire @ playcanvas-stable.min.js:6
n @ playcanvas-stable.min.js:6
(anonymous) @ playcanvas-stable.min.js:6
e._onSuccess @ playcanvas-stable.min.js:6
(anonymous) @ playcanvas-stable.min.js:6
(anonymous) @ playcanvas-stable.min.js:6
n.onload.n.onreadystatechange @ playcanvas-stable.min.js:6
load (async)
e._loadScript @ playcanvas-stable.min.js:6
e.load @ playcanvas-stable.min.js:6
h @ playcanvas-stable.min.js:6
e.load @ playcanvas-stable.min.js:6
i.load @ playcanvas-stable.min.js:6
i.add @ playcanvas-stable.min.js:6
Network.addAsset @ Network.js:290
Network.popQueue @ Network.js:257
onAssetLoad @ Network.js:303
Network.addAsset @ Network.js:308
Network.popQueue @ Network.js:257
onAssetLoad @ Network.js:303
e.fire @ playcanvas-stable.min.js:6
n @ playcanvas-stable.min.js:6
(anonymous) @ playcanvas-stable.min.js:6
e._onSuccess @ playcanvas-stable.min.js:6
(anonymous) @ playcanvas-stable.min.js:6
(anonymous) @ playcanvas-stable.min.js:6

Well, place a breakpoint and debug? I don’t see where you define this.video.

don’t the attributes get loaded as fields of the script when the entity is loaded ?

Did you parse the script after creating the attributes?

this version works when i do the parse but the question is how can i parse the script in code and link the references?

this is how i try to parse and make the links in code but its not catching because i guess its not parsing.

    fs.readFile("stock/entity/video.json", function(err, entityStockBuf) {
      if (err) {
        return next(err);
      }
      var entityStockJson = JSON.parse(entityStockBuf);

      // Populate the stock Entities
      var rootEntity = entityStockJson["c8da7d7e-44e9-11e5-bef4-22000ac52f27"];
      rootEntity.id = req.rootEntityId;
      rootEntity.resource_id = req.rootEntityId;
      rootEntity.parent = sceneEntityId;
      rootEntity.record_id = null; // add down the pipeline
      rootEntity.childrenList = [req.videoEntityId, req.playButtonEntityId];
      req.entities[req.rootEntityId] = rootEntity;
      req.rootEntity = rootEntity;

      var videoEntity = entityStockJson["4e8df516-8452-4dc5-9ab7-3235ed5e017e"];
      videoEntity.components.model.materialAsset = req.assetScreenMaterialId;
      videoEntity.components.script.scripts["videotexture"].attributes["materials"] = [req.assetScreenMaterialId]
      videoEntity.components.script.scripts["videotexture"].attributes["video"] = req.assetVideoId;
      videoEntity.resource_id = req.videoEntityId;
      videoEntity.id = req.videoEntityId;
      videoEntity.parent = req.rootEntityId;
      videoEntity.record_id = null; // add down the pipeline
      req.entities[req.videoEntityId] = videoEntity;

      var playButtonEntity = entityStockJson["5492229d-75c8-4b97-833b-ab9c6dab8762"];
      playButtonEntity.components.button.imageEntity = req.playButtonEntityId;
      playButtonEntity.components.script.scripts["buttonLogic"].attributes["buttonEntity"] = req.playButtonEntityId;
      playButtonEntity.resource_id = req.playButtonEntityId;
      playButtonEntity.id = req.playButtonEntityId;
      playButtonEntity.parent = req.rootEntityId;
      playButtonEntity.record_id = null; // add down the pipeline
      req.entities[req.playButtonEntityId] = playButtonEntity;

      //console.log(req.assets);
      //console.log(req.assetFiles);
      //console.log(req.entities);
      console.log("finish create image assets + entity");
      next();
    });

and the entity “.components.script.scripts…” attributes are supposed to be loaded in the code i posted previously where i put a line of #############'s

Try to preload the video asset first, before creating the HTML element. Its probably not when you are using it.

i am loading the assets first, i tried adding a delay after asset load and entity load but i get the same error.

in debugging. i am seeing the attributes stored in

entity.script.videotexture.__attributesRaw

as

{
    "materials": [
        29645412
    ],
    "video": 29645418,
    "playEvent": "tv:play"
}

but the following

entity.script.videotexture.__attributes

as

{}

checking this thread for a potential solution…

wait i just got an idea

and looking back at projects that were successfully imported i see it:

    var characters = this.entity.script.textMesh.characters;

i can just access this.entity and any fields thereof, correct?

trying this in videotexture.js (asset)

    this.video = this.app.storage.entities[this.entity.script.videotexture.__attributesRaw.video];
    video.src = this.video.getFileUrl();

i cant access the script attributes after loading, it looks like this:

__attributes

as

{
    "materials": [
        null
    ],
    "video": null,
    "playEvent": "tv:play"
}

and

__attributesRaw

as

null

but it does access the entity which should be sufficient if i add my own field to entity

It does indeed work when I manually set attributes as a field on the entity and access it from the asset to which it is assigned and enabled, like so:


Videotexture.attributes.add('video', {
    type: 'asset'
});


...


this.video = this.entity.custom.script.videotexture.attributes.video;


and i look up the asset using a similar custom field on pc.app:

this.videoAsset = this.app.storage.assets[this.video];

then the asset can be used like normal.