What does pc.createScript actually do?

Can anyone briefly explain how the engine handles the creation and instantiation of our scripts. I’ve spent some serious time - we’re talking several hours - over the past few days, reading thru and jumping between the scripts, scripts handler and scripts registry code and I’m getting dizzy.

It looks to me that when we call pc.createScript(‘myScript’), we are getting in return a constructor function. But, looking at the code and following the trail, it seems like before createScript() returns this function, it adds it to the script registry. Now this is where I get confused (well, one of the many places where I get confused actually); it looks to me that in the same process of adding this function to the registry, the engine actually instantiates it right away. It seems to me that, at that point, we haven’t extended the ScriptType function yet by adding the initialize and update methods that contains our game logic. There is obviously something I’m not getting. If anyone could shed some light on this, I would be grateful.

What do you mean by this? It doesn’t instantiate the instance of the script object on the entity straightaway, it creates a class definition that can be extended.

Edit: Edited for beverity.

Ok so before returning script, createScript calls registry.add(script). When I look at the code for ScriptRegistry#add I see this:

        ....
        setTimeout(function() {
            if (! self._scripts.hasOwnProperty(script.__name))
                return;

            var components = self.app.systems.script._components;
            var i, s, scriptInstance, attributes;
            var scriptInstances = [ ];
            var scriptInstancesInitialized = [ ];

            for(i = 0; i < components.length; i++) {
                // check if awaiting for script
                if (components[i]._scriptsIndex[script.__name] && components[i]._scriptsIndex[script.__name].awaiting) {
                    if (components[i]._scriptsData && components[i]._scriptsData[script.__name])
                        attributes = components[i]._scriptsData[script.__name].attributes;

                    scriptInstance = components[i].create(script.__name, {
                        preloading: true,
                        ind: components[i]._scriptsIndex[script.__name].ind,
                        attributes: attributes
                    });

                    if (scriptInstance)
                        scriptInstances.push(scriptInstance);
                }
            }

            // initialize attributes
            for(i = 0; i < scriptInstances.length; i++)
                scriptInstances[i].__initializeAttributes();

            // call initialize()
            for(i = 0; i < scriptInstances.length; i++) {
                if (scriptInstances[i].enabled) {
                    scriptInstances[i]._initialized = true;

                    scriptInstancesInitialized.push(scriptInstances[i]);

                    if (scriptInstances[i].initialize)
                        scriptInstances[i].initialize();
                }
            }

            // call postInitialize()
            for(i = 0; i < scriptInstancesInitialized.length; i++) {
                scriptInstancesInitialized[i]._postInitialized = true;

                if (scriptInstancesInitialized[i].postInitialize)
                    scriptInstancesInitialized[i].postInitialize();
            }
        });
       ....

What is going on here? I don’t understand it all but the comments do talk about creating script instances and calling initialize and postInitialize. And all this seems to be happening before createScript actually returns.

This is pretty new to me as well so let’s see if I can run through this step by step:

