How to make a script globally available across all scenes?

Hi everyone!

I’m trying to create a script in PlayCanvas that loads assets dynamically before switching between scenes. The script works fine for the first scene (Start) but fails to load assets for the next scene (Space). My main issue is that the script is tied to an entity in the Start scene, so it doesn’t persist when switching to another scene.

Currently, the script only loads assets tagged with Start but ignores those tagged with Space. I need the script to remain active across all scenes, dynamically load assets for the current scene, and handle scene transitions.
Here’s the script code:

// Create the preload script
var Preload = pc.createScript('preload');

// Initialize method
Preload.prototype.initialize = function () {
    console.log("[Preload] Script initialized...");

    // Load assets for the current scene (Start)
    this.loadAssets('Start', () => {
        console.log("[Preload] Start scene assets loaded successfully.");

        // Then load assets for the next scene (Space)
        this.loadAssets('Space', () => {
            console.log("[Preload] Space scene assets loaded successfully.");
            this.switchToScene('Space');
        });
    });
};

// Method to load assets by tag
Preload.prototype.loadAssets = function (tag, callback) {
    const assets = this.app.assets.findByTag(tag);
    console.log(`[Preload] Found ${assets.length} assets with tag: ${tag}`);

    if (assets.length === 0) {
        console.warn(`[Preload] No assets found with tag: ${tag}`);
        if (callback) callback();
        return;
    }

    let loadedCount = 0;

    assets.forEach((asset) => {
        console.log(`[Preload] Loading asset: ${asset.name} (${asset.type})`);
        asset.once('load', () => {
            console.log(`[Preload] Asset loaded: ${asset.name} (${asset.type})`);
            loadedCount++;

            // Callback when all assets are loaded
            if (loadedCount === assets.length) {
                console.log(`[Preload] All assets with tag '${tag}' loaded.`);
                if (callback) callback();
            }
        });

        this.app.assets.load(asset);
    });
};

// Method to switch to another scene
Preload.prototype.switchToScene = function (sceneName) {
    console.log(`[Preload] Switching to scene: ${sceneName}`);
    const scene = this.app.scenes.find(sceneName);

    if (scene) {
        this.app.scenes.loadScene(scene.url, (err, loadedScene) => {
            if (err) {
                console.error(`[Preload] Failed to load scene: ${sceneName}`, err);
            } else {
                console.log(`[Preload] Scene '${sceneName}' successfully loaded.`);
            }
        });
    } else {
        console.error(`[Preload] Scene '${sceneName}' not found.`);
    }
};
  • How can I make the script global so it remains active across all scenes and handles asset loading dynamically?
  • Why does the script only load assets for the first scene and not the subsequent scenes? How can I fix this behavior?

Any help would be greatly appreciated. Thanks in advance!

Here is an example of how I did it.

https://playcanvas.com/editor/code/1202866?tabs=172220452,172245301&focused=172220452

https://playcanvas.com/project/1202866/overview/scene-change-with-asset-preload

The scene manager is instance of a class that you create when the app starts and you keep a reference to it so you can access it from other scripts. It is not attached to an entity and is therefore not destroyed when changing scenes. I put this logic in the createLoadingScreen(callback) function as this is called at app startup.

1 Like

I personally use the approach with multiple loaded scenes.
There is a Main scene - which is never unloaded and has everything you need in it. It has a SceneManager that already loads/unloads certain scenes (Menu is replaced by Game and vice versa).

