zoukankan      html  css  js  c++  java
  • Cocos Creator 资源加载流程剖析【六】——场景切换流程

    这里讨论场景切换的完整流程,从我们调用了loadScene开始切换场景,到场景切换完成背后发生的事情。整个流程可以分为场景加载和场景切换两部分,另外还简单讨论了场景的预加载。

    • 加载场景的流程

    loadScene主要做了3件事,通过_getSceneUuid获取要加载场景的信息,对于原生平台的非启动场景执行了cc.LoaderLayer.preload(但查询了所有的代码,并没有发现LoaderLayer的实现,也没有发现任何对cc.runtime赋值的地方),最后通过_loadSceneByUuid加载场景

        loadScene: function (sceneName, onLaunched, _onUnloaded) {
            // 同一时间只能有一个场景在加载
            if (this._loadingScene) {
                cc.errorID(1213, sceneName, this._loadingScene);
                return false;
            }
            // 获取场景的信息
            var info = this._getSceneUuid(sceneName);
            if (info) {
                var uuid = info.uuid;
                // 触发一个场景开始加载的事件
                this.emit(cc.Director.EVENT_BEFORE_SCENE_LOADING, sceneName);
                // 设置当前正在加载的场景
                this._loadingScene = sceneName;
                // 在原生运行时且该场景并非启动场景时,可以进行异步加载。
                if (CC_JSB && cc.runtime && uuid !== this._launchSceneUuid) {
                    var self = this;
                    var groupName = cc.path.basename(info.url) + '_' + info.uuid;
                    console.log('==> start preload: ' + groupName);
                    var ensureAsync = false;
                    // 如果cc.LoaderLayer.preload是异步的,会在preload结束后执行_loadSceneByUuid。否则会在preload结束的下一帧执行_loadSceneByUuid。
                    cc.LoaderLayer.preload([groupName], function () {
                        console.log('==> end preload: ' + groupName);
                        if (ensureAsync) {
                            self._loadSceneByUuid(uuid, onLaunched, _onUnloaded);
                        } else {
                            setTimeout(function () {
                                self._loadSceneByUuid(uuid, onLaunched, _onUnloaded);
                            }, 0);
                        }
                    });
                    ensureAsync = true;
                } else {
                    this._loadSceneByUuid(uuid, onLaunched, _onUnloaded);
                }
                return true;
            } else {
                cc.errorID(1214, sceneName);
                return false;
            }
        },
    

    Creator2.x版本的loadScene则直接多了,执行_getSceneUuid,触发EVENT_BEFORE_SCENE_LOADING事件,再调用_loadSceneByUuid。

        loadScene: function (sceneName, onLaunched, _onUnloaded) {
            if (this._loadingScene) {
                cc.errorID(1208, sceneName, this._loadingScene);
                return false;
            }
            var info = this._getSceneUuid(sceneName);
            if (info) {
                var uuid = info.uuid;
                this.emit(cc.Director.EVENT_BEFORE_SCENE_LOADING, sceneName);
                this._loadingScene = sceneName;
                this._loadSceneByUuid(uuid, onLaunched, _onUnloaded);
                return true;
            } else {
                cc.errorID(1209, sceneName);
                return false;
            }
        },
    

    _loadSceneByUuid方法也很简单,调用了cc.AssetLibrary.loadAsset加载资源,并指定了资源加载结束后的回调,也就是执行runSceneImmediate以及用户传入的onLaunched回调。

    _loadSceneByUuid方法在Creator2.x和Creator1.x中没有区别

        _loadSceneByUuid: function (uuid, onLaunched, onUnloaded, dontRunScene) {
            if (CC_EDITOR) {
                if (typeof onLaunched === 'boolean') {
                    dontRunScene = onLaunched;
                    onLaunched = null;
                }
                if (typeof onUnloaded === 'boolean') {
                    dontRunScene = onUnloaded;
                    onUnloaded = null;
                }
            }
            console.time('LoadScene ' + uuid);
            cc.AssetLibrary.loadAsset(uuid, function (error, sceneAsset) {
                console.timeEnd('LoadScene ' + uuid);
                var self = cc.director;
                self._loadingScene = '';
                if (error) {
                    error = 'Failed to load scene: ' + error;
                    cc.error(error);
                } else {
                    // runSceneImmediate启动场景
                    if (sceneAsset instanceof cc.SceneAsset) {
                        var scene = sceneAsset.scene;
                        scene._id = sceneAsset._uuid;
                        scene._name = sceneAsset._name;
                        if (CC_EDITOR) {
                            if (!dontRunScene) {
                                self.runSceneImmediate(scene, onUnloaded, onLaunched);
                            } else {
                                scene._load();
                                if (onLaunched) {
                                    onLaunched(null, scene);
                                }
                            }
                        } else {
                            self.runSceneImmediate(scene, onUnloaded, onLaunched);
                        }
                        return;
                    } else {
                        error = 'The asset ' + uuid + ' is not a scene';
                        cc.error(error);
                    }
                }
                if (onLaunched) {
                    onLaunched(error);
                }
            });
        },
    

    loadAsset做的事情也非常简单,就是调用Loader.load去做真正的加载,在加载完成之后将场景所依赖的资源设置给asset.scene.dependAssets,用于场景的释放,另外因为场景并不作为一个可重复使用的资源,所以这里会将场景从Loader中移除。

    loadAsset方法在Creator2.x和Creator1.x中没有区别

        loadAsset: function (uuid, callback, options) {
            if (typeof uuid !== 'string') {
                return callInNextTick(callback, new Error('[AssetLibrary] uuid must be string'), null);
            }
            
            var item = {
                uuid: uuid,
                type: 'uuid'
            };
            if (options && options.existingAsset) {
                item.existingAsset = options.existingAsset;
            }
            Loader.load(item, function (error, asset) {
                if (error || !asset) {
                    error = new Error('[AssetLibrary] loading JSON or dependencies failed: ' + (error ? error.message : 'Unknown error'));
                } else {
                    if (asset.constructor === cc.SceneAsset) {
                        if (CC_EDITOR && !asset.scene) {
                            Editor.error('Sorry, the scene data of "%s" is corrupted!', uuid);
                        } else {
                            var key = cc.loader._getReferenceKey(uuid);
                            // 这里其实是递归获取场景这个item的dependKeys数组(去重复)
                            asset.scene.dependAssets = AutoReleaseUtils.getDependsRecursively(key);
                        }
                    }
                    if (CC_EDITOR || isScene(asset)) {
                        var id = cc.loader._getReferenceKey(uuid);
                        Loader.removeItem(id);
                    }
                }
                if (callback) {
                    callback(error, asset);
                }
            });
        },
    
    • 场景运行与切换

    runSceneImmediate做的事情非常多,大概可以分为以下几个事情(虽然Creator2.x的runSceneImmediate方法写法有些变化,但大体做的事情类似):

    • 新场景的初始化(_load方法)
    • 将持久节点从旧场景挪到新场景中
    • 销毁旧场景、自动释放应该释放的资源(旧场景中标记为自动释放且新场景中没有引用到的资源)
    • 一系列场景切换流程的回调和事件执行
      • 开始启动场景的回调和事件
      • 激活并运行新场景
      • 场景启动完成的回调和事件
        runSceneImmediate: function (scene, onBeforeLoadScene, onLaunched) {
            const console = window.console;    // should mangle
            const INIT_SCENE = CC_DEBUG ? 'InitScene' : 'I';
            const AUTO_RELEASE = CC_DEBUG ? 'AutoRelease' : 'AR';
            const DESTROY = CC_DEBUG ? 'Destroy' : 'D';
            const ATTACH_PERSIST = CC_DEBUG ? 'AttachPersist' : 'AP';
            const ACTIVATE = CC_DEBUG ? 'Activate' : 'A';
    
            // 场景的初始化,scene._load会调用CCNode的_onBatchCreated
            // 1. PrefabHelper.syncWithPrefab(this); 大多数情况下会跳过
            // 2. _updateDummySgNode将自己的属性同步给sgNode,并确保sgNode是自己的子节点
            // 3. 如果当前节点未激活,则调用ActionManager和EventManager的pauseTarget
            // 4. 遍历子节点调用它们的_onBatchCreated
            if (scene instanceof cc.Scene) {
                console.time(INIT_SCENE);
                scene._load();  // ensure scene initialized
                console.timeEnd(INIT_SCENE);
            }
    
            // detach persist nodes
            // 将持久节点从旧场景中移除,并暂时保存到persistNodeList中
            var game = cc.game;
            var persistNodeList = Object.keys(game._persistRootNodes).map(function (x) {
                return game._persistRootNodes[x];
            });
            for (let i = 0; i < persistNodeList.length; i++) {
                let node = persistNodeList[i];
                game._ignoreRemovePersistNode = node;
                node.parent = null;
                game._ignoreRemovePersistNode = null;
            }
    
            var oldScene = this._scene;
    
            // auto release assets
            // 调用autoRelease进行资源释放,传入旧场景资源和新场景资源进行对比释放
            // 当一个资源【勾选了自动释放且没有被新场景引用到时】就会被释放
            console.time(AUTO_RELEASE);
            var autoReleaseAssets = oldScene && oldScene.autoReleaseAssets && oldScene.dependAssets;
            AutoReleaseUtils.autoRelease(autoReleaseAssets, scene.dependAssets, persistNodeList);
            console.timeEnd(AUTO_RELEASE);
    
            // unload scene
            // 释放旧的场景,销毁所有子节点和组件
            console.time(DESTROY);
            if (cc.isValid(oldScene)) {
                oldScene.destroy();
            }
    
            this._scene = null;
    
            // purge destroyed nodes belongs to old scene
            cc.Object._deferredDestroy();
            console.timeEnd(DESTROY);
    
            // 执行开始加载场景回调并触发对应的事件(其实这里应该是启动场景)
            if (onBeforeLoadScene) {
                onBeforeLoadScene();
            }
            this.emit(cc.Director.EVENT_BEFORE_SCENE_LAUNCH, scene);
    
            var sgScene = scene;
    
            // Run an Entity Scene
            if (scene instanceof cc.Scene) {
                this._scene = scene;
                sgScene = scene._sgNode;
    
                // Re-attach or replace persist nodes
                // 重新添加持久节点到新场景中,如果发现新场景有相同的节点,这里会执行一个替换的操作
                console.time(ATTACH_PERSIST);
                for (let i = 0; i < persistNodeList.length; i++) {
                    let node = persistNodeList[i];
                    var existNode = scene.getChildByUuid(node.uuid);
                    if (existNode) {
                        // scene also contains the persist node, select the old one
                        var index = existNode.getSiblingIndex();
                        existNode._destroyImmediate();
                        scene.insertChild(node, index);
                    }
                    else {
                        node.parent = scene;
                    }
                }
                // 激活新场景
                console.timeEnd(ATTACH_PERSIST);
                console.time(ACTIVATE);
                scene._activate();
                console.timeEnd(ACTIVATE);
            }
    
            // Run or replace rendering scene
            // 启动或替换场景
            if (!this.getRunningScene()) {
                this.runWithScene(sgScene);
            }
            else {
                this.replaceScene(sgScene);
            }
    
            // 执行场景启动完成的回调,并触发事件
            if (onLaunched) {
                onLaunched(null, scene);
            }
            this.emit(cc.Director.EVENT_AFTER_SCENE_LAUNCH, scene);
        },
    

    autoRelease传入2个场景的资源,以及持久节点,自动释放掉应该自动释放的资源(下个场景和持久节点引用到的资源不会被释放,标记为自动释放的资源会被释放)

        autoRelease: function (oldSceneAssets, nextSceneAssets, persistNodes) {
            var releaseSettings = cc.loader._autoReleaseSetting;
            var excludeMap = JS.createMap();
    
            // collect next scene assets
            // 收集下一个场景所需的资源
            if (nextSceneAssets) {
                for (let i = 0; i < nextSceneAssets.length; i++) {
                    excludeMap[nextSceneAssets[i]] = true;
                }
            }
    
            // collect assets used by persist nodes
            // 收集常驻节点引用的资源
            for (let i = 0; i < persistNodes.length; i++) {
                visitNode(persistNodes[i], excludeMap)
            }
    
            // remove ununsed scene assets
            // 移除旧场景中不再使用的资源
            if (oldSceneAssets) {
                for (let i = 0; i < oldSceneAssets.length; i++) {
                    let key = oldSceneAssets[i];
                    if (releaseSettings[key] !== false && !excludeMap[key]) {
                        cc.loader.release(key);
                    }
                }
            }
    
            // remove auto release assets
            // (releasing asset will change _autoReleaseSetting, so don't use for-in)
            // 释放标记了auto release的资源
            var keys = Object.keys(releaseSettings);
            for (let i = 0; i < keys.length; i++) {
                let key = keys[i];
                if (releaseSettings[key] === true && !excludeMap[key]) {
                    cc.loader.release(key);
                }
            }
        },
    

    cc.loader.release的实现如下,release并不会去释放它依赖的资源,只是释放这个资源本身。将资源从cc.loader中移除,如果该资源的content是一个cc.Asset,会调用它的release、并release其rawUrls对应的资源。如果是纹理则会调用cc.textureCache.removeTextureForKey进行移除,而声音类型的资源会执行cc.audioEngine.uncache进行释放。

    proto.release = function (asset) {
        if (Array.isArray(asset)) {
            for (let i = 0; i < asset.length; i++) {
                var key = asset[i];
                this.release(key);
            }
        } else if (asset) {
            var id = this._getReferenceKey(asset);
            var item = this.getItem(id);
            if (item) {
                var removed = this.removeItem(id);
                asset = item.content;
                if (asset instanceof cc.Asset) {
                    if (CC_JSB && asset instanceof cc.SpriteFrame && removed) {
                        // for the "Temporary solution" in deserialize.js
                        asset.release();
                    }
                    var urls = asset.rawUrls;
                    for (let i = 0; i < urls.length; i++) {
                        this.release(urls[i]);
                    }
                } else if (asset instanceof cc.Texture2D) {
                    cc.textureCache.removeTextureForKey(item.rawUrl || item.url);
                } else if (AUDIO_TYPES.indexOf(item.type) !== -1) {
                    cc.audioEngine.uncache(item.rawUrl || item.url);
                }
                if (CC_DEBUG && removed) {
                    this._releasedAssetChecker_DEBUG.setReleased(item, id);
                }
            }
        }
    };
    

    cc.loader._autoReleaseSetting记录了所有资源是否会自动释放。通过cc.loader.setAutoRelease或setAutoReleaseRecursively可以控制是否自动释放。

    proto.setAutoRelease = function (assetOrUrlOrUuid, autoRelease) {
        var key = this._getReferenceKey(assetOrUrlOrUuid);
        if (key) {
            this._autoReleaseSetting[key] = !!autoRelease;
        }
        else if (CC_DEV) {
            cc.warnID(4902);
        }
    };
    
    proto.setAutoReleaseRecursively = function (assetOrUrlOrUuid, autoRelease) {
        autoRelease = !!autoRelease;
        var key = this._getReferenceKey(assetOrUrlOrUuid);
        if (key) {
            this._autoReleaseSetting[key] = autoRelease;
    
            var depends = AutoReleaseUtils.getDependsRecursively(key);
            for (var i = 0; i < depends.length; i++) {
                var depend = depends[i];
                this._autoReleaseSetting[depend] = autoRelease;
            }
        }
        else if (CC_DEV) {
            cc.warnID(4902);
        }
    };
    

    所有loadRes加载进来的资源都会自动执行setAutoReleaseRecursively(uuid, false),如果我们将某个资源设置为自动释放,然后用loadRes加载了一个依赖了该资源的新资源,之前的自动释放设置会被覆盖。

    • 预加载场景

    preloadScene的实现非常简单,拿到场景信息之后触发EVENT_BEFORE_SCENE_LOADING事件并调用cc.loader.load加载资源。这个流程与切换场景并不冲突,只是让场景资源加载的这个流程提前了而已,预加载的场景就算不是接下来要切换的场景,也不会冲突,但可能造成性能和内存的浪费。

    Creator2.x的preloadScene比1.x多了一个onProgress参数,在cc.loader.load的时候传入。

        preloadScene: function (sceneName, onLoaded) {
            var info = this._getSceneUuid(sceneName);
            if (info) {
                this.emit(cc.Director.EVENT_BEFORE_SCENE_LOADING, sceneName);
                cc.loader.load({ uuid: info.uuid, type: 'uuid' }, function (error, asset) {
                    if (error) {
                        cc.errorID(1210, sceneName, error.message);
                    }
                    if (onLoaded) {
                        onLoaded(error, asset);
                    }
                });
            } else {
                var error = 'Can not preload the scene "' + sceneName + '" because it is not in the build settings.';
                onLoaded(new Error(error));
                cc.error('preloadScene: ' + error);
            }
        },
    
  • 相关阅读:
    Mybatis(4) 映射文件-参数处理
    Mybatis(3) 映射文件-增删改查
    Mabatis(2) 全局配置文件
    Mybatis(1) 创建Mybatis HelloWorld
    过滤器和拦截器之间的区别
    Redis(3) 配置文件 redis.conf
    Redis(2) 数据类型
    Redis(1) 初识Redis
    ActiveMQ(4) ActiveMQ JDBC 持久化 Mysql 数据库
    8.字典
  • 原文地址:https://www.cnblogs.com/ybgame/p/10844766.html
Copyright © 2011-2022 走看看