ScriptRegistry.prototype.add = function(script) {
    var self = this;
    if (this._scripts.hasOwnProperty(script.__name)) {
      setTimeout(function() {
        if (script.prototype.swap) {
          var old = self._scripts[script.__name];
          var ind = self._list.indexOf(old);
          self._list[ind] = script;
          self._scripts[script.__name] = script;
          self.fire("swap", script.__name, script);
          self.fire("swap:" + script.__name, script);
        } else {
          console.warn("script registry already has '" + script.__name + "' script, define 'swap' method for new script type to enable code hot swapping");
        }
      });
      return false;
    }

This part check if the script class definition already exists and if there is a swap function, call that for hot reloading of the script during development https://blog.playcanvas.com/playcanvas-scripts-2-0/

    this._scripts[script.__name] = script;
    this._list.push(script);
    this.fire("add", script.__name, script);
    this.fire("add:" + script.__name, script);

This adds it to the registry so it can be found by name. When the entity is created, it will contain the names of the scripts to be added to the entity.

setTimeout(function() {
      if (!self._scripts.hasOwnProperty(script.__name)) {
        return;
      }
      var components = self.app.systems.script._components;
      var i, s, scriptInstance, attributes;
      var scriptInstances = [];
      var scriptInstancesInitialized = [];
      for (i = 0;i < components.length;i++) {
        if (components[i]._scriptsIndex[script.__name] && components[i]._scriptsIndex[script.__name].awaiting) {
          if (components[i]._scriptsData && components[i]._scriptsData[script.__name]) {
            attributes = components[i]._scriptsData[script.__name].attributes;
          }
          scriptInstance = components[i].create(script.__name, {preloading:true, ind:components[i]._scriptsIndex[script.__name].ind, attributes:attributes});
          if (scriptInstance) {
            scriptInstances.push(scriptInstance);
          }
        }
      }
      for (i = 0;i < scriptInstances.length;i++) {
        scriptInstances[i].__initializeAttributes();
      }
      for (i = 0;i < scriptInstances.length;i++) {
        if (scriptInstances[i].enabled) {
          scriptInstances[i]._initialized = true;
          scriptInstancesInitialized.push(scriptInstances[i]);
          if (scriptInstances[i].initialize) {
            scriptInstances[i].initialize();
          }
        }
      }
      for (i = 0;i < scriptInstancesInitialized.length;i++) {
        scriptInstancesInitialized[i]._postInitialized = true;
        if (scriptInstancesInitialized[i].postInitialize) {
          scriptInstancesInitialized[i].postInitialize();
        }
      }
    });

This bit is interesting. By using setTimeout with a time of 0, the code in the callback is called in the next frame.

It looks like (and I’m guessing here), is that it finds all the instances of the PlayCanvas script definition and calls initialize and then postInitialize.

So in terms of code flow, it would look something like this:

// Script
var Pulse = pc.createScript('pulse');

// In engine
    this._scripts[script.__name] = script;
    this._list.push(script);
    this.fire("add", script.__name, script);
    this.fire("add:" + script.__name, script);

// Back to script
Pulse.prototype.initialize = function() {
    this.secsSinceStart = this.pulseTimeSecs + 1;
    this.entity.on("pulse:start", this.onStartEvent, this);
};

// update code called every frame
Pulse.prototype.update = function(dt) {
    this.secsSinceStart += dt; 
    
    if (this.secsSinceStart <= this.pulseTimeSecs) {
        var normalisedTime = this.secsSinceStart / this.pulseTimeSecs;
        var pingPongTime = Math.abs(normalisedTime -0.5) * 2;
        var scale = (0.3 * pingPongTime) + 0.7;
        var localScale = this.entity.getLocalScale();
        localScale.set(scale, scale, scale);
        this.entity.setLocalScale(localScale);
    }
};

Pulse.prototype.onStartEvent = function() {
    this.secsSinceStart = 0;
};

// And then on the next frame, all the timeouts are called
{
      if (!self._scripts.hasOwnProperty(script.__name)) {
        return;
      }
      var components = self.app.systems.script._components;
      var i, s, scriptInstance, attributes;
      var scriptInstances = [];
      var scriptInstancesInitialized = [];
      for (i = 0;i < components.length;i++) {
        if (components[i]._scriptsIndex[script.__name] && components[i]._scriptsIndex[script.__name].awaiting) {
          if (components[i]._scriptsData && components[i]._scriptsData[script.__name]) {
            attributes = components[i]._scriptsData[script.__name].attributes;
          }
          scriptInstance = components[i].create(script.__name, {preloading:true, ind:components[i]._scriptsIndex[script.__name].ind, attributes:attributes});
          if (scriptInstance) {
            scriptInstances.push(scriptInstance);
          }
        }
      }
      for (i = 0;i < scriptInstances.length;i++) {
        scriptInstances[i].__initializeAttributes();
      }
      for (i = 0;i < scriptInstances.length;i++) {
        if (scriptInstances[i].enabled) {
          scriptInstances[i]._initialized = true;
          scriptInstancesInitialized.push(scriptInstances[i]);
          if (scriptInstances[i].initialize) {
            scriptInstances[i].initialize();
          }
        }
      }
      for (i = 0;i < scriptInstancesInitialized.length;i++) {
        scriptInstancesInitialized[i]._postInitialized = true;
        if (scriptInstancesInitialized[i].postInitialize) {
          scriptInstancesInitialized[i].postInitialize();
        }
      }
    }

Ok, so if I understand correctly, using setTimeout actually allows the program to keep going and return the script to us, where we can extend it with our code and THEN it runs the stuff in the setTimeout’s callback. Am I getting this right?

(Edited - Remove code, I had made an error)

I believe so. It should be quite easy to verify with the debugger and breakpoints.

1 Like

Yeah, that’s what I’m trying to do. Will report back with my findings. I’m doing all this because I’m trying to find a way to interface with the API by using ES6 classes syntax and I’m having a hard time finding a way that works.

I’ve just checked this with the debugger and the order is the same way that I’ve described it :slight_smile:

For more information about using setTimeout with 0 secs, read: https://stackoverflow.com/questions/779379/why-is-settimeoutfn-0-sometimes-useful

1 Like

I have created a simplified createScript function to play around with and to better understand how the engine works. It’s not meant to be used in PlayCanvas, just to get a better feel for the flow and to try to find a way to use ES6 classes.

var registry = [];

var createScript = function (name) {
    var script = function (args) {
        this.args = args;
    }
    script._name = name;

    registry.push(script);

    setTimeout(function() {
        var testInstance = new registry[0]();        
        console.log(testInstance.initialize());
    });

    return script;
}

var Test = createScript('test');

Test.prototype.initialize = function() {
    return 'initialize in da house yo!'
}