Cocos Creator 源碼解讀:引擎啟動與主循環(huán)

前言

預(yù)備

不知道你有沒有想過谴分,假如把游戲世界比作一輛汽車谬擦,那么這輛“汽車”是如何啟動颗品,又是如何持續(xù)運(yùn)轉(zhuǎn)的呢袖牙?

如題侧巨,本文的內(nèi)容主要為 Cocos Creator 引擎的啟動流程主循環(huán)

而在主循環(huán)的內(nèi)容中還會涉及到:組件的生命周期和計時器鞭达、緩動系統(tǒng)司忱、動畫系統(tǒng)和物理系統(tǒng)等...

本文會在宏觀上為大家解讀主循環(huán)與各個模塊之間的關(guān)系,對于各個模塊也會簡單介紹畴蹭,但不會深入到模塊的具體實(shí)現(xiàn)坦仍。

因?yàn)槿绻衙總€模塊都“摸”一遍,那這篇文章怕是寫不完了叨襟。

Go!

希望大家看完這篇文章之后能夠更加了解 Cocos Creator 引擎桨踪。

同時也希望本文可以起到“師傅領(lǐng)進(jìn)門”的作用,大家一起加油修行鴨~

另外《源碼解讀》系列(應(yīng)該)會持續(xù)更新芹啥,如果你想要皮皮來解讀解讀引擎的某個模塊,也歡迎留言告訴我铺峭,我...我考慮下哈哈哈~

本文以 Cocos Creator 2.4.3 版本為參考墓怀。


正文

啟動流程

index.html

對于 Web 平臺 index.html 文件就是絕對的起點(diǎn)。

在默認(rèn)的 index.html 文件中卫键,定義了游戲啟動頁面的布局傀履,加載了 main.js 文件,并且還有一段立即執(zhí)行的代碼。

這里截取文件中一部分比較關(guān)鍵的代碼:

// 加載引擎腳本
loadScript(debug ? 'cocos2d-js.js' : 'cocos2d-js-min.ec334.js', function () {
    // 是否開啟了物理系統(tǒng)钓账?
    if (CC_PHYSICS_BUILTIN || CC_PHYSICS_CANNON) {
        // 加載物理系統(tǒng)腳本并啟動引擎
        loadScript(debug ? 'physics.js' : 'physics-min.js', window.boot);
    } else {
        // 啟動引擎
        window.boot();
    }
});
復(fù)制代碼

上面這段代碼主要用于加載引擎腳本和物理系統(tǒng)腳本碴犬,腳本加載完成之后就會調(diào)用 main.js 中定義的 window.boot() 函數(shù)。

?? 對于原生平臺梆暮,會在 {項目目錄}build\jsb-link\frameworks\runtime-src\Classes\AppDelegate.cpp 文件的 applicationDidFinishLaunching() 函數(shù)中加載 main.js 文件服协。(感謝請容我安眠大佬的補(bǔ)充)

?? 代碼壓縮

腳本文件名中帶有 -min 字樣一般代表著這個文件內(nèi)的代碼是被壓縮過的。

壓縮代碼可以節(jié)省代碼文件所占用的空間啦粹,加快文件加載速度偿荷,減少流量消耗,但同時也讓代碼失去了可閱讀性唠椭,不利于調(diào)試跳纳。

所以開啟調(diào)試模式后會直接使用未經(jīng)過壓縮的代碼文件,便于開發(fā)調(diào)試和定位錯誤贪嫂。

main.js

window.boot()

對于不同平臺 main.js 的內(nèi)容也有些許差異寺庄,這里我們忽略差異部分,只關(guān)注其中關(guān)鍵的共同行為力崇。

關(guān)于 main.js 文件的內(nèi)容基本上就是定義了 window.boot() 函數(shù)斗塘。

?? 對于非 Web 平臺,會在定義完之后直接就調(diào)用 window.boot() 函數(shù)餐曹,所以 main.js 就是他們的起點(diǎn)逛拱。

