Cocos源码分析 (一)

因为工作需要对Cocos做一些较为底层的修改和定制化,近期对Cocos引擎源码做了一些分析。里面主要涉及到一些比较关键的点,其他部分阅读源码和注释就足够了。

网页版本和原生版本有一些差别,为了方便理解,先对web版本进行分析,理清引擎层逻辑,后面会事半功倍。现在引擎的最新版本是v3.1,和一年前相比,变化确实不小,因为2d 和 3d 已经合并,估计在可预见的未来,引擎整体架构不会发生太大变化。

调试web版本

调试运行web版,在index.html中,需要关注的代码段如下:

<!-- Polyfills bundle. -->
<script src="/scripting/polyfills/bundle.js"></script>

<!-- Import map that map the engine feature unit modules. -->
<script src="/scripting/engine/bin/.cache/dev/preview/import-map.json" type="systemjs-importmap"></script>

<!-- Import map that map the entry of project module(s). -->
<script src="/scripting/import-map-project" type="systemjs-importmap"></script>

<!-- Import map that map 'cc', 'cc/env'. -->
<script src="/scripting/import-map-global" type="systemjs-importmap"></script>

<!-- SystemJS support. -->
<script src="/scripting/systemjs/system.js"></script>

<script src="/scripting/engine/bin/.cache/dev/preview/bundled/index.js"></script>


<!-- Manifest file. -->
<script name="setting" src="/settings.js?scene=current_scene"></script>

<script>
    window.onload = function() {
        System.import("/preview-app/index.js").then(function (mod) {
            return mod.bootstrap({
                engineBaseUrl: '/scripting/engine',
                settings: window._CCSettings,
                devices: {"Default":{"name":"Default","width":960,"height":640,"ratio":1},"iPhone SE":{"name":"iPhone SE","width":375,"height":667,"ratio":2},"iPhone XS Max":{"name":"iPhone XS Max","width":414,"height":896,"ratio":3},"iPhone XS":{"name":"iPhone XS","width":375,"height":812,"ratio":3},"iPhone XR":{"name":"iPhone XR","width":414,"height":896,"ratio":2},"iPhone 8 Plus":{"name":"iPhone 8 Plus","width":414,"height":736,"ratio":2.6},"iPhone 8":{"name":"iPhone 8","width":375,"height":667,"ratio":2},"Galaxy S9 Plus":{"name":"Galaxy S9 Plus","width":412,"height":846,"ratio":3.5},"Galaxy S9":{"name":"Galaxy S9","width":360,"height":740,"ratio":4},"Galaxy S7":{"name":"Galaxy S7","width":360,"height":640,"ratio":4},"Huawei P20 Lite":{"name":"Huawei P20 Lite","width":360,"height":760,"ratio":3},"Huawei P20 Pro":{"name":"Huawei P20 Pro","width":360,"height":747,"ratio":3},"Huawei P20":{"name":"Huawei P20","width":360,"height":748,"ratio":3},"Huawei P10":{"name":"Huawei P10","width":360,"height":640,"ratio":3},"Moto Z2":{"name":"Moto Z2","width":360,"height":640,"ratio":4},"Redmi Note 5":{"name":"Redmi Note 5","width":393,"height":786,"ratio":2.75},"Redmi Note 4":{"name":"Redmi Note 4","width":360,"height":640,"ratio":3},"Pixel 3 XL":{"name":"Pixel 3 XL","width":393,"height":786,"ratio":3.67},"Pixel 2 XL":{"name":"Pixel 2 XL","width":412,"height":823,"ratio":3.5},"Pixel 2":{"name":"Pixel 2","width":412,"height":640,"ratio":2.6},"Nexus 6P":{"name":"Nexus 6P","width":412,"height":732,"ratio":3.5},"Nexus 6":{"name":"Nexus 6","width":360,"height":640,"ratio":4},"LG K20":{"name":"LG K20","width":360,"height":640,"ratio":2},"LG K7":{"name":"LG K7","width":320,"height":570,"ratio":1.5},"ZTE ZMAX Pro":{"name":"ZTE ZMAX Pro","width":360,"height":640,"ratio":3},"ZTE Majesty Pro":{"name":"ZTE Majesty Pro","width":320,"height":570,"ratio":1.5}},
            });
        }).catch(function(err) {
            console.error(err);
        });
    };
