cocos creator大厅和子游戏

思考:鄙人从业cocos开发5、6年,之前一直用cocos2d-x c++版开发儿童教育类游戏。随着业务的增加,子游戏达到80多个了。也伴随一些问题出现,比如这些问题:1、包体太大。我们app包最大的时候达到200M,这对于用户来说实在是比较大呀!特别是安卓用户。2、如果某个学习的游戏有bug,需要再次发版修复。安卓平台审核比较快,可是苹果可慢了,一周的时间。之前我和小伙伴们也有想着解决这些问题。比如,让子游戏的资源可以从服务器下载,代码生成的.a、 .so动态库也下载。虽然解决了上述的问题,但是,太多的子游戏编译比较长,上传服务器测试等待时间长等问题。我们的业务已经不适合用cocos2d-x来做了。我也承认c++更加流畅、功能丰富、库多、调试方便,可是能驾驭c++的程序员有几个。一直关注CocosCreator的发展,发现这个新产物完全可以满足我们的业务。

接下来我们带着几个问题来探讨如何实现大厅和子游戏的^ - ^

一、CocosCreator能热更新吗?就是可以将代码和资源一起下载下来的那种可以吗?子游戏可以下载吗?

答:答案是可以,cocos官网就有详细的文档介绍热更新了。热更新文档在这里就不在介绍了。
大概的步骤:

  • 1、用version_generator.js生成新版本的资源配置文件;
  • 2、将资源放到配置文件对应的服务器中,最好是zip包;
  • 3、每次进入游戏时,用jsb.AssetsManager检查版本checkUpdate()检查版本和更新版本update()

问题来了,我们子游戏如何热更新呢?经过测试,子游戏只要服务器保存的地址不一样,版本号不一样就可以热更新。一般大厅都是一个游戏列表,携带子游戏的下载地址、是否更新、游戏图片、游戏名称等。点击大厅某个子游戏图标然后,热更新对应子游戏的地址。如果版本和本地不一致,assetsManager就会更新下载。客户端保存的子游戏的地方一般是jsb.fileUtils.getWritablePath()+'/'+md5(子游戏版服务器地址+子游戏版本)

下面是提供新版本2.0.6的代码

@ccclass
export default class HomeScene extends cc.Component {

	start() {
		initAssetManage("game1");
 		checkUpdate("game1");
    }