window.boot() 函數(shù)內(nèi)部有以下關(guān)鍵行為:

  1. 定義 onStart 回調(diào)函數(shù):主要用于加載啟動場景
  2. cc.assetManager.init(...):初始化 AssetManager
  3. cc.assetManager.loadScript(...):加載 src 目錄下的插件腳本
  4. cc.assetManager.loadBundle(...):加載項目中的 bundle
  5. cc.game.run(...):啟動引擎

這部分的代碼就不貼了,小伙伴們可以看看自己的項目構(gòu)建后的 main.js 文件台猴。

cc.game

cc.game 對象是 cc.Game 類的一個實(shí)例朽合,cc.game 包含了游戲主體信息并負(fù)責(zé)驅(qū)動游戲。

說人話饱狂,cc.game 對象就是管理引擎生命周期的模塊曹步,啟動、暫停和重啟等操作都需要用到它休讳。

CCGame.js:github.com/cocos-creat…

run()

cc.game.run() 函數(shù)內(nèi)指定了引擎配置和 onStart 回調(diào)并觸發(fā) cc.game.prepare() 函數(shù)讲婚。

run: function (config, onStart) {
    // 指定引擎配置
    this._initConfig(config);
    this.onStart = onStart;
    this.prepare(game.onStart && game.onStart.bind(game));
}
復(fù)制代碼

傳送門:github.com/cocos-creat…

prepare()

cc.game.prepare() 函數(shù)內(nèi)主要在項目預(yù)覽時快速編譯項目代碼并調(diào)用 _prepareFinished() 函數(shù)。

prepare(cb) {
    // 已經(jīng)準(zhǔn)備過則跳過
    if (this._prepared) {
        if (cb) cb();
        return;
    }
    // 加載預(yù)覽項目代碼
    this._loadPreviewScript(() => {
        this._prepareFinished(cb);
    });
}
復(fù)制代碼

傳送門:github.com/cocos-creat…

對于快速編譯的細(xì)節(jié)俊柔,可以在項目預(yù)覽時打開瀏覽器的開發(fā)者工具筹麸,在 Sources 欄中搜索(Ctrl + P) __quick_compile_project__ 即可找到 __quick_compile_project__.js 文件。

_prepareFinished()

cc.game._prepareFinished() 函數(shù)的作用主要為初始化引擎、設(shè)置幀率計時器和初始化內(nèi)建資源(effect 資源和 material 資源)器予。

當(dāng)內(nèi)建資源加載完成后就會調(diào)用 cc.game._runMainLoop() 啟動引擎主循環(huán)荸频。

_prepareFinished(cb) {
    // 初始化引擎
    this._initEngine();
    // 設(shè)置幀率計時器
    this._setAnimFrame();
    // 初始化內(nèi)建資源(加載內(nèi)置的 effect 和 material 資源)
    cc.assetManager.builtins.init(() => {
        // 打印引擎版本到控制臺
        console.log('Cocos Creator v' + cc.ENGINE_VERSION);
        this._prepared = true;
        // 啟動 mainLoop
        this._runMainLoop();
        // 發(fā)射 ‘game_inited’ 事件(即引擎已初始化完成)
        this.emit(this.EVENT_GAME_INITED);
        // 調(diào)用 main.js 中定義的 onStart 函數(shù)
        if (cb) cb();
    });
}
復(fù)制代碼

傳送門:github.com/cocos-creat…

?? 對于 _prepareFinished() 內(nèi)調(diào)用的 _setAnimFrame() 函數(shù)這里必須提一下。

_setAnimFrame()

cc.game._setAnimFrame() 內(nèi)部對不同的游戲幀率做了適配酵紫。

另外還對 window.requestAnimationFrame() 接口做了兼容性封裝,用于兼容不同的瀏覽器環(huán)境,具體的我們下面再說奖地。

這里就不貼 _setAnimFrame() 的代碼了橄唬,有需要的小伙伴可自行查閱。

傳送門:github.com/cocos-creat…

_runMainLoop()

cc.game._runMainLoop() 這個函數(shù)的名字取得很簡單直接参歹,攤牌了它就是用來運(yùn)行 mainLoop() 函數(shù)的仰楚。

