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.

2 Likes

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);
    },
}

Thank you so much for your example and explanation on how to work with scenes and assets. Your approach helped me a lot in my project! But I’m facing one problem that I can’t solve yet.

  1. When the first “main” scene is loaded (e.g., the main menu), I want all its assets to preload at once.
  2. When transitioning to the next scene (e.g., a game scene), already loaded assets should not be reloaded.
  3. If no asset loading is required, I’d like to skip the loading screen so that the transition looks smooth.

Right now, when transitioning between scenes, the assets still reload even if they’ve already been loaded. The loading screen also appears even when it’s unnecessary.

I’ve tried:

  • Adding caching for loaded scenes to avoid reloading, but I’m not sure I implemented it correctly.
  • Adding a flag to skip asset loading, but the loading screen still appears regardless.

Could you suggest which files or parts of your code I should focus on to solve this issue? Any advice would be greatly appreciated!

Thanks again for your amazing example – it’s been super helpful! :blush:

Hi @Anton_L

In my project the first scene is named _startup. It is completely empty scene that requires no assets. I then parse all the scenes to determine for each scene which assets are required. Then I call changeScene to switch to the first real scene. The changeScene function will load all a scenes assets before switching to it.

The changeScene function was designed to

  • Destroy old scene
  • Unload assets for old scene
  • Load assets for new scene
  • Load new scene
  • Display a transition while changing scenes

This three classes cooperate to achieve this SceneAssets, SceneManager, SceneTransition.

If different scenes share some of the same assets and you don’t want to unload and reload them when switching scenes , then you’ll need to alter the logic in SceneAssets and SceneManager.

If you don’t want to display the transition when changing scenes then you’ll need to alter the logic in SceneManager.

If you wanted to preload all of the main menu scene and the game scenes assets when switching to the main menu scene, i.e. loading multiple scenes assets at once, then that isn’t the design of my example project in which the purpose was to only load the assets needed in the current scene and unload other assets.