    //**        热更新代码       ***/
    initAssetManage(gameName) {
        if (cc.sys.isBrowser) {
            return;
        }
        this.storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '') + gameName);

        let versionComareHandle = function (versionA, versionB) {
            let vA = versionA.split('.');
            let vB = versionB.split('.');
            for (let i = 0; i < vA.length; ++i) {
                let a = parseInt(vA[i]);
                let b = parseInt(vB[i] || 0);
                if (a !== b) {
                    return a - b;
                }
            }
            return vB.length > vA.length ? -1 : 0;
        }

        this.assetsManager = new jsb.AssetsManager('', this.storagePath, versionComareHandle);

        // this.assetsManager.setVerifyCallback((filePath, asset) => {           
        //获取下载下来的文件的MD5跟manifest文件的md5进行比较,是否文件有问题
        // if (filePath.endsWith('project.manifest')) return true;
        // let data = jsb.fileUtils.getDataFromFile(filePath);
        // let fileMD5 = MD5(data);
        // let assetsMD5 = asset.md5;
        // return (fileMD5 === assetsMD5 || assetsMD5 === '111111');
        // });

        if (cc.sys.os === cc.sys.OS_ANDROID) {
            this.assetsManager.setMaxConcurrentTask(2);
        }

    }


    checkCb(event) {
        switch (event.getEventCode()) {
            case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
                cc.log('本地没有配置文件: ' + event.getMessage());
                break;
            case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
                cc.log('下载配置文件错误: ' + event.getMessage());
                break;
            case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
                cc.log('解析文件错误: ' + event.getMessage());
                break;
            case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                cc.log('已经是最新的: ' + event.getMessage());
                setTimeout(() => this.getCourseConfig(), 2000);
                break;
            case jsb.EventAssetsManager.NEW_VERSION_FOUND:
                cc.log('找到新版本');
                setTimeout(() => this.hotUpdate(), 2000);
                return;
            default:
                return;
        }
    }

    checkUpdate(gameName) {
        if (cc.sys.isBrowser) return;

        let UIRLFILE = "http://192.168.0.1:8080/server/" + gameName;
        let remoteManifestUrl = this.storagePath + "/project.manifest";
        this.manifestUrl = remoteManifestUrl;

        let customManifestStr = JSON.stringify({
            "packageUrl": UIRLFILE,
            "remoteManifestUrl": UIRLFILE + "/project.manifest",
            "remoteVersionUrl": UIRLFILE + "/version.manifest",
            "version": "1.0.0.0",
            "assets": {},
            "searchPaths": []
        });


        this.assetsManager.setEventCallback(this.checkCb.bind(this));

        if (this.assetsManager.getState() === jsb.AssetsManager.State.UNINITED) {
            if (jsb.fileUtils.isFileExist(remoteManifestUrl)) {
                cc.log('加载本地Manifest');
                this.assetsManager.loadLocalManifest(this.manifestUrl);
            } else {
                cc.log('加载网络Manifest');
                let manifest = new jsb.Manifest(customManifestStr, this.storagePath);
                this.assetsManager.loadLocalManifest(manifest, this.storagePath);
            }
        }

        cc.log("检查文件更新:" + remoteManifestUrl);
        this.assetsManager.checkUpdate();
    }


    updateCb(event) {
        switch (event.getEventCode()) {
            case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
                /*0 本地没有配置文件*/
                cc.log('本地没有配置文件: ' + event.getMessage());
                break;
            case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
                /*1下载配置文件错误*/
                cc.log('下载配置文件错误: ' + event.getMessage());
                break;
            case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
                /*2 解析文件错误*/
                cc.log('解析文件错误: ' + event.getMessage());
                break;
            case jsb.EventAssetsManager.NEW_VERSION_FOUND:
                /*3发现新的更新*/
                cc.log('发现新的更新: ' + event.getMessage());
                break;
            case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                /*4 已经是最新的*/
                cc.log('已经是最新的: ' + event.getMessage());
                break;
            case jsb.EventAssetsManager.UPDATE_PROGRESSION:
                /*5 最新进展  做 进度的*/
                var msg = Math.round(event.getPercent() * 100) + "%";
                cc.log('进度: ' + msg);
                // this.progress.string =  msg;
                break;
            case jsb.EventAssetsManager.ASSET_UPDATED:
                /*6需要更新*/
                // cc.log('需要更新: ' + event.getMessage());
                break;
            case jsb.EventAssetsManager.ERROR_UPDATING:
                /*7更新错误*/
                cc.log('更新错误: ' + event.getMessage());
                break;
            case jsb.EventAssetsManager.UPDATE_FINISHED:
                /*8更新完成*/
                cc.log("更新完成");
                setTimeout(() => this.getCourseConfig(), 2000);
                break;
            case jsb.EventAssetsManager.UPDATE_FAILED:
                /*9更新失败*/
                cc.log('更新失败: ' + event.getMessage());
                // this.getfiles("subgame", 1);
                break;
            case jsb.EventAssetsManager.ERROR_DECOMPRESS:
                /*10解压失败*/
                cc.log('解压失败: ' + event.getMessage());
                break;
        }
    }

    hotUpdate() {
        if (this.assetsManager) {

            if (this.assetsManager.getState() === jsb.AssetsManager.State.UNINITED) {
                this.assetsManager.loadLocalManifest(this.manifestUrl);
            }

            this.assetsManager.setEventCallback(this.updateCb.bind(this));

            this.assetsManager.update();

        }
    }

    //**        热更新代码       ***/
}

二、子游戏如何进入退出?

答:这个问题是一个不错的问题,经过2周对CocosCreator的研究,特别游戏启动的过程退出的过程。
1、让我们先讨论一下【进入】;
进入流程如下图:
游戏进入流程