讓我們瞧瞧代碼:

_runMainLoop: function () {
    if (CC_EDITOR) return;
    if (!this._prepared) return;
    // 定義局部變量
    var self = this, callback, config = self.config,
        director = cc.director,
        skip = true, frameRate = config.frameRate;
    // 展示或隱藏性能統(tǒng)計
    debug.setDisplayStats(config.showFPS);
    // 設(shè)置幀回調(diào)
    callback = function (now) {
        if (!self._paused) {
            // 循環(huán)調(diào)用回調(diào)
            self._intervalId = window.requestAnimFrame(callback);
            if (!CC_JSB && !CC_RUNTIME && frameRate === 30) {
                if (skip = !skip) return;
            }
            // 調(diào)用 mainLoop
            director.mainLoop(now);
        }
    };
    // 將在下一幀開始循環(huán)回調(diào)
    self._intervalId = window.requestAnimFrame(callback);
    self._paused = false;
}
復(fù)制代碼

傳送門:github.com/cocos-creat…

通過以上代碼我們可以得知,_runMainLoop() 主要通過 window.requestAnimFrame() 接口來實(shí)現(xiàn)循環(huán)調(diào)用 mainLoop() 函數(shù)泽示。

window.requestAnimFrame()

window.requestAnimFrame() 就是我們上面說到的 _setAnimFrame() 內(nèi)部對于 window.requestAnimationFrame() 的兼容性封裝缸血。

對前端不太熟悉的小伙伴可能會有疑問,window.requestAnimationFrame() 又是啥械筛,是用來干嘛的捎泻,又是如何運(yùn)行的?

window.requestAnimationFrame()

簡單來說埋哟,window.requestAnimationFrame() 用于向?yàn)g覽器請求進(jìn)行一次重繪(repaint)笆豁,并在重繪之前調(diào)用指定的回調(diào)函數(shù)。

window.requestAnimationFrame() 接收一個回調(diào)作為參數(shù)并返回一個整數(shù)作為唯一標(biāo)識赤赊,瀏覽器將會在下一個重繪之前執(zhí)行這個回調(diào)闯狱;并且執(zhí)行回調(diào)時會傳入一個參數(shù),參數(shù)的值與 performance.now() 返回的值相等抛计。

performance.now() 的返回值可以簡單理解為瀏覽器窗口的運(yùn)行時長哄孤,即從打開窗口到當(dāng)前時刻的時間差。

MDN 文檔:developer.mozilla.org/zh-CN/docs/…

回調(diào)函數(shù)的執(zhí)行次數(shù)通常與瀏覽器屏幕刷新次數(shù)相匹配吹截,也就是說瘦陈,對于刷新率為 60Hz 的顯示器,瀏覽器會在一秒內(nèi)執(zhí)行 60 次回調(diào)函數(shù)波俄。

對于 window.requestAnimationFrame() 的說明到此為止晨逝,如果想要了解更多信息請自行搜索。

MDN 文檔:developer.mozilla.org/zh-CN/docs/…

小結(jié)

畫一張圖來對引擎的啟動流程做一個小小的總結(jié)叭~

主循環(huán)

經(jīng)歷了一番波折后懦铺,終于來到了最期待的引擎主循環(huán)部分捉貌,話不多說,我們繼續(xù)冬念!

cc.director

cc.director 對象是導(dǎo)演類 cc.Director 的實(shí)例趁窃,引擎通主要過 cc.director 對象來管理游戲的邏輯流程。

CCDirector.js:github.com/cocos-creat…

mainLoop()

?? cc.director.mainLoop() 函數(shù)可能是引擎中最關(guān)鍵的邏輯之一了急前,包含的內(nèi)容很多也很關(guān)鍵醒陆。

現(xiàn)在讓我們進(jìn)入 mainLoop() 函數(shù)內(nèi)部來一探究竟吧!

這里我選擇性剔除掉了函數(shù)中一些的代碼叔汁,還搞了點(diǎn)注釋:

