最近公司從cocos2dx轉cocoscreator。第一次使用,遇到很多坑。工欲善其事,必先利其器。學習官方文檔是必須的。重要的地點標紅,還有自己出錯的地點就加了備註。以官方文檔爲基礎。
Cocos Creator 有一套統一的資源管理機制,在本篇教程,我們將介紹
- 資源的分類
- 如何在 屬性檢查器 裏設置資源
- 動態加載資源
- 加載遠程資源和設備資源
- 資源的依賴和釋放
資源的分類
目前的資源分成兩種,一種叫做 Asset,一種叫做 Raw Asset。
Asset
Creator 提供了名爲 "Asset" 的資源類型,cc.SpriteFrame
, cc.AnimationClip
, cc.Prefab
等資源都屬於 Asset。Asset 的加載是統一併且自動化的,相互依賴的 Asset 能夠被自動預加載。
例如,當引擎在加載場景時,會先自動加載場景關聯到的資源,這些資源如果再關聯其它資源,其它也會被先被加載,等加載全部完成後,場景加載纔會結束。
腳本中可以這樣定義一個 Asset 屬性:
// NewScript.js
cc.Class({
extends: cc.Component,
properties: {
spriteFrame: {
default: null,
type: cc.SpriteFrame
},
}
});
Raw Asset
Cocos2d 的一些舊 API 並沒有使用上面提到的 Asset 對象,而是直接用 URL 字符串指代資源。爲了兼容這些 API,我們把這類資源叫做 "Raw Asset"。圖片(cc.Texture2D
),聲音(cc.AudioClip
),粒子(cc.ParticleAsset
)等資源都是 Raw Asset。Raw Asset 在腳本里由一個 url
字符串來表示,當你要在引擎中使用 Raw Asset,只要把 url 傳給引擎的 API,引擎內部會自動加載這個 url 對應的資源。
當你在腳本里聲明一個類型是 cc.Texture2D
的 Raw Asset,一開始可能會想這樣定義:
cc.Class({
extends: cc.Component,
properties: {
textureURL: {
default: null,
type: cc.Texture2D
}
}
});
這樣寫的問題在於,在代碼中 textureURL
實際上是一個字符串,而不是 cc.Texture2D
的實例。爲了不混淆
type 的語義,在 CCClass 中聲明 Raw Asset 的屬性時,要用 url: cc.Texture2D
而不是 type:
cc.Texture2D
。cc.Class({
extends: cc.Component,
properties: {
textureURL: {
default: "",
url: cc.Texture2D
}
}
});
如何在屬性檢查器裏設置資源
不論是 Asset 還是 Raw Asset,只要在腳本中定義好類型,就能直接在 屬性檢查器 很方便地設置資源。假設我們有這樣一個組件:
// NewScript.js
cc.Class({
extends: cc.Component,
properties: {
textureURL: {
default: "",
url: cc.Texture2D
},
spriteFrame: {
default: null,
type: cc.SpriteFrame
},
}
});
將它添加到場景後,屬性檢查器 裏是這樣的:
接下來我們從 資源管理器 裏面分別將一張 Texture 和一個 SpriteFrame 拖到 屬性檢查器 的對應屬性中:
結果如下:
這樣就能在腳本里直接拿到設置好的資源:
onLoad: function () {
var spriteFrame = this.spriteFrame;
var textureURL = this.textureURL;
spriteFrame.setTexture(textureURL);
}
在 屬性檢查器 裏設置資源雖然很直觀,但資源只能在場景裏預先設好,沒辦法動態切換。如果需要動態切換,你需要看看下面的內容。動態加載
動態加載資源要注意兩點,一是所有需要通過腳本動態加載的資源,都必須放置在 resources
文件夾或它的子文件夾下。resources
需要在
assets 文件夾中手工創建,並且必須位於 assets 的根目錄,就像這樣:
注意:拷貝項目內的資源,記住不能拷貝.meta資源。這會導致cocoscreator資源管理器添加不了資源。
這裏的 image/image
, prefab
, anim
, font
都是常見的 Asset,而 image
, audio
則是常見的 Raw Asset。
resources
文件夾裏面的資源,可以關聯依賴到文件夾外部的其它資源,同樣也可以被外部場景或資源引用到。項目構建時,除了已在 構建發佈 面板勾選的場景外,resources
文件夾裏面的所有資源,連同它們關聯依賴的resources
文件夾外部的資源,都會被導出。如果一份資源不需要由腳本直接動態加載,那麼千萬不要放在resources
文件夾裏。
第二個要注意的是 Creator 相比之前的 Cocos2d-html5,資源動態加載的時都是異步的,需要在回調函數中獲得載入的資源。這麼做是因爲 Creator 除了場景關聯的資源,沒有另外的資源預加載列表,動態加載的資源是真正的動態加載。
動態加載 Asset
Creator 提供了 cc.loader.loadRes
這個 API 來專門加載那些位於 resources 目錄下的 Asset。和 cc.loader.load
不同的是,loadRes 一次只能加載單個 Asset。調用時,你只要傳入相對 resources 的路徑即可,並且路徑的結尾處不能包含文件擴展名。
// 加載 Prefab
cc.loader.loadRes("test assets/prefab", function (err, prefab) {
var newNode = cc.instantiate(prefab);
cc.director.getScene().addChild(newNode);
});
// 加載 AnimationClip
var self = this;
cc.loader.loadRes("test assets/anim", function (err, clip) {
self.node.getComponent(cc.Animation).addClip(clip, "anim");
});
// 加載 SpriteAtlas(圖集),並且獲取其中的一個 SpriteFrame
// 注意 atlas 資源文件(plist)通常會和一個同名的圖片文件(png)放在一個目錄下, 所以需要在第二個參數指定資源類型
cc.loader.loadRes("test assets/sheep", cc.SpriteAtlas, function (err, atlas) {
var frame = atlas.getSpriteFrame('sheep_down_0');
sprite.spriteFrame = frame;
});
加載獨立的 SpriteFrame
圖片設置爲 Sprite 後,將會在 資源管理器 中生成一個對應的 SpriteFrame。但如果直接加載 test assets/image
,得到的類型將會是 cc.Texture2D。你必須指定第二個參數爲資源的類型,才能加載到圖片生成的 cc.SpriteFrame:
// 加載 SpriteFrame
var self = this;
cc.loader.loadRes("test assets/image", cc.SpriteFrame, function (err, spriteFrame) {
self.node.getComponent(cc.Sprite).spriteFrame = spriteFrame;
});
如果指定了類型參數,就會在路徑下查找指定類型的資源。當你在同一個路徑下同時包含了多個重名資源(例如同時包含 player.clip 和 player.psd),或者需要獲取“子資源”(例如獲取 Texture2D 生成的 SpriteFrame),就需要聲明類型。
資源釋放
loadRes
加載進來的單個資源如果需要釋放,可以調用 cc.loader.releaseRes
,releaseRes
可以傳入和 loadRes
相同的路徑和類型參數。
cc.loader.releaseRes("test assets/image", cc.SpriteFrame);
cc.loader.releaseRes("test assets/anim");
此外,你也可以使用 cc.loader.releaseAsset
來釋放特定的 Asset 實例。
cc.loader.releaseAsset(spriteFrame);
動態加載 Raw Asset
Raw Asset 可以直接使用 url 從遠程服務器上加載,也可以從項目中動態加載。對遠程加載而言,原先 Cocos2d 的加載方式不變,使用 cc.loader.load 即可。對項目裏的 Raw Asset,加載方式和 Asset 一樣:
// 加載 Texture,不需要後綴名
cc.loader.loadRes("test assets/image", function (err, texture) {
...
});
cc.url.raw
Raw Asset 加載成功後,如果需要傳給一些 url 形式的 API,還是需要給出完整路徑才行。你需要用 cc.url.raw
進行一次 url 的轉換:
// 原 url 會報錯!文件找不到
var texture = cc.textureCache.addImage("resources/test assets/image.png");
// 改用 cc.url.raw,此時需要聲明 resources 目錄和文件後綴名
var realUrl = cc.url.raw("resources/test assets/image.png");
var texture = cc.textureCache.addImage(realUrl);
資源批量加載
cc.loader.loadResDir
可以加載相同路徑下的多個資源:
// 加載 test assets 目錄下所有資源
cc.loader.loadResDir("test assets", function (err, assets) {
// ...
});
// 加載 sheep.plist 圖集中的所有 SpriteFrame
cc.loader.loadResDir("test assets/sheep", cc.SpriteFrame, function (err, assets) {
// assets 是一個 SpriteFrame 數組,已經包含了圖集中的所有 SpriteFrame。
// 而 loadRes('test assets/sheep', cc.SpriteAtlas, function (err, atlas) {...}) 獲得的則是整個 SpriteAtlas 對象。
});
加載遠程資源和設備資源
在目前的 Cocos Creator 中,我們支持加載遠程貼圖資源,這對於加載用戶頭像等需要向服務器請求的貼圖很友好,需要注意的是,這需要開發者直接調用 cc.loader.load
。同時,如果用戶用其他方式下載了資源到本地設備存儲中,也需要用同樣的 API 來加載,上文中的 loadRes
等 API 只適用於應用包內的資源和熱更新的本地資源。下面是這個 API 的用法:
// 遠程 url 帶圖片後綴名
var remoteUrl = "http://unknown.org/someres.png";
cc.loader.load(remoteUrl, function (err, texture) {
// Use texture to create sprite frame
});
// 遠程 url 不帶圖片後綴名,此時必須指定遠程圖片文件的類型
remoteUrl = "http://unknown.org/emoji?id=124982374";
cc.loader.load({url: remoteUrl, type: 'png'}, function () {
// Use texture to create sprite frame
});
// 用絕對路徑加載設備存儲內的資源,比如相冊
var absolutePath = "/dara/data/some/path/to/image.png"
cc.loader.load(absolutePath, function () {
// Use texture to create sprite frame
});
目前的此類手動資源加載還有一些限制,對用戶影響比較大的是:
- 遠程加載不支持圖片文件以外類型的資源(已在 1.5/1.6 支持計劃中)
- 這種加載方式只支持 raw asset 資源類型,不支持 SpriteFrame、SpriteAtlas、Tilemap 等資源的直接加載和解析(需要後續版本中的 Assets Bundle 支持)
- Web 端的遠程加載受到瀏覽器的 CORS 跨域策略限制,如果對方服務器禁止跨域訪問,那麼會加載失敗,而且在 WebGL 渲染模式下,即便對方服務器允許 http 請求成功之後也無法渲染,這是 WebGL 的安全策略的限制
資源的依賴和釋放
在加載完資源之後,所有的資源都會臨時被緩存到 cc.loader
中,以避免重複加載資源時發送無意義的 http 請求,當然,緩存的內容都會佔用內存,有些資源可能用戶不再需要了,想要釋放它們,這裏介紹一下在做資源釋放時需要注意的事項。
首先最爲重要的一點就是:資源之間是互相依賴的。
比如下圖,Prefab 資源中的 Node 包含 Sprite 組件,Sprite 組件依賴於 SpriteFrame,SpriteFrame 資源依賴於 Texture 資源,而 Prefab,SpriteFrame 和 Texture 資源都被 cc.loader 緩存起來了。這樣做的好處是,有可能有另一個 SpriteAtlas 資源依賴於同樣的一個 SpriteFrame 和 Texture,那麼當你手動加載這個 SpriteAtlas 的時候,就不需要再重新請求貼圖資源了,cc.loader 會自動使用緩存中的資源。
在搞明白資源的相互引用之後,資源釋放的問題也就呼之欲出了,當你選擇釋放一個 Prefab 時,我們是不會自動釋放它依賴的其他資源的,因爲有可能這些依賴資源還有其他的用處。所以用戶在釋放資源時經常會問我們,爲什麼我都把資源釋放了,內存佔用還是居高不下?原因就是真正佔用內存的貼圖等基礎資源並不會隨着你釋放 Prefab 或者 SpriteAtlas 而被釋放。
接下來要介紹問題的另一個核心:JavaScript 中無法跟蹤對象引用。
在 JavaScript 這種腳本語言中,由於其弱類型特性,以及爲了代碼的便利,往往是不包含內存管理功能的,所有對象的內存都由垃圾回收機制來管理。這就導致 JS 層邏輯永遠不知道一個對象會在什麼時候被釋放,這意味着引擎無法通過類似引用計數的機制來管理外部對象對資源的引用,也無法嚴謹得統計資源是否不再被需要了。基於以上的原因,目前 cc.loader 的設計實際上是依賴於用戶根據遊戲邏輯管理資源,用戶可以決定在某一時刻不再需要某些資源以及它依賴的資源,立即將它們在 cc.loader 中的緩存釋放。也可以選擇在釋放依賴資源的時候,防止部分共享資源被釋放。下面是一個簡單的示例:
// 直接釋放某個貼圖
cc.loader.release(texture);
// 釋放一個 prefab 以及所有它依賴的資源
var deps = cc.loader.getDependsRecursively('prefabs/sample');
cc.loader.release(deps);
// 如果在這個 prefab 中有一些和場景其他部分共享的資源,你不希望它們被釋放,有兩種方法:
// 1. 顯式聲明禁止某個資源的自動釋放
cc.loader.setAutoRelease(texture2d, false);
// 2. 將這個資源從依賴列表中刪除
var deps = cc.loader.getDependsRecursively('prefabs/sample');
var index = deps.indexOf(texture2d._uuid);
if (index !== -1)
deps.splice(index, 1);
cc.loader.release(deps);
最後一個值得關注的要點:JavaScript 的垃圾回收是延遲的。
想象一種情況,當你釋放了 cc.loader 對某個資源的引用之後,由於考慮不周的原因,遊戲邏輯再次請求了這個資源。此時垃圾回收還沒有開始(垃圾回收的時機不可控),或者你的遊戲邏輯某處,仍然持有一個對於這個舊資源的引用,那麼意味着這個資源還存在內存中,但是 cc.loader 已經訪問不到了,所以會重新加載它。這造成這個資源在內存中有兩份同樣的拷貝,浪費了內存。如果只是一個資源還好,但是如果類似的資源很多,甚至不止一次被重複加載,這對於內存的壓力是有可能很高的。如果觀察到遊戲使用的內存曲線有這樣的異常,請仔細檢查遊戲邏輯,是否存在泄漏,如果沒有的話,垃圾回收機制是會正常回收這些內存的。
以上就是管理資源依賴和釋放時需要注意的細節,這部分的功能和 API 設計還沒有完全定案,我們還是希望盡力給大家帶來儘可能方便的引擎 API,所以後續也會嘗試一些其他的辦法提升友好度,屆時會更新這篇文檔。