从图上来看到,main.js是需要好好研究就的。添加一些注释大家可以更好的理解mian.js是如何走。

// QQPlay window need to be inited first
if (false) {
    BK.Script.loadlib('GameRes://libs/qqplay-adapter.js');
}

window.boot = function () {
	//*step2 读取配置 把window._CCStettings
    var settings = window._CCSettings;
    window._CCSettings = undefined;

    if ( !settings.debug ) {
        var uuids = settings.uuids;

        var rawAssets = settings.rawAssets;
        var assetTypes = settings.assetTypes;
        var realRawAssets = settings.rawAssets = {};
        for (var mount in rawAssets) {
            var entries = rawAssets[mount];
            var realEntries = realRawAssets[mount] = {};
            for (var id in entries) {
                var entry = entries[id];
                var type = entry[1];
                // retrieve minified raw asset
                if (typeof type === 'number') {
                    entry[1] = assetTypes[type];
                }
                // retrieve uuid
                realEntries[uuids[id] || id] = entry;
            }
        }

        var scenes = settings.scenes;
        for (var i = 0; i < scenes.length; ++i) {
            var scene = scenes[i];
            if (typeof scene.uuid === 'number') {
                scene.uuid = uuids[scene.uuid];
            }
        }

        var packedAssets = settings.packedAssets;
        for (var packId in packedAssets) {
            var packedIds = packedAssets[packId];
            for (var j = 0; j < packedIds.length; ++j) {
                if (typeof packedIds[j] === 'number') {
                    packedIds[j] = uuids[packedIds[j]];
                }
            }
        }
    }

    function setLoadingDisplay () {
        // Loading splash scene
        var splash = document.getElementById('splash');
        var progressBar = splash.querySelector('.progress-bar span');
        cc.loader.onProgress = function (completedCount, totalCount, item) {
            var percent = 100 * completedCount / totalCount;
            if (progressBar) {
                progressBar.style.width = percent.toFixed(2) + '%';
            }
        };
        splash.style.display = 'block';
        progressBar.style.width = '0%';

        cc.director.once(cc.Director.EVENT_AFTER_SCENE_LAUNCH, function () {
            splash.style.display = 'none';
        });
    }

	//*step5 初始化大环境后进入具体的场景
    var onStart = function () {
        cc.loader.downloader._subpackages = settings.subpackages;

        cc.view.enableRetina(true);
        cc.view.resizeWithBrowserSize(true);

        if (!false && !false) {
            if (cc.sys.isBrowser) {
                setLoadingDisplay();
            }

            if (cc.sys.isMobile) {
                if (settings.orientation === 'landscape') {
                    cc.view.setOrientation(cc.macro.ORIENTATION_LANDSCAPE);
                }
                else if (settings.orientation === 'portrait') {
                    cc.view.setOrientation(cc.macro.ORIENTATION_PORTRAIT);
                }
                cc.view.enableAutoFullScreen([
                    cc.sys.BROWSER_TYPE_BAIDU,
                    cc.sys.BROWSER_TYPE_WECHAT,
                    cc.sys.BROWSER_TYPE_MOBILE_QQ,
                    cc.sys.BROWSER_TYPE_MIUI,
                ].indexOf(cc.sys.browserType) < 0);
            }

            // Limit downloading max concurrent task to 2,
            // more tasks simultaneously may cause performance draw back on some android system / browsers.
            // You can adjust the number based on your own test result, you have to set it before any loading process to take effect.
            if (cc.sys.isBrowser && cc.sys.os === cc.sys.OS_ANDROID) {
                cc.macro.DOWNLOAD_MAX_CONCURRENT = 2;
            }
        }

        // init assets 配置项目中资源的uuid,方便后面加载对应资源转换为路径。
        //这样进入场景后才能找到资源。
        cc.AssetLibrary.init({
            libraryPath: 'res/import',
            rawAssetsBase: 'res/raw-',
            rawAssets: settings.rawAssets,
            packedAssets: settings.packedAssets,
            md5AssetsMap: settings.md5AssetsMap
        });

        var launchScene = settings.launchScene;

        // load scene 进入第一个场景
        cc.director.loadScene(launchScene, null,
            function () {
                if (cc.sys.isBrowser) {
                    // show canvas
                    var canvas = document.getElementById('GameCanvas');
                    canvas.style.visibility = '';
                    var div = document.getElementById('GameDiv');
                    if (div) {
                        div.style.backgroundImage = '';
                    }
                }
                cc.loader.onProgress = null;
                console.log('Success to load scene: ' + launchScene);
            }
        );
    };

    // jsList
    var jsList = settings.jsList;

    if (false) {
        BK.Script.loadlib();
    }
    else {
    	//*step3 加载项目源码
    	//游戏所有源码都被编译成一个文件了,那就src/project.js 没有什么可看
        var bundledScript = settings.debug ? 'src/project.dev.js' : 'src/project.js';
        if (jsList) {
            jsList = jsList.map(function (x) {
                return 'src/' + x;
            });
            jsList.push(bundledScript);
        }
        else {
            jsList = [bundledScript];
        }
    }
    
    //*step4 根据配置开启游戏
    var option = {
        id: 'GameCanvas',
        scenes: settings.scenes,
        debugMode: settings.debug ? cc.debug.DebugMode.INFO : cc.debug.DebugMode.ERROR,
        showFPS: !false && settings.debug,
        frameRate: 60,
        jsList: jsList,
        groupList: settings.groupList,
        collisionMatrix: settings.collisionMatrix,
    }

    cc.game.run(option, onStart);
};