mainLoop: function(now) {
    // 計算“全局”增量時間(DeltaTime)
    // 也就是距離上一次調(diào)用 mainLoop 的時間間隔
    this.calculateDeltaTime(now);
    // 游戲沒有暫停則進(jìn)行更新
    if (!this._paused) {
        // 發(fā)射 'director_before_update' 事件
        this.emit(cc.Director.EVENT_BEFORE_UPDATE);
        // 調(diào)用新增的組件(已啟用)的 start 函數(shù)
        this._compScheduler.startPhase();
        // 調(diào)用所有組件(已啟用)的 update 函數(shù)
        this._compScheduler.updatePhase(this._deltaTime);
        // 更新調(diào)度器(cc.Scheduler)
        this._scheduler.update(this._deltaTime);
        // 調(diào)用所有組件(已啟用)的 lateUpdate 函數(shù)
        this._compScheduler.lateUpdatePhase(this._deltaTime);
        // 發(fā)射 'director_after_update' 事件
        this.emit(cc.Director.EVENT_AFTER_UPDATE);
        // 銷毀最近被移除的實(shí)體(節(jié)點(diǎn))
        Obj._deferredDestroy();
    }
    // 發(fā)射 'director_before_draw' 事件
    this.emit(cc.Director.EVENT_BEFORE_DRAW);
    // 渲染游戲場景
    renderer.render(this._scene, this._deltaTime);
    // 發(fā)射 'director_after_draw' 事件
    this.emit(cc.Director.EVENT_AFTER_DRAW);
    // 更新事件管理器的事件監(jiān)聽(cc.eventManager 已被廢棄)
    eventManager.frameUpdateListeners();
    // 累加游戲運(yùn)行的總幀數(shù)
    this._totalFrames++;
}
復(fù)制代碼

傳送門:github.com/cocos-creat…

接下來我們來對主循環(huán)中的關(guān)鍵點(diǎn)一一進(jìn)行分解。

ComponentScheduler

cc.director 對象中的 _compScheduler屬性 是 ComponentScheduler 類的實(shí)例。

大多數(shù)小伙伴可能對于 ComponentScheduler 這個類沒有什么印象据块,我來簡單解釋一下码邻。

ComponentScheduler 的名字直譯過來就是“組件調(diào)度器”,從名字上就可以看出另假,這個類是用來調(diào)度組件的像屋。

說人話,ComponentScheduler 類是用來集中調(diào)度(管理)游戲場景中所有組件(cc.Component)的生命周期的边篮。

文字不夠直觀己莺,看完下面這張圖大概就懂了:

component-scheduler.js:github.com/cocos-creat…

startPhase

// 調(diào)用新增的組件(已啟用)的 start 函數(shù)
this._compScheduler.startPhase();
復(fù)制代碼

組件的 start 回調(diào)函數(shù)會在組件第一次激活前,也就是第一次執(zhí)行 update 之前觸發(fā)戈轿。

在組件的一生中 start 回調(diào)只會被觸發(fā)一次凌受,onLoadonEnable 也一樣。

只不過 onLoadonEnable 是由 NodeActivator 類的實(shí)例來管理的:

  • onLoad 會在節(jié)點(diǎn)激活時就觸發(fā)
  • onEnable 會在組件被啟用時觸發(fā)

start 則會等到下一次主循環(huán) mainLoop() 時才觸發(fā)思杯。

?? NodeActivator

NodeActivator 類主要用于啟用和禁用節(jié)點(diǎn)以及身上的組件胜蛉。

cc.director 對象中就擁有一個實(shí)例 _nodeActivator,游戲中所有節(jié)點(diǎn)的啟用和禁用都需要通過它來操作色乾。

像這樣:cc.director._nodeActivator.activateNode(this, value);

node-activator.js:github.com/cocos-creat…

updatePhase

// 調(diào)用所有組件(已啟用)的 update 函數(shù)
this._compScheduler.updatePhase(deltaTime);
復(fù)制代碼

組件的 update 函數(shù)在每一幀都會被觸發(fā)一次誊册。

lateUpdatePhase