const SceneManager = {
    /**@type {pc.AppBase} */
    app:null,
    /**@type {pc.Entity[]} */
    rootsList: [],
    
    activeLoadingsCount: 0,

    /**@param {pc.AppBase} app*/
    init(app){
        this.app = app;
    },
    /**@param {pc.SceneRegistryItem | String} scene */
    changeScene(scene) { //example of 1 additive scene usage
        if(this.activeLoadingsCount > 0) return;
        this.unloadAllLoadedScenes(); //destroy all additive scenes - leave only if needed
        this.loadScene(scene);
    },
    /**@param {pc.SceneRegistryItem | String} scene*/
    loadScene(scene){
        if(!(scene instanceof pc.SceneRegistryItem)){
            scene = this.app.scenes.find(scene);
        }
        if(!scene){
            console.error('loadScene with scene =', scene);
            return;
        }
        if(this.activeLoadingsCount <= 0) this.app.fire('loadingStateChange', true);
        this.activeLoadingsCount++;
        var status = 0;
        // Load the scene hierarchy with a callback when it has finished
        this.app.scenes.loadSceneHierarchy(scene, (err, loadedSceneRootEntity) => {
            if (err) {
                console.error(err);
            } else {
                loadedSceneRootEntity.scene = scene;
                this.rootsList.push(loadedSceneRootEntity);
                status++;
                if(status === 2) this.onLoadingEnd();
            }
        });

        // Load the scene settings with a callback when it has finished
        this.app.scenes.loadSceneSettings(scene, function (err) {
            if (err) {
                console.error(err);
            } else {
                status++;
                if(status === 2) SceneManager.onLoadingEnd();
            }
        });
    },
    onLoadingEnd(){
        this.activeLoadingsCount--;
        if(this.activeLoadingsCount <= 0){
            this.app.fire('loadingStateChange', false);
        }
    },
    /**
     * @param {pc.SceneRegistryItem} scene
     * */
    unloadScene(scene){
        for(const sceneRoot of this.rootsList){
            const index = this.rootsList.indexOf(sceneRoot);
            if(index !== -1){
                this.rootsList.splice(index, 1);
            }
            sceneRoot.destroy();
        }
    },
    unloadAllLoadedScenes(){
        for(const sceneRoot of this.rootsList){
            sceneRoot.destroy();
        }
        this.rootsList.length = 0;
    },
};

Example of Main script

class Main extends pc.ScriptType{}
pc.registerScript(Main, 'main');

Main.appData = {
    titleName:'Bubble Shooter',
    idName:'BubbleShooter', //do not change between updates
}

/**@type {Main} */
Main.instance;

Main.attributes.add('firstScene', {type: 'string', default:'Menu'});
Main.attributes.add('loadingEntities', {type: 'entity', array:true}); // loading screen and camera for rendering it

Main.prototype.initialize = function() {
    Main.instance = this;
    
    SaveManager.initialize(this.app);

    this.app.on('loadingStateChange', (isLoading)=>{
        for(var loadingEntity of this.loadingEntities){
            loadingEntity.enabled = isLoading;
        }
    });
    SceneManager.init(this.app);
    SceneManager.loadScene(this.firstScene);
    //
};

Separate systems that do not require any references to entities/assets - can be created globally as objects.

const SaveManager = {
    data: {
        playTime: 0,
        money: 0
    },
    /**@type {pc.AppBase} */
    app:null,
    /**@type {String} */
    saveKey:undefined,
    /**@type {pc.EventHandle} */
    saveEventHandle:null,

    /**@param {pc.AppBase} app*/
    initialize(app) {
        this.app = app;
        this.saveKey = `${Main.appData.idName}`
        this.saveEventHadnler = null;
        //
        this.load();
    },
    
    save() {
        if(this.saveEventHandle !== null) return;

        if(this.app !== null)
        {
            this.saveEventHandle = this.app.once('update', this.instantSave, this);
        }
        else {
            this.instantSave();
        }
    },

    instantSave() {
        if(this.saveEventHandle !== null){
            this.saveEventHandle = null;
        }
        localStorage.setItem(this.saveKey, JSON.stringify(this.data));
    },
    load() {
        var jsonStr = localStorage.getItem(this.saveKey);
        if (jsonStr === null) return;
        var loadData = JSON.parse(jsonStr);
        this.data = mergeDeep(this.data, loadData);
        //console.log('save data', this.data);
    },
}