// main.js is qqplay and jsb platform entry file, so we must leave platform init code here
if (false) {
    BK.Script.loadlib('GameRes://src/settings.js');
    BK.Script.loadlib();
    BK.Script.loadlib('GameRes://libs/qqplay-downloader.js');

    var ORIENTATIONS = {
        'portrait': 1,
        'landscape left': 2,
        'landscape right': 3
    };
    BK.Director.screenMode = ORIENTATIONS[window._CCSettings.orientation];
    initAdapter();
    cc.game.once(cc.game.EVENT_ENGINE_INITED, function () {
        initRendererAdapter();
    });

    qqPlayDownloader.REMOTE_SERVER_ROOT = "";
    var prevPipe = cc.loader.md5Pipe || cc.loader.assetLoader;
    cc.loader.insertPipeAfter(prevPipe, qqPlayDownloader);
    
    window.boot();
}
else if (window.jsb) {
	//*step1 加载项目的配置到 window._CCSettings 
	//查看src/settings.js 发现window._CCSettings={....} 里面都是项目配置
	//几个常用:
	//groupList 分组
	//collisionMatrix 刚体碰撞配置表
	//rawAssets 资源列表
	//launchScene 开始场景
	//packedAssets资源打包列表uuid
    require('src/settings.js');
    require('src/cocos2d-jsb.js');
    require('jsb-adapter/engine/index.js');
    window.boot();
}

大概了解进入流程后,我们假设一个命题:【 在大厅运行的情况下,我们更新配置是否可以进入子游戏。】。这个命题也是有基础支撑的。
一个是require('/xxx/x/x/xxxx.js');,这个函数可以加载绝对路径的脚本到jsVM,哪怕是sd卡里的。我们可以把子游戏的配置文件(subGamePath/src/settings.js)和代码(subGamePath/src/project.js)加载进来了。
第二个是jsb.fileUtils.addSearchPath(subGamePath, true);,为什么是他?用过cocos2d-x的小伙伴都知道,它添加搜索路径后,不管游戏资源在哪里都能加载。哪怕还是在sd卡里。这样,子游戏的资源路径加载到搜索路径后,子游戏资源我们就可以找到了。
有了这两个支撑点,我们可以加载一个完整的子游戏了。之后,就是代码替换的工作了。替换的过程和main.js的工作比较相似。让我们看看具体是如何进入子游戏的,直接上代码。

//支撑cocoscreator版本2.0.6或更高
cc.Hall = cc.Hall || {};