// 調(diào)用所有組件(已啟用)的 lateUpdate 函數(shù)
this._compScheduler.lateUpdatePhase(deltaTime);
復(fù)制代碼

組件的 lateUpdate 函數(shù)會在 update 和調(diào)度器 cc.Scheduler 更新之后被觸發(fā)。調(diào)度器的更新內(nèi)容包括緩動暖璧、動畫和物理等案怯,這一點(diǎn)下面會展開。

ParticleSystem

BTW澎办,粒子系統(tǒng)組件(cc.ParticleSystem)就是在 lateUpdate 回調(diào)函數(shù)中進(jìn)行更新的嘲碱。

CCParticleSystem.js:github.com/cocos-creat…

Tips

請謹(jǐn)慎使用 updatelateUpdate 回調(diào),因?yàn)樗鼈兠恳粠紩挥|發(fā)浮驳,如果 updatelateUpdate 內(nèi)的邏輯過多悍汛,就會使得每一幀的執(zhí)行時間(即幀時間 Frame time)都變長,導(dǎo)致游戲運(yùn)行幀數(shù)降低或出現(xiàn)不穩(wěn)定的情況至会。

?? 注意這不是不讓你用离咐,該用還得用,只是不要濫用奉件,不要啥玩意都往里邊賽~

Scheduler

cc.director 對象的 _scheduler 屬性為 cc.Scheduler 類的實(shí)例宵蛀。

cc.Scheduler 是負(fù)責(zé)觸發(fā)回調(diào)函數(shù)的類。

Scheduler.js:github.com/cocos-creat…

?? 你絕對猜不到下面這一行看起來如此平平無奇的代碼執(zhí)行之后會發(fā)生什么县貌。

// 更新調(diào)度器(cc.Scheduler 類實(shí)例)
this._scheduler.update(this._deltaTime);
復(fù)制代碼

cc.director.mainLoop() 中使用 _scheduler.update() 函數(shù)來分發(fā) update术陶,在調(diào)度器(cc.director._scheduler)內(nèi)部會根據(jù)優(yōu)先級先后觸發(fā)各個系統(tǒng)模塊和組件計時器的更新。

系統(tǒng)模塊

調(diào)度器的更新會先觸發(fā)以下系統(tǒng)模塊的更新:

  • ActionManager
  • AnimationManager
  • CollisionManager
  • PhysicsManager
  • Physics3DManager
  • InputManager

以上這些模塊都以 cc.director._scheduler.scheduleUpdate() 的方式注冊到調(diào)度器上煤痕,因?yàn)檫@些模塊每一幀都需要進(jìn)行更新梧宫。

除了 InputManager 以外的模塊的優(yōu)先級都為 cc.Scheduler.PRIORITY_SYSTEM接谨,也就是系統(tǒng)優(yōu)先級,會優(yōu)先被觸發(fā)塘匣。

ActionManager

ActionManager動作管理器脓豪,用于管理游戲中的所有動作,也就是緩動系統(tǒng) ActionTween(其實(shí)它們本質(zhì)上是同一種東西)忌卤。

CCActionManager.js:github.com/cocos-creat…

AnimationManager

AnimationManager動畫管理器扫夜,用于管理游戲中的所有動畫,驅(qū)動節(jié)點(diǎn)上的 Animation 組件播放動畫驰徊。

animation-manager.js:github.com/cocos-creat…

CollisionManager

CollisionManager碰撞組件管理器笤闯,用于處理節(jié)點(diǎn)之間的碰撞組件是否產(chǎn)生了碰撞,并調(diào)用相應(yīng)回調(diào)函數(shù)棍厂。

CCCollisionManager.js:github.com/cocos-creat…

PhysicsManager

PhysicsManager物理系統(tǒng)管理器颗味,內(nèi)部以 Box2D 作為 2D 物理引擎,加以封裝并開放部分常用的接口勋桶。同時 PhysicsManager 還負(fù)責(zé)管理碰撞信息的分發(fā)脱衙。

CCPhysicsManager.js:github.com/cocos-creat…

