Any way to *return* a list of script(s) from a ScriptComponent

Hi all,

I’ve been having a bit of trouble trying to figure out how best to do this. Basically, what I want to do is receive a PlayCanvas entity in a script, then check via code what (if any) scripts are attached to that entity’s Script component. As far as I can tell the only way to check if a script is on an entity at the moment is by knowing the name of the script you want to check. But is there any way to return the name(s) of any scripts that may exist on a ScriptComponent?

For further context, I’m using TypeScript in my project, and I have a large array of templates, each of which has scripts inheriting from a base class (let’s call it UITemplateBase). For example:

class UITemplateBase extends ScriptTypeBase{ // The base class that all my UI template classes derive from
  public Init(){
    // Init function to be overridden by derivatives/children
  }
}

class SomeUIClass extends UITemplateBase{
  public override Init(){
    // my init logic here
  }
}
export default SomeUIClass;

I’ve already set up a system to hunt through that list of templates to instantiate the one I want, but when I’ve got it I need to call the Init() function on it, which I can’t do from the base class because doing a getScript() with the base class type doesn’t return me anything. E.g.

this.templateEntity.script?.get('UITemplateBase'); // -> this returns null

Seems like I need the specific name of that particular template’s inheriting script. >.< So it would be ideal if I could do something like:

var scriptName = this.templateEntity.script?.scripts[0].scriptName; // -> would ideally return "SomeUIClass"
var desiredScript:SomeUIClass = this.templateEntity.script?.get(scriptName);
desiredScript.Init();

Thanks in advance!

EDIT: Updated title and body for clarity, I hope what I’m asking makes sense!
EDIT2: Added a bit more context to my examples.

You could iterate over its scripts, something like:

for (const script of templateEntity.script.scripts) {
    if (script.constructor.name === 'SomeUIClass') {
        script.init();
    }
}

Thanks for the quick reply! Unfortunately this doesn’t seem to be working for me, as script.constructor.name seems to only return "scriptType", which may be something to do with the PlayCanvas TypeScript template? entity.script.scripts is an array of type ScriptType[].

Well, your entity can only have ScriptType scripts or their derivatives. If SomeUIClass derives UITemplateBase, then UITemplateBase must be a pc.ScriptType or extend one that is a pc.ScriptType.

That logic tracks, I’m just not sure how to get the instance of SomeUIClass from that ScriptType. My temporary workaround solution is to have an event in every class that derives from UITemplateBase that fires with its name after it’s initialised, which my template spawner listens to and then uses to get the script it needs using entity.script?.get(). But this feels really hacky, so I was hoping there was some way I could just get any scripts on the entity from the ScriptComponent itself, somehow, without knowing exactly what the scripts are.

Well, then the snippet I showed should work. Let me make an example.

Edit:
Here is an example:
https://playcanvas.com/project/1142219/overview/blank-project

I’m wondering if there’s some disconnect between how this works in JavaScript and how it’s working in TypeScript.

I’ve quickly tried to replicate your methodology with this result:


image

The way I want this to work additionally would require not knowing the name of the script I want, just that it’s derived from the base class. I have an array of dozens of these derived UI classes, all of which instantiate the same way with their inherited Init() functions, so I’m trying to avoid having to create some sort of massive if/elseif/else function to check every possible derived class it could be.

As a C# example, I could find a class by its base class to access any inherited functionality, which is something I used often in Unity and am trying to replicate if at all possible here (or find a suitable substitute). If this were C# I could simply do gameObject.GetComponent<UITemplateBase>().Init(); and that would be that. My hackey workaround solution does work, so I’ll stick with it if it’s the closest approximation to the desired functionality.

There shouldn’t be? I don’t use TypeScript, but if it works in JS, it should work in TS.

If you want to get the base class name from the inherited, you can do this:

for (const script of templateEntity.script.scripts) {
    if (Object.getPrototypeOf(script.constructor).name === 'UITemplateBase') {
        script.init();
    }
}

Also, I don’t know what is this.AcriveNode in your snippet, or where this is pointing to. You could make a small example project, like I did, that shows your issue. Just a minimum repro.

In that particular example this.ActiveNode is an entity that has just been instantiated from a template asset exposed as an attribute and populated via the PlayCanvas editor. The this. is the usual PlayCanvas syntax that is used with all variables declared outside of a function, I think? So in this case this. is the class that’s instantiating/initialising the UIs. (not being a seasoned JS/TS user I’m not entirely sure why variables need a ‘this.’ before them, but I’ve gotten used to it. Something to do with scope I’m guessing?)

I’ll throw together an example project for you tomorrow. Not sure how intelligible it’ll be, as the way I work is by compiling TS code to a JS file that is uploaded to PlayCanvas via playcanvas-sync. The JS file is not formatted so it may be difficult to read.

To pre-empt any assumptions, this is the first time I’ve ever had trouble with a PlayCanvas component in code, before this everything has worked exactly as it would in JavaScript.

Yes, this is needed for scoping, mostly when working with class instances. If you have a chance, a small example could help me or someone to help you better.

I’ve thrown together a quick example in my test project here:
https://playcanvas.com/project/847046/overview/test-app

It takes a bit of doing setting up a completely new project with my TypeScript setup, so I hope this is sufficient. I’ve added a “TEST SPAWNER” entity to the Main Screen, which has an example script that picks a template at random and attempts to spawn and initialise it. The code can be viewed in the index.js file, although, again, it’s difficult to read due to how it compiles. I find it easier to check code in the web browser source terminal, which auto-formats the JS.

You are using some custom script, called ScriptTypeBase, which has a method called .getScript.

You need to change it to match the snippet I showed above. Just remove everything there, and use:

let foundScript = null;
for (const script of entity?.script.scripts) {
    if (Object.getPrototypeOf(script.constructor).name === 'UITemplateBase') {
        foundScript = script;
    }
}
return foundScript;

Edit:
If ScriptTypeBase is not yours (e.g. from some third party library), then don’t use that method in your UISpawner, and create your own method with the contents I showed.

1 Like

Indeed, ScriptTypeBase came with the template I’m using and has been generally a very helpful class for writing cleaner PlayCanvas code in TypeScript, but I recognise that it is likely contributing to the problem I’m experiencing here. :stuck_out_tongue:

So I have implemented what you recommended, rather than updating ScriptTypeBase I’ve updated the UISpawner with your solution. I couldn’t cast the variable foundScript to my UITemplateBase as it “shares no overlap with ScriptType | Undefined”, so I’ve typed it as unknown, but otherwise proceeded as you have exemplified:

Unfortunately, this still doesn’t seem to be working, it’s logging as ‘null’:
image

When I console.log the for loop, I get:


image

So the problem remains: for whatever reason, script.constructor.name is only seeing scriptType, not any of the names I actually give my scripts. Is this because the TypeScript template compiles all of my code into a single JS file?