Custom classes in externally loaded scenes

I have a manager class which adds buttons at runtime to a set of quads and connects them to their content. In order to get all the necessary information at runtime, I’ve created a custom class which holds the element to which I am adding the button behaviour and the elements which it shows when clicked. This works well in a local scene, but I need it to work in a remote scene.

I am using your scene switcher component to load external scenes from URLs. Some of these scenes have the manager class described above. However, when the manager class is loaded as part of an external glb, the references to the objects which are part of the custom class are not correct. They have different uuids from the actual objects in the scene. So, when I add the button behaviour, it will not, in fact, be added to the correct Object3D which is in the scene, but to another object which does not exist in the hierarchy.

I’m attaching here a simplified example of this issue (no buttons, just console.log messages). This contains the custom class and the reference manager

import { Behaviour, GameObject, serializable } from "@needle-tools/engine";
import { Object3D } from "three";

// Documentation → https://docs.needle.tools/scripting

export class ReferenceContainer {
    @serializable(Object3D)
    transform: Object3D | undefined;

    @serializable(GameObject)
    gameobject: GameObject | undefined;
}

export class ReferenceTest extends Behaviour {

    @serializable(ReferenceContainer)
    container: ReferenceContainer[] = [];

    @serializable(Object3D)
    transformRef: Object3D | undefined;

    @serializable(GameObject)
    gameObjectRef: GameObject | undefined;
    
    start() {
        console.log("ReferenceTest: transformRef", this.transformRef);
        console.log("ReferenceTest: gameObjectRef", this.gameObjectRef);
        this.container.forEach((c, index) => {
            console.log(`ReferenceTest: container[${index}] transform`, c.transform);
            console.log(`ReferenceTest: container[${index}] gameobject`, c.gameobject);
        });
    }
}

Here is a SelfReference script to tell me what is in the scene.

import { Behaviour } from "@needle-tools/engine";

export class SelfReference extends Behaviour {
    
    start() {
        console.log("SelfReference: gameObject", this.gameObject);
    }
}

This is the Unity scene

And here is a screenshot with what I get when I load the scene with:

const asset = AssetReference.getOrCreateFromUrl(url, this.context);
const instance = await asset.instantiate(this);

As you can see, the references from the containers are not actually part of the scene hierarchy, only the references to the objects directly are correct. So any component added to them will do nothing. I need to reference the objects in the scene.

How can I do that?

Hi, i think this is a known issue, I’ll look into it tomorrow. It’s caused when you have nested object references (like in your example) where the original reference is not replaced with the new clone (instantiate effectively clones the loaded objects and components which causes the issue you describe). Will get back to you tomorrow.

In the meantime a question: do you have both a transform and a GameObject reference in your components that point to the same object (Gameobject and respective transform?). If that’s the case you can remove either of them because in Needle Engine there is no distinction between a Transform and a GameObject - both become the same Object3d object (the core object in three.js). It will not be a big deal to keep them but also won’t do anything.

Hi Marcel,

Thanks for taking the time to reply. I’ve started implementing the bot’s suggestion since apparently when saving the glb, the exporter does assign unique names to every object, and it seems to be working ok so far.

As for the dual references, don’t worry. It was just a test. My custom class had Object3D references, while my core behaviour had other GameObject references which were not lost. Since the GameObject references worked, I thought maybe it’s a type issue.

Looking forward to any advice you may be able to give me tomorrow.

Hello,

Just to clarify: The exporter does not change the object’s names, this is something that three.js does (the three.js GLTFLoader is creating unique names for each object in a loaded glTF file) - we do not touch object names.

The issue you’ve encountered is unfortunately nothing we can change without more consideration - it’s caused by the fact that instantiate does not deepClone nested references (meaning: when your component references a custom class like the ReferenceContainer container in your case as a property then the new instance of your component does still point to the same instance as your original component). This was a decision early on for performance reasons and changing the default now might cause issues in existing applications. That being said it’s known and something we want to change.

Ouch, so the fact that the getObjectByName approach does work is purely due to the happy accident that the GLTFLoader creates unique names in the same order every time.

I need this functionality because I can’t serialize nested arrays in Unity unless I make a custom class. Here are some screenshots with my actual approach:

This is the array of Elements (Custom Class) which has separate arrays for Contents and Triggers

And here you can see how it looks in the hierarchy (they were prefabs, all of them, until I unpacked them):

The case of the selected QuadButton-OffState is interesting because when I load the GLB file, its name is QuadButton-OffState_38, even though it points to the original reference. So when I do getObjectByName for QuadButton-OffState_38, it points to the actual object in the hierarchy and I can reassign it. Thankfully, this is done only once at the beginning, so we don’t notice any drastic performance issues.

Not sure what you mean by the GltfLoader naming order - if you search the objects in the hierarchy using the names of the assigned object references then these names will always have the matching name of whatever new clone exists in the scene after instantiate (which is the reference you try to resolve). This was always the case in three and will probably never change.

That being said: All Object3D/Group objects also have a unique guid which is carried over from Unity and might be a better workaround to make sure the reference is the same.

I’m just saying that the renaming done by the GltfLoader is consistent across multiple sessions. Because, as you can see, my Unity objects definitely didn’t have that number appended at the end.

Anyway, I just tried your suggestion with the Unity assigned guid, but it won’t work. It’s definitely not the same string. Apparently something happens to it when it’s loaded externally.

When loaded externally and referenced directly, so not part of a custom class, the guid of the same object looks like this guid: "04ad616c-90dd-5637-a2c7-0074b053730e" and is the same as the uuid, so it changes on every refresh. But when part of a custom class it looks like this guid: “1214056767” and there’s also a new property, which I can’t find on the direct reference Symbol(gltf-loader-internal-usage-tracker): “1757594222159”

The scene.getObjectByName() workaround is not ideal, but it’s the only one that appears to work currently.