Physics3DManager

Physics3DManager3D 物理系統(tǒng)管理器,Cocos Creator 中的 3D 物理引擎有 Cannon.jsBuiltin 可選例驹,Physics3DManager 給它們封裝了統(tǒng)一的常用接口捐韩。

physics-manager.ts:github.com/cocos-creat…

InputManager

InputManager輸入事件管理器,用于管理所有輸入事件鹃锈。開發(fā)者主動啟用加速度計(Accelerometer)之后荤胁,引擎會定時通過 InputManager 發(fā)送 cc.SystemEvent.EventType.DEVICEMOTION 事件(默認(rèn)間隔為 0.2 秒)。

CCInputManager.js:github.com/cocos-creat…

組件計時器

相信大多數(shù)小伙伴都使用過組件的 schedule()scheduleOnce() 接口屎债,主要用來重復(fù)執(zhí)行或定時執(zhí)行函數(shù)仅政。

實(shí)際上,cc.Componentschedule() 接口依賴的也是 cc.Scheduler 類盆驹,具體使用的也是 cc.director 對象中的 _scheduler 實(shí)例圆丹。

組件的 schedule() 接口在 cc.director._scheduler.schedule() 接口之外加了一層封裝,以組件自身作為 target躯喇,這樣一來組件內(nèi)的定時任務(wù)就與組件生命周期綁定辫封,當(dāng)組件被銷毀時定時任務(wù)也會被移除。

scheduleOnce() 接口則是在組件的 schedule() 接口之外又加了一層封裝廉丽,固定只會在指定時間后執(zhí)行一次倦微。

CCComponent.js:github.com/cocos-creat…

[文檔] 使用計時器:docs.cocos.com/creator/man…

另外我還注意到,有不少小伙伴還不是很清楚組件的計時器和 setTimeout()正压、setInterval() 之間的區(qū)別和用法欣福,那就趁這個機(jī)會簡單講一下吧~

setTimeout & setInterval

setTimeout()setInterval() 都是由瀏覽器或 Node.js 這類 runtime 所提供的接口。

  • setTimeout() 接口用于設(shè)置一個定時器焦履,該定時器在定時器到期后執(zhí)行一個函數(shù)或指定的一段代碼拓劝。
  • setInterval() 接口用于重復(fù)調(diào)用一個函數(shù)或執(zhí)行一個代碼段雏逾,在每次調(diào)用之間具有固定的時間延遲。

?? 再補(bǔ)充一個小知識:

在瀏覽器中 setTimeout()setInterval() 的最小延時(間隔)是 4ms郑临。

如果是未激活(后臺)的標(biāo)簽頁(tab)校套,最小延時(間隔)則加長到 1000ms。

?? 舉個栗子

假如我在當(dāng)前標(biāo)簽頁設(shè)置了一個每 500ms 輸出一個 log 的定時器牧抵,當(dāng)我切換到別的標(biāo)簽頁之后,那么這個定時器就會變成每 1000ms 才輸出一個 log侨把。

像這樣犀变,感興趣的話可以自己去試試:

setInterval(() => {
    console.log(new Date().getTime());
}, 500);
// 模擬輸出
// 標(biāo)簽頁在前臺
// 1604373521000
// 1604373521500
// 1604373522000
// 切換到別的標(biāo)簽頁后
// 1604373523000
// 1604373524000
// 1604373525000
復(fù)制代碼
區(qū)別 & 用法

組件的計時器依賴于引擎的 mainLoop() 和組件自身,如果引擎被暫停秋柄,那么組件的計時器也會被暫停获枝,如果組件或組件所在的節(jié)點(diǎn)被銷毀了,那么計時器也會失效骇笔。

setTimeout()setInterval() 都依賴于當(dāng)前所處的 window 對象省店,也就是說只要當(dāng)前瀏覽器標(biāo)簽頁不關(guān)閉,setTimeout()setInterval() 都還是會執(zhí)行的笨触。

當(dāng)你需要在組件內(nèi)部定時或重復(fù)執(zhí)行某一函數(shù)或操作某個節(jié)點(diǎn)懦傍,那么可以使用組件的計時器。

