Res.ts 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import { Asset, error, instantiate, Node, Prefab, resources, sp, SpriteAtlas, SpriteFrame } from 'cc';
  2. /** 资源缓存基础数据结构 */
  3. interface CacheData {
  4. asset: Asset,
  5. /** 资源是否需要释放 */
  6. release: boolean,
  7. /** 资源最后一次被加载的时间点(秒) */
  8. lastLoadTime: number,
  9. }
  10. /** 预制体资源缓存数据 */
  11. interface PrefabCacheData extends CacheData {
  12. /** 此prefab关联的实例节点 */
  13. nodes?: Node[],
  14. }
  15. /**
  16. * 资源管理类
  17. *
  18. * 资源释放要点:
  19. * 1. 尽量使用此类的接口加载所有资源、instantiate节点实例,否则需要自行管理引用计数
  20. * 2. Res.instantiate不要对动态生成的节点使用,尽量只instantiate prefab上预设好的节点,否则有可能会导致引用计数的管理出错
  21. * 3. 调用load接口时如需传入release参数,则同一资源在全局调用load时release参数尽量保持一致,否则可能不符合预期
  22. * 4. 请使用ResSpine、ResSprite组件去动态加载spine、图片资源,否则需要自行管理这些资源的引用计数
  23. */
  24. export default class Res {
  25. /** 节点与关联prefab路径 */
  26. private static _nodePath: Map<Node, string> = new Map();
  27. private static _prefabCache: Map<string, PrefabCacheData> = new Map();
  28. private static _spriteFrameCache: Map<string, CacheData> = new Map();
  29. private static _spriteAtlasCache: Map<string, CacheData> = new Map();
  30. private static _skeletonDataCache: Map<string, CacheData> = new Map();
  31. private static _otherCache: Map<string, Asset> = new Map();
  32. /** 资源释放的间隔时间(秒),资源超过此间隔未被load才可释放 */
  33. public static releaseSec: number = 0;
  34. /**
  35. * 通过节点查找对应的缓存prefab url
  36. * @param node
  37. */
  38. private static getCachePrefabUrl(node: Node | Prefab): string {
  39. let url = "";
  40. if (node instanceof Prefab) {
  41. url = node['__resCacheUrl'] || "";
  42. } else if (node instanceof Node) {
  43. let cur = node;
  44. while (cur) {
  45. if (cur['_prefab'] && cur['_prefab']['root']) {
  46. url = this._nodePath.get(cur['_prefab']['root']) || "";
  47. if (url) {
  48. break;
  49. }
  50. }
  51. cur = cur.parent;
  52. }
  53. }
  54. return url;
  55. }
  56. /**
  57. * 缓存资源
  58. * @param url 资源路径
  59. * @param asset 资源
  60. * @param release 资源是否需要释放
  61. */
  62. private static cacheAsset(url: string, asset: Asset, release: boolean = true): void {
  63. const func = (map: Map<string, CacheData>) => {
  64. if (map.has(url)) {
  65. return;
  66. }
  67. asset.addRef();
  68. if (asset instanceof Prefab) {
  69. asset['__resCacheUrl'] = url;
  70. }
  71. const cacheData: CacheData = {
  72. asset: asset,
  73. release: release,
  74. lastLoadTime: Date.now() / 1000
  75. };
  76. map.set(url, cacheData);
  77. };
  78. if (asset instanceof Prefab) {
  79. func(this._prefabCache);
  80. } else if (asset instanceof SpriteFrame) {
  81. func(this._spriteFrameCache);
  82. } else if (asset instanceof SpriteAtlas) {
  83. func(this._spriteAtlasCache);
  84. } else if (asset instanceof sp.SkeletonData) {
  85. func(this._skeletonDataCache);
  86. } else {
  87. if (this._otherCache.has(url)) {
  88. return;
  89. }
  90. asset.addRef();
  91. this._otherCache.set(url, asset);
  92. }
  93. }
  94. /**
  95. * 获取缓存资源。通常不应直接调用此接口,除非调用前能确保资源已加载并且能自行管理引用计数
  96. * @param url 资源路径
  97. * @param type 资源类型
  98. */
  99. public static get<T extends Asset>(url: string, type: typeof Asset): T {
  100. let asset: unknown = null;
  101. const func = (map: Map<string, CacheData>) => {
  102. const data = map.get(url);
  103. if (data) {
  104. asset = data.asset;
  105. data.lastLoadTime = Date.now() / 1000;
  106. }
  107. };
  108. if (type === Prefab) {
  109. func(this._prefabCache);
  110. } else if (type === SpriteFrame) {
  111. func(this._spriteFrameCache);
  112. } else if (type === SpriteAtlas) {
  113. func(this._spriteAtlasCache);
  114. } else if (type === sp.SkeletonData) {
  115. func(this._skeletonDataCache);
  116. } else {
  117. asset = this._otherCache.get(url);
  118. }
  119. return asset as T;
  120. }
  121. /**
  122. * 加载resources文件夹下单个资源
  123. * @param url 资源路径
  124. * @param type 资源类型
  125. * @param release 资源是否需要释放
  126. */
  127. public static async load<T extends Asset>(url: string, type: typeof Asset, release: boolean = true): Promise<T> {
  128. let asset: T = this.get(url, type);
  129. if (asset) {
  130. return asset;
  131. }
  132. asset = await new Promise((resolve, reject) => {
  133. resources.load(url, type, (error: Error, resource: T) => {
  134. if (error) {
  135. console.error(`[Res.load] error: ${error}`);
  136. resolve(null);
  137. } else {
  138. resolve(resource);
  139. }
  140. });
  141. });
  142. this.cacheAsset(url, asset, release);
  143. return asset;
  144. }
  145. /**
  146. * 加载resources文件夹下某个文件夹内某类资源
  147. * @param url 资源路径
  148. * @param type 资源类型
  149. * @param release 资源是否需要释放
  150. */
  151. public static loadDir<T extends Asset>(url: string, type: typeof Asset, release: boolean = true): Promise<T[]> {
  152. return new Promise((resolve, reject) => {
  153. resources.loadDir(url, type, (error: Error, resource: T[]) => {
  154. if (error) {
  155. console.error(`[Res.loadDir] error: ${error}`);
  156. resolve([]);
  157. } else {
  158. const infos = resources.getDirWithPath(url, type);
  159. resource.forEach((asset, i) => { this.cacheAsset(infos[i].path, asset, release); });
  160. resolve(resource);
  161. }
  162. });
  163. });
  164. }
  165. /**
  166. * 获取节点实例,建立节点与缓存prefab的联系
  167. * @param original 用于创建节点的prefab或node
  168. * @param related 如果original不是动态加载的prefab,则需传入与original相关联的动态加载的prefab或node,便于资源释放的管理
  169. * @example
  170. * // A为动态加载的prefab,aNode为A的实例节点(aNode = Res.instantiate(A)),original为被A静态引用的prefab,则调用时需要用如下方式,保证引用关系正确
  171. * Res.instantiate(original, A)
  172. * Res.instantiate(original, aNode)
  173. *
  174. * // A为动态加载的prefab,aNode为A的实例节点(aNode = Res.instantiate(A)),original为aNode的某个子节点,则如下方式均可保证引用关系正确
  175. * Res.instantiate(original)
  176. * Res.instantiate(original, A)
  177. * Res.instantiate(original, aNode)
  178. */
  179. public static instantiate(original: Node | Prefab, related?: Node | Prefab): Node {
  180. if (!original) {
  181. error("[Res.instantiate] original is null");
  182. return null;
  183. }
  184. const node = instantiate(original) as Node;
  185. let cacheData: PrefabCacheData = null;
  186. const url = this.getCachePrefabUrl(related) || this.getCachePrefabUrl(original);
  187. if (url) {
  188. cacheData = this._prefabCache.get(url);
  189. // release为true才缓存关联节点
  190. if (cacheData && cacheData.release) {
  191. if (!Array.isArray(cacheData.nodes)) {
  192. cacheData.nodes = [];
  193. }
  194. cacheData.nodes.push(node);
  195. this._nodePath.set(node, url);
  196. }
  197. }
  198. return node;
  199. }
  200. /**
  201. * 尝试释放所有缓存资源
  202. */
  203. public static releaseAll(): void {
  204. const nowSec = Date.now() / 1000;
  205. // prefab
  206. this._prefabCache.forEach((cacheData, url) => {
  207. if (!cacheData.release || nowSec - cacheData.lastLoadTime < this.releaseSec) {
  208. return;
  209. }
  210. if (Array.isArray(cacheData.nodes)) {
  211. for (let i = cacheData.nodes.length - 1; i >= 0; i--) {
  212. const node = cacheData.nodes[i];
  213. if (node.isValid) {
  214. continue;
  215. }
  216. this._nodePath.delete(node);
  217. cacheData.nodes.splice(i, 1);
  218. }
  219. if (cacheData.nodes.length === 0) {
  220. delete cacheData.nodes;
  221. }
  222. }
  223. if (!Array.isArray(cacheData.nodes)) {
  224. cacheData.asset.decRef();
  225. this._prefabCache.delete(url);
  226. }
  227. });
  228. // spriteFrame、spriteAtlas、skeletonData
  229. const arr = [this._spriteFrameCache, this._spriteAtlasCache, this._skeletonDataCache];
  230. arr.forEach((map) => {
  231. map.forEach((cacheData, url) => {
  232. if (!cacheData.release || nowSec - cacheData.lastLoadTime < this.releaseSec) {
  233. return;
  234. }
  235. cacheData.asset.decRef();
  236. map.delete(url);
  237. });
  238. });
  239. }
  240. }