cc.Hall.changeConfig = function (subGamePath) {
    subGamePath = subGamePath ? subGamePath : '';

    if (window.jsb) {
        /// 1.初始化资源Lib路径Root.

        // var subGamePath = '/storage/emulated/0/bubbleGame/';
        cc.log("Hall ----->subGamePath:" + subGamePath);
        /// 2.subgame资源未映射,则初始化资源映射表,否则略过映射.

        // cc.sys.garbageCollect();
        // cc.sys.restartVM();
        /// 加载settings.js
        // var hallSettings = window._CCSettings;
        window.require(subGamePath + 'src/settings.js');
        cc.log("Hall ----->加载settings.js");
        var settings = window._CCSettings;
        // settings.rawAssets.push(hallSettings.rawAssets);
        // settings.packedAssets.push(hallSettings.packedAssets);
        // settings.md5AssetsMap.push(hallSettings.md5AssetsMap);

        window._CCSettings = undefined;

        if (!settings.debug) {
            var uuids = settings.uuids;

            var rawAssets = settings.rawAssets;
            var assetTypes = settings.assetTypes;
            var realRawAssets = settings.rawAssets = {};
            for (var mount in rawAssets) {
                var entries = rawAssets[mount];
                var realEntries = realRawAssets[mount] = {};
                for (var id in entries) {
                    var entry = entries[id];
                    var type = entry[1];
                    // retrieve minified raw asset
                    if (typeof type === 'number') {
                        entry[1] = assetTypes[type];
                    }
                    // retrieve uuid
                    realEntries[uuids[id] || id] = entry;
                }
            }

            var scenes = settings.scenes;
            for (var i = 0; i < scenes.length; ++i) {
                var scene = scenes[i];

                if (typeof scene.uuid === 'number') {
                    scene.uuid = uuids[scene.uuid];
                }
            }

            var packedAssets = settings.packedAssets;
            for (var packId in packedAssets) {
                var packedIds = packedAssets[packId];
                for (var j = 0; j < packedIds.length; ++j) {
                    if (typeof packedIds[j] === 'number') {
                        packedIds[j] = uuids[packedIds[j]];
                    }
                }
            }
        }

        /// 加载project.js
        var projectDir = 'src/project.js';
        if (settings.debug) {
            projectDir = 'src/project.dev.js';
        }
        cc.log("Hall ----->加载project.js");
        window.require(subGamePath + projectDir);

        /// 如果当前搜索路径没有subgame,则添加进去搜索路径。
        var currentSearchPaths = jsb.fileUtils.getSearchPaths();
        cc.log("Hall ----->currentSearchPaths:" + currentSearchPaths);
        if (currentSearchPaths && currentSearchPaths.indexOf(subGamePath) === -1) {
            jsb.fileUtils.addSearchPath(subGamePath, true);
            cc.log('Hall -----> 之前未添加,添加下subGamePath' + currentSearchPaths);
        }

        cc.AssetLibrary.init({
            libraryPath:  'res/import',
            rawAssetsBase:  'res/raw-',
            rawAssets: settings.rawAssets,
            packedAssets: settings.packedAssets,
            md5AssetsMap: settings.md5AssetsMap
        });


        cc.game.collisionMatrix = settings.collisionMatrix;
        cc.game.groupList = settings.groupList;

        cc.log('Hall ----->AssetLibrary init');
        var launchScene = settings.launchScene;

        /// 将subgame的场景添加到cc.game中,使得cc.director.loadScene可以从cc.game._sceneInfos查找到相关场景
        for (var i = 0; i < settings.scenes.length; ++i) {
            cc.game._sceneInfos.push(settings.scenes[i]);
        }

        /// 3.加载初始场景
        cc.log('Hall ----->加载初始场景');
        cc.director.loadScene(launchScene, null,
            function () {
                cc.log('Hall ----->成功加载初始场景' + launchScene);
            }
        );

    }

}

//进入子游戏
cc.Hall.enterSubGame = function (subGamePath) {
    this.changeConfig(subGamePath);
}

//回到大厅
cc.Hall.backHall = function () {
    this.changeConfig();
}