?? 讓我們想象一個場景:

在當(dāng)前場景中的某個腳本內(nèi)使用 setInterval() 來重復(fù)移動場景中的某個節(jié)點(diǎn)芦劣,當(dāng)我們切換場景后會發(fā)生什么粗俱?

當(dāng)定時器再次調(diào)用回調(diào)嘗試移動節(jié)點(diǎn)的時候,會無法找到目標(biāo)節(jié)點(diǎn)而報錯虚吟,因?yàn)楣?jié)點(diǎn)已經(jīng)跟著之前的場景一起被銷毀了寸认,而定時器還在繼續(xù)執(zhí)行。

這種情況下使用組件的計時器就不會有這種問題串慰,因?yàn)橛嫊r器會隨著組件的銷毀而被清除偏塞。

而當(dāng)我們需要執(zhí)行一些與游戲場景沒有關(guān)聯(lián)的事情的時候,就可以考慮使用 setTimeout()setInterval()邦鲫。

?? 當(dāng)然能用組件計時器的話最好還是用組件計時器啦~

小結(jié)

依然還是畫一張圖來小小總結(jié)一下 Scheduler灸叼。

總結(jié)

?? 關(guān)于引擎的啟動流程和主循環(huán)就解讀到這里啦。

如果有遺漏或錯誤的地方掂碱,也歡迎大家提出來怜姿,畢竟熬夜寫文章精神恍惚漏了也是情有可原的對吧哈哈哈~

最后的最后,還是畫張圖來做一個最后的總結(jié)~(?? 逐漸愛上畫圖~)


傳送門

微信推文版本

個人博客:菜鳥小棧

開源主頁:陳皮皮

Eazax-CCC 游戲開發(fā)腳手架

Eazax-CCC 示例在線預(yù)覽


更多分享

《為什么選擇使用 TypeScript 疼燥?》

《高斯模糊 Shader》

《一文看懂 YAML》

《Cocos Creator 性能優(yōu)化:DrawCall》

《互聯(lián)網(wǎng)運(yùn)營術(shù)語掃盲》

《在 Cocos Creator 里畫個炫酷的雷達(dá)圖》

《用 Shader 寫個完美的波浪》

《在 Cocos Creator 中優(yōu)雅且高效地管理彈窗》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末沧卢,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子醉者,更是在濱河造成了極大的恐慌但狭,老刑警劉巖披诗,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異立磁,居然都是意外死亡呈队,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進(jìn)店門唱歧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宪摧,“玉大人,你說我怎么就攤上這事颅崩〖赣冢” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵沿后,是天一觀的道長沿彭。 經(jīng)常有香客問我,道長尖滚,這世上最難降的妖魔是什么喉刘? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮漆弄,結(jié)果婚禮上睦裳,老公的妹妹穿的比我還像新娘。我一直安慰自己撼唾,他們只是感情好推沸,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著券坞,像睡著了一般鬓催。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上恨锚,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天宇驾,我揣著相機(jī)與錄音,去河邊找鬼猴伶。 笑死课舍,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的他挎。 我是一名探鬼主播筝尾,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼办桨!你這毒婦竟也來了筹淫?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤呢撞,失蹤者是張志新(化名)和其女友劉穎损姜,沒想到半個月后饰剥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡摧阅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年汰蓉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片棒卷。...
    茶點(diǎn)故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡顾孽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出比规,到底是詐尸還是另有隱情岩齿,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布苞俘,位于F島的核電站,受9級特大地震影響龄章,放射性物質(zhì)發(fā)生泄漏吃谣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一做裙、第九天 我趴在偏房一處隱蔽的房頂上張望岗憋。 院中可真熱鬧,春花似錦锚贱、人聲如沸仔戈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽监徘。三九已至,卻和暖如春吧碾,著一層夾襖步出監(jiān)牢的瞬間凰盔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工倦春, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留户敬,地道東北人。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓睁本,卻偏偏與公主長得像尿庐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子呢堰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容