</script>

需要特别注意的是<script name="setting" src="/settings.js?scene=current_scene"></script>这一行,里面对window._CCSettings进行了赋值。这是引擎所需要的设置信息。具体每个设置项可以自行查阅引擎代码,后面也会涉及到一部分设置项。

window.onload事件中,借助SystemJS来动态加载preview-app/index.js模块。

SystemJS相关信息可以查看 `github.com/systemjs/systemjs`

preview-app中主要内容就是绘制预览时候的页面,包括fps设置,暂停恢复引擎,设置预览设备和分辨率等等,简单来说这部分的代码是和editor相关,与引擎运行时关系不大。

preview-app模块加载完成后回调用bootstrap方法,在所有页面绘制完成,模块加载完成后,会最终调用Game中的init方法以启动引擎。由于preview-app模块属于编辑器的一部分,所以无法查看远吗,js代码经过编译混淆已经无法阅读,智能通过断点方式查找一些与引擎相关的关键操作。具体的操作流程如下:

  1. 调用Game中的init方法,并传入配置信息
  2. 调用assetsManager.loadBundle,加载名为main的资源包
  3. 使用XMLHttpRequest加载current_scene场景json文件
  4. 设置 game.canvas.setAttribute(“tabindex”, -1)
  5. 设置 game.canvas.style.backgroundColor = “”
  6. 监听 director.once(n.Director.EVENT_AFTER_SCENE_LAUNCH,…) 事件
  7. 设置 view.enableRetina(!0)
  8. 设置 view.resizeWithBrowserSize(!0)
  9. 设置 view.enableAutoFullScreen(!1)
  10. 设置 view.setDesignResolutionSize(Number®, Number(i), a || n.ResolutionPolicy.FIXED_HEIGHT)
  11. 调用 n.game.pause() 暂停游戏引擎
  12. 调用 assetManager.loadWithJson 加载场景资源
  13. assetManager.loadWithJson 加载中 调用 e.reportLoadProgress®,刷新进度条
  14. assetManager.loadWithJson 加载完成后,调用 director.runSceneImmediate 运行刚刚加载完成的场景。加载完成前调用 game.resume() 恢复游戏引擎
  15. director.once(n.Director.EVENT_AFTER_SCENE_LAUNCH) 事件触发,关闭预览界面的logo和进度条等元素,进入游戏。

相对来说,在预览状态下,很多操作都和preview功能相关,而且一些配置文件因为要与编辑器联动,很多配置都处于可设置状态。

release web版本

相比调试版本,release版本的代码则显得“干净”许多。在index.html中,通过SystemJS加载index.js文 件和application.js文件,随后index.js会先创建页面中的canvas标签。紧接着就是加载引擎文件,这里我遇到了一个以前没有见过的东西,此环节加载了一个名为ammo.wasm的文件,实际上是 github.com/kripken/ammo.js 这个物理引擎的WebAssembly build 版本。

之后使用XMLHttpRequest家在并设置settings.json文件,并赋值给window._CCSettings。

window._CCSettings会经过一系列值的判断和处理,并将处理后的结果作为参数传递给cc.game.init ,之后则依然调用assetsManager.loadBundle,加载资源包。完成后调用cc.game.run启动引擎。当引擎启动完成后,会回调一个名为onGameStarted的函数,函数内容如下:

  function onGameStarted(cc, settings) {
    window._CCSettings = undefined;
    cc.view.enableRetina(true);
    cc.view.resizeWithBrowserSize(true);

    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(false);
    }

    var launchScene = settings.launchScene; // load scene

    cc.director.loadScene(launchScene, null, function () {
      cc.view.setDesignResolutionSize(960, 640, 4);
      console.log("Success to load scene: ".concat(launchScene));
    });
  }