看了代码后大家可能觉得就这么简单,是的,就这么简单。如何使用:

 //进入子游戏
 window.require('src/Hall.js');
 //gamePath子游戏的下载保存路径也许是sd卡也许是jsb.fileUtils.getWritablePath()+'/'+md5(子游戏版服务器地址+子游戏版本)
 cc.Hall.enterSubGame(gamePath);


//回到大厅
 window.require('src/Hall.js');
 cc.Hall.backHall();

2、让我们讨论一下【退出】;
为什么退出只是不填路径就可以了?因为大厅的main.js中也没有填路径呀。
你看上面main.js代码里的*step1。没有路径?不是,jsVM在初始化的时候pwd就是指到工程的路径下面了。

三、大厅和子游戏如何数据交互?

答:答案在这里-》通过常驻节点进行场景资源管理和参数传递

具体实现:添加一个Gloal节点和Global.js保存为常驻节点。子游戏在不重启游戏的情况下,可以访问常驻节点数据。
在这里插入图片描述

在这里插入图片描述


@ccclass
export default class Global extends cc.Component {


    //课程资源
    public gameData;

    public gameScriptName: string

    public currentGameId = 0;
    public currentCourseId = 0;

    public engineIsReady = false;
}

@ccclass
export default class HomeScene extends cc.Component {
   onLoad() {
        cc.log("The loading scene did load")
        let globalNode = cc.find('Global');
        if (!cc.game.isPersistRootNode(globalNode)) {
            cc.game.addPersistRootNode(globalNode);
        }
}

@ccclass
export default class SubGameScene extends cc.Component {
   onLoad() {
        cc.log("The loading scene did load")
       let global = cc.find("Global").getComponent("Global") as Global
       let data = global.gameData
	}

}

四、子游戏共享大厅的资源和代码

我:这个下次再说;
看客:不是不会吧❓;
我:不是不写,?需要插件去做这个事情,更加方便些。人工去做,太苦。根本不合适。
看客:你不会写插件吧。
我:?呵呵。
在这里插入图片描述

我:能给的自有思路,这个插件你们拿去也没有用不骗人。大概是这样的,打包子游戏的时候,把大厅的资源源码和子游戏一起打包,然后剔除大厅的资源。这样子游戏的settings.js里会有大厅资源的uuid,然而子游戏中代码会有大厅的代码。剔除后子游戏会根据settings.js里的uuid去找大厅的资源。代码在启动大厅的时候已经加载到jsVM,子游戏不会再次加载大厅的代码。

结语:好好工作,好好学习
在这里插入图片描述

Cocos Creator大厅是一个游戏开发引擎,适用于跨平台的游戏开发。它提供了一个灵活的开发环境,支持多种游戏类型的制作。Cocos Creator大厅具有以下特点: 首先,Cocos Creator大厅支持多平台发布。开发者可以在不同的平台上发布他们的游戏,包括iOS、Android、Windows等。这使得游戏可以触及到更广泛的受众,并获得更多的曝光,从而增加了游戏的市场价值。 其次,Cocos Creator大厅提供了丰富的工具和资源。它具有强大的动画编辑器,可以轻松地创建各种游戏角色的动画效果。此外,它还提供了丰富的预置资源库,开发者可以直接使用这些资源,加快游戏开发的速度。 另外,Cocos Creator大厅具有快速迭代和实时预览的能力。开发者可以实时编辑和调试游戏,无需重新编译和运行。这大大提高了游戏制作的效率,减少了开发时间。同时,Cocos Creator大厅还支持团队协作,多人可以同时开发和编辑游戏,促进了开发流程的顺畅进行。 最后,Cocos Creator大厅提供了丰富的社区支持。开发者可以在社区中获取到其他开发者的意见和建议,解决开发中遇到的问题。社区还提供了大量的教程和示例代码,方便开发者学习和借鉴。 综上所述,Cocos Creator大厅是一个功能强大的游戏开发引擎,为开发者提供了丰富的工具和资源,支持多平台发布和实时预览。它的出现大大简化了游戏开发的流程,提高了开发效率。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值