44 KiB
基于Oops Plugin Framework的资源管理系统
**本文档引用的文件** - [loader.md](file://doc/core/common/loader.md) - [ResLoader.ts](file://extensions/oops-plugin-framework/assets/core/common/loader/ResLoader.ts) - [Initialize.ts](file://assets/script/game/initialize/Initialize.ts) - [LoadingViewComp.ts](file://assets/script/game/initialize/view/LoadingViewComp.ts) - [GameMap.ts](file://assets/script/game/map/GameMap.ts) - [HeroSpine.ts](file://assets/script/game/hero/HeroSpine.ts) - [HeroAnmComp.ts](file://assets/script/game/hero/HeroAnmComp.ts) - [config.json](file://assets/resources/config.json) - [builder.json](file://settings/v2/packages/builder.json) - [engine.json](file://settings/v2/packages/engine.json)目录
简介
Oops Plugin Framework的资源管理系统是一个功能强大的资源加载与管理框架,专为Cocos Creator游戏开发而设计。该系统提供了完整的资源生命周期管理,包括加载、缓存、释放和监控等功能,支持Spine骨骼动画、SpriteFrame、配置文件等多种资源类型的高效管理。
核心特性
- 多格式支持:支持Spine骨骼动画、SpriteFrame、音频、纹理等多种资源类型
- 智能缓存:自动管理资源缓存,避免重复加载
- 异步加载:支持进度回调和错误处理
- Bundle管理:灵活的资源包划分和热更新支持
- 内存优化:自动垃圾回收和手动释放机制
- 调试工具:内置资源状态监控和调试功能
系统架构
graph TB
subgraph "资源管理层"
ResLoader[ResLoader 核心类]
AssetManager[AssetManager 引擎层]
BundleManager[Bundle管理器]
end
subgraph "资源类型"
SpineRes[Spine骨骼动画]
SpriteRes[SpriteFrame精灵图]
ConfigRes[配置文件]
AudioRes[音频资源]
end
subgraph "加载策略"
SyncLoad[同步加载]
AsyncLoad[异步加载]
BatchLoad[批量加载]
LazyLoad[延迟加载]
end
subgraph "内存管理"
RefCount[引用计数]
AutoGC[自动垃圾回收]
ManualRelease[手动释放]
end
ResLoader --> AssetManager
ResLoader --> BundleManager
AssetManager --> SpineRes
AssetManager --> SpriteRes
AssetManager --> ConfigRes
AssetManager --> AudioRes
ResLoader --> SyncLoad
ResLoader --> AsyncLoad
ResLoader --> BatchLoad
ResLoader --> LazyLoad
ResLoader --> RefCount
RefCount --> AutoGC
RefCount --> ManualRelease
图表来源
章节来源
核心API详解
load - 单资源加载
load方法是最基础的资源加载API,支持多种重载形式以适应不同的使用场景。
基本语法
// 加载默认Bundle中的资源
oops.res.load(path, type, onComplete);
// 加载指定Bundle中的资源
oops.res.load(bundleName, path, type, onComplete);
// 支持进度回调
oops.res.load(path, type, onProgress, onComplete);
参数说明
| 参数 | 类型 | 描述 |
|---|---|---|
bundleName |
string |
Bundle名称,默认为"resources" |
paths |
string | string[] |
资源路径或路径数组 |
type |
AssetType<T> |
资源类型构造函数 |
onProgress |
ProgressCallback |
加载进度回调函数 |
onComplete |
CompleteCallback<T> |
加载完成回调函数 |
使用示例
加载Spine骨骼动画:
// 加载角色动画资源
oops.res.load("hero/skeleton", sp.SkeletonData, (err, skeletonData) => {
if (err) {
console.error("角色动画加载失败:", err);
return;
}
this.skeletonData = skeletonData;
this.updateSkeleton();
});
加载SpriteFrame:
// 加载UI按钮图片
oops.res.load("ui/button_normal", SpriteFrame, (err, spriteFrame) => {
if (err) {
console.error("按钮图片加载失败:", err);
return;
}
this.button.spriteFrame = spriteFrame;
});
loadDir - 文件夹批量加载
loadDir方法用于批量加载指定目录下的所有资源,特别适合加载同一类型的资源集合。
语法结构
oops.res.loadDir(bundleName, dir, type, onProgress, onComplete);
进度回调机制
sequenceDiagram
participant Client as 客户端
participant ResLoader as ResLoader
participant AssetManager as AssetManager
participant Progress as 进度回调
Client->>ResLoader : loadDir("game", onProgress, onComplete)
ResLoader->>AssetManager : loadDir(path, type, progress, complete)
AssetManager->>AssetManager : 开始扫描目录
loop 每个资源文件
AssetManager->>Progress : onProgress(finished, total, item)
Progress->>Client : 更新UI进度条
end
AssetManager->>ResLoader : 加载完成
ResLoader->>Client : onComplete(resources[])
图表来源
实际应用案例
游戏资源加载流程:
// 加载游戏主界面资源
oops.res.loadDir("game", (finished, total, item) => {
// 更新加载进度
this.updateProgressBar(finished, total);
console.log(`正在加载: ${item.uuid} (${finished}/${total})`);
}, () => {
// 所有资源加载完成
this.onGameResourcesLoaded();
});
章节来源
loadRemote - 远程资源加载
loadRemote方法专门用于加载网络上的远程资源,支持图片、音频等文件的动态下载。
方法签名
oops.res.loadRemote<T extends Asset>(url: string, options: IRemoteOptions, onComplete: CompleteCallback<T>);
配置选项
| 选项 | 类型 | 描述 |
|---|---|---|
ext |
string |
文件扩展名,如".png"、".mp3" |
timeout |
number |
请求超时时间(毫秒) |
headers |
Record<string, string> |
自定义请求头 |
完整实现示例
// 加载远程图片资源
const options: IRemoteOptions = {
ext: ".png",
timeout: 10000
};
oops.res.loadRemote<ImageAsset>(imageUrl, options, (err, imageAsset) => {
if (err) {
console.error("远程图片加载失败:", err);
return;
}
// 创建纹理和SpriteFrame
const texture = new Texture2D();
texture.image = imageAsset;
const spriteFrame = new SpriteFrame();
spriteFrame.texture = texture;
// 应用到UI元素
this.avatarSprite.spriteFrame = spriteFrame;
});
loadBundle - 资源包加载
loadBundle方法支持从服务器动态加载资源包,实现游戏的热更新功能。
使用流程
flowchart TD
Start([开始热更新]) --> CheckVersion{检查版本}
CheckVersion --> |需要更新| LoadBundle[加载远程Bundle]
CheckVersion --> |无需更新| UseLocal[使用本地资源]
LoadBundle --> VerifyMD5{验证MD5}
VerifyMD5 --> |验证成功| ReplaceAssets[替换旧资源]
VerifyMD5 --> |验证失败| RetryDownload[重试下载]
ReplaceAssets --> UpdateCache[更新缓存]
UpdateCache --> Complete([更新完成])
RetryDownload --> MaxRetry{达到最大重试次数?}
MaxRetry --> |否| LoadBundle
MaxRetry --> |是| FallbackLocal[回退到本地]
FallbackLocal --> Complete
UseLocal --> Complete
图表来源
热更新实现
// 热更新资源包
async function hotUpdate() {
const serverUrl = "http://192.168.1.13:8082/";
const md5 = "8e5c0"; // 构建后的MD5字符
try {
// 加载远程资源包
const bundle = await oops.res.loadBundle(serverUrl, md5);
console.log("热更新包加载成功:", bundle.name);
// 更新资源引用
this.updateResourceReferences();
// 清理旧资源
oops.res.releaseDir("", "resources");
} catch (error) {
console.error("热更新失败:", error);
// 回退到本地资源
this.useLocalResources();
}
}
章节来源
release & releaseDir - 资源释放
资源释放是内存管理的关键环节,系统提供了精确的资源释放机制。
release - 单资源释放
// 释放单个资源
oops.res.release("model/player", "resources");
// 释放其他Bundle中的资源
oops.res.release("ui/button", "assets");
releaseDir - 批量资源释放
// 释放整个目录的资源
oops.res.releaseDir("game", "resources");
// 释放后清理Bundle(如果需要)
oops.res.releaseDir("temp", "temporary");
内存管理原理
classDiagram
class Asset {
+number refCount
+decRef() void
+addRef() void
+destroy() void
}
class ResLoader {
+release(path, bundle) void
+releaseDir(path, bundle) void
-releasePrefabtDepsRecursively(uuid) void
}
class AssetManager {
+assets Map~string,Asset~
+releaseAsset(asset) void
+getBundle(name) Bundle
}
class Bundle {
+get(path, type) Asset
+getDirWithPath(path) AssetInfo[]
+remove() void
}
ResLoader --> AssetManager
AssetManager --> Asset
AssetManager --> Bundle
ResLoader --> Bundle
图表来源
章节来源
资源加载场景
Spine骨骼动画加载
Spine骨骼动画是游戏中常见的动画表现形式,系统提供了专门的加载和管理机制。
加载流程
sequenceDiagram
participant Game as 游戏逻辑
participant ResLoader as 资源加载器
participant SpineManager as Spine管理器
participant Skeleton as 骨骼实例
Game->>ResLoader : load("spine/hero", SkeletonData)
ResLoader->>ResLoader : 查找缓存资源
alt 缓存命中
ResLoader-->>Game : 返回缓存资源
else 缓存未命中
ResLoader->>ResLoader : 下载并解析资源
ResLoader-->>Game : 返回新资源
end
Game->>SpineManager : 创建骨骼实例
SpineManager->>Skeleton : 设置SkeletonData
Skeleton-->>SpineManager : 骨骼实例就绪
SpineManager-->>Game : 动画控制器就绪
图表来源
实际应用代码
HeroSpine组件实现:
// 在HeroSpine组件中加载动画资源
protected initAnimator() {
// 加载Spine骨骼数据
oops.res.load("hero/skeleton", sp.SkeletonData, (err, skeletonData) => {
if (err) {
console.error("骨骼数据加载失败:", err);
return;
}
// 设置骨骼数据
this.skeleton.skeletonData = skeletonData;
// 初始化动画组件
this.setupAnimationController();
});
}
// 动画状态切换
playAnimation(name: string) {
// 检查动画是否正在播放
if (this.skeleton.animation === name && this.skeleton.isAnimationCached(name)) {
return;
}
// 播放指定动画
this.skeleton.animation = name;
this.skeleton.setAnimation(0, name, true);
}
HeroAnmComp动画控制器:
// 动画播放控制
idle() {
if (this.anmcon.getState("idle").isPlaying) return;
this.anmcon.play("idle");
this.default_anim = 'idle';
}
atk() {
if (this.anmcon.getState("atk0").isPlaying) return;
this.anmcon.play("atk0");
}
move() {
if (this.anmcon.getState("move").isPlaying) return;
this.anmcon.play("move");
this.default_anim = 'move';
}
章节来源
SpriteFrame精灵图加载
SpriteFrame是UI和游戏元素的基础资源类型,系统提供了高效的加载和管理机制。
加载策略
| 场景 | 推荐策略 | 示例 |
|---|---|---|
| UI按钮 | 同步加载 | oops.res.load("ui/button", SpriteFrame) |
| 背景图片 | 异步加载 | oops.res.loadDir("background", ...) |
| 角色皮肤 | 延迟加载 | 游戏开始后按需加载 |
| 动态图标 | 缓存复用 | 首次加载后缓存 |
实际应用
// UI按钮状态管理
class ButtonStateManager {
private buttonStates: Map<string, SpriteFrame> = new Map();
async loadButtonStates() {
const states = ['normal', 'hover', 'pressed', 'disabled'];
// 并行加载所有状态
await Promise.all(states.map(async (state) => {
const spriteFrame = await oops.res.loadAsync(
"ui/button_" + state,
SpriteFrame
);
this.buttonStates.set(state, spriteFrame);
}));
}
setButtonState(button: Button, state: string) {
const spriteFrame = this.buttonStates.get(state);
if (spriteFrame) {
button.spriteFrame = spriteFrame;
}
}
}
配置文件加载
配置文件是游戏运行时的重要数据源,系统提供了灵活的配置加载机制。
配置文件组织结构
graph TD
ConfigRoot[配置根目录] --> GameConfig[game/]
ConfigRoot --> MapConfig[map/]
ConfigRoot --> LanguageConfig[language/]
GameConfig --> NetCode[NetCode.json]
GameConfig --> GameUI[GameUIConfig.ts]
MapConfig --> MapData[map.json]
MapConfig --> DeliveryData[map_delivery.json]
LanguageConfig --> EnLang[en.json]
LanguageConfig --> ZhLang[zh.json]
图表来源
加载实现
// 配置文件加载器
class ConfigLoader {
private configs: Map<string, any> = new Map();
async loadGameConfig() {
// 加载网络代码配置
const netCode = await this.loadJsonConfig("game/NetCode");
this.configs.set("netCode", netCode);
// 加载UI配置
const uiConfig = await this.loadJsonConfig("game/GameUIConfig");
this.configs.set("ui", uiConfig);
// 加载地图配置
const mapConfigs = await Promise.all([
this.loadJsonConfig("map/map"),
this.loadJsonConfig("map/map_delivery")
]);
this.configs.set("map", mapConfigs);
}
private async loadJsonConfig(path: string): Promise<any> {
return new Promise((resolve, reject) => {
oops.res.load(path, JsonAsset, (err, jsonAsset) => {
if (err) {
reject(err);
return;
}
resolve(JSON.parse(jsonAsset.json));
});
});
}
}
章节来源
Bundle管理策略
Bundle划分原则
合理的Bundle划分是资源管理的基础,遵循以下原则:
1. 功能模块划分
graph LR
Resources[resources] --> Common[公共资源]
Resources --> Game[游戏资源]
Resources --> UI[界面资源]
Resources --> Audio[音频资源]
Common --> Fonts[字体文件]
Common --> Shaders[着色器]
Game --> Heroes[英雄资源]
Game --> Maps[地图资源]
Game --> Skills[技能资源]
UI --> Icons[图标资源]
UI --> Animations[动画资源]
Audio --> Music[背景音乐]
Audio --> Effects[音效]
2. 生命周期划分
- 常驻Bundle:游戏启动必需的核心资源
- 场景Bundle:特定场景的资源包
- 临时Bundle:临时使用的资源包
- 更新Bundle:可热更新的资源包
3. 加载优先级划分
// 高优先级资源(立即加载)
const highPriorityBundles = [
"resources", // 核心引擎资源
"common", // 公共组件资源
"ui/loading" // 加载界面资源
];
// 中优先级资源(预加载)
const mediumPriorityBundles = [
"game", // 游戏主资源
"language" // 语言包资源
];
// 低优先级资源(延迟加载)
const lowPriorityBundles = [
"heroes", // 英雄资源
"skills" // 技能资源
];
Bundle配置管理
构建配置
在builder.json中配置Bundle压缩和分发策略:
{
"bundleConfig": {
"custom": {
"auto_98AwgBoURL0KsT0uXAsTE4": {
"displayName": "resources",
"configs": {
"native": {
"preferredOptions": {
"compressionType": "merge_dep",
"isRemote": false
}
},
"wechatgame": {
"compressionType": "subpackage",
"isRemote": false
}
}
}
}
}
}
运行时Bundle管理
class BundleManager {
private loadedBundles: Set<string> = new Set();
// 按优先级加载Bundle
async loadBundlesInOrder(priorityList: string[]) {
for (const bundleName of priorityList) {
if (!this.loadedBundles.has(bundleName)) {
await this.loadBundle(bundleName);
this.loadedBundles.add(bundleName);
}
}
}
// 条件加载Bundle
async conditionallyLoadBundle(condition: boolean, bundleName: string) {
if (condition && !this.loadedBundles.has(bundleName)) {
await this.loadBundle(bundleName);
this.loadedBundles.add(bundleName);
}
}
// 释放Bundle
releaseBundle(bundleName: string) {
oops.res.releaseDir("", bundleName);
this.loadedBundles.delete(bundleName);
}
}
章节来源
性能优化技术
批量加载优化
并行加载策略
// 使用Promise.all进行并行加载
async loadMultipleResources() {
const tasks = [
oops.res.loadAsync("hero/skeleton", sp.SkeletonData),
oops.res.loadAsync("hero/idle", AnimationClip),
oops.res.loadAsync("hero/walk", AnimationClip),
oops.res.loadAsync("hero/atk", AnimationClip)
];
try {
const [skeletonData, idleClip, walkClip, atkClip] = await Promise.all(tasks);
// 批量设置资源
this.setupCharacterAnimations(skeletonData, {
idle: idleClip,
walk: walkClip,
attack: atkClip
});
} catch (error) {
console.error("批量加载失败:", error);
}
}
分批加载策略
class BatchLoader {
private readonly BATCH_SIZE = 10;
private readonly LOAD_DELAY = 100;
async loadResourcesInBatches(resourcePaths: string[]) {
const batches = this.chunkArray(resourcePaths, this.BATCH_SIZE);
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
// 并行加载当前批次
await Promise.all(batch.map(path =>
oops.res.loadAsync(path, Asset)
));
// 可选:添加延迟避免卡顿
if (i < batches.length - 1) {
await this.delay(this.LOAD_DELAY);
}
}
}
private chunkArray<T>(array: T[], chunkSize: number): T[][] {
const chunks = [];
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
return chunks;
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
延迟加载策略
按需加载实现
class LazyResourceManager {
private loadedResources: Map<string, Asset> = new Map();
private loadingPromises: Map<string, Promise<Asset>> = new Map();
// 按需加载资源
async getResource<T extends Asset>(path: string, type: AssetType<T>): Promise<T> {
// 检查缓存
const cached = this.loadedResources.get(path);
if (cached) {
return cached as T;
}
// 检查是否正在加载
const loadingPromise = this.loadingPromises.get(path);
if (loadingPromise) {
return loadingPromise.then(asset => asset as T);
}
// 开始加载
const promise = new Promise<T>((resolve, reject) => {
oops.res.load(path, type, (err, asset) => {
if (err) {
reject(err);
return;
}
this.loadedResources.set(path, asset);
this.loadingPromises.delete(path);
resolve(asset as T);
});
});
this.loadingPromises.set(path, promise);
return promise;
}
// 预加载资源
preloadResources(paths: string[]) {
paths.forEach(path => {
if (!this.loadedResources.has(path) && !this.loadingPromises.has(path)) {
this.getResource(path, Asset);
}
});
}
}
内存预分配优化
class MemoryOptimizer {
private static readonly MEMORY_POOL_SIZE = 1024 * 1024 * 50; // 50MB
private memoryPool: ArrayBuffer[] = [];
// 预分配内存池
preallocateMemory() {
const chunkSize = 1024 * 1024; // 1MB
const chunksNeeded = this.MEMORY_POOL_SIZE / chunkSize;
for (let i = 0; i < chunksNeeded; i++) {
this.memoryPool.push(new ArrayBuffer(chunkSize));
}
}
// 获取内存块
getMemoryChunk(size: number): ArrayBuffer {
const index = Math.floor(Math.random() * this.memoryPool.length);
const chunk = this.memoryPool[index];
// 如果大小合适,直接返回
if (chunk.byteLength >= size) {
return chunk;
}
// 否则创建新的内存块
return new ArrayBuffer(size);
}
// 释放内存
releaseMemory(chunk: ArrayBuffer) {
// 可以选择将内存块放回池中
// 或者让JavaScript垃圾回收器处理
}
}
章节来源
内存管理机制
引用计数系统
系统采用自动引用计数机制管理资源生命周期。
引用计数原理
stateDiagram-v2
[*] --> Created : 创建资源
Created --> Referenced : addRef()
Referenced --> Referenced : addRef()
Referenced --> Released : decRef()
Released --> [*] : refCount === 0
Referenced --> Destroyed : destroy()
Destroyed --> [*] : 清理资源
自动释放机制
class ResourceManager {
private assets: Map<string, Asset> = new Map();
private refCounts: Map<string, number> = new Map();
// 加载资源并增加引用计数
loadWithRefCount<T extends Asset>(path: string, type: AssetType<T>): T {
let asset = this.assets.get(path);
if (!asset) {
// 首次加载
asset = this.createAsset(path, type);
this.assets.set(path, asset);
this.refCounts.set(path, 1);
} else {
// 已存在,增加引用计数
const count = this.refCounts.get(path) || 0;
this.refCounts.set(path, count + 1);
}
return asset;
}
// 减少引用计数
releaseWithRefCount(path: string) {
const count = this.refCounts.get(path) || 0;
if (count <= 1) {
// 最后一个引用,释放资源
this.assets.delete(path);
this.refCounts.delete(path);
} else {
this.refCounts.set(path, count - 1);
}
}
// 手动释放所有资源
releaseAll() {
this.assets.clear();
this.refCounts.clear();
}
}
垃圾回收策略
自动垃圾回收
class GarbageCollector {
private readonly MAX_CACHE_SIZE = 1000;
private readonly CLEANUP_INTERVAL = 60000; // 1分钟
constructor() {
setInterval(() => this.cleanup(), this.CLEANUP_INTERVAL);
}
// 清理过期资源
cleanup() {
const now = Date.now();
const expiredKeys: string[] = [];
// 检查缓存大小
if (this.cache.size > this.MAX_CACHE_SIZE) {
// 清理最久未使用的资源
const sortedEntries = Array.from(this.cache.entries())
.sort((a, b) => a[1].lastAccess - b[1].lastAccess);
const itemsToRemove = sortedEntries.slice(0, 100);
itemsToRemove.forEach(([key]) => {
this.cache.delete(key);
});
}
}
}
手动释放策略
场景化释放
class SceneResourceManager {
private sceneResources: Set<string> = new Set();
// 记录场景资源
recordSceneResource(path: string) {
this.sceneResources.add(path);
}
// 场景切换时释放资源
releaseSceneResources() {
this.sceneResources.forEach(path => {
oops.res.release(path);
});
this.sceneResources.clear();
}
// 批量释放
releaseBatch(paths: string[]) {
paths.forEach(path => {
oops.res.release(path);
this.sceneResources.delete(path);
});
}
}
章节来源
调试与监控
资源状态监控
系统提供了完整的资源状态监控和调试功能。
dump方法使用
// 打印所有缓存资源信息
oops.res.dump();
// 输出示例:
// Asset {uuid: "123456", nativeUrl: "assets/resources/ui/button.png"}
// Asset {uuid: "789012", nativeUrl: "assets/resources/audio/bg_music.mp3"}
// 当前资源总数:156
资源统计工具
class ResourceMonitor {
private statistics: Map<string, ResourceStats> = new Map();
// 记录资源加载
recordLoad(path: string, type: string, size: number) {
const stats = this.statistics.get(path) || {
loads: 0,
hits: 0,
misses: 0,
totalSize: 0,
lastAccess: 0
};
stats.loads++;
stats.totalSize += size;
stats.lastAccess = Date.now();
this.statistics.set(path, stats);
}
// 记录缓存命中
recordHit(path: string) {
const stats = this.statistics.get(path);
if (stats) {
stats.hits++;
}
}
// 获取统计报告
getReport(): ResourceReport {
const totalResources = this.statistics.size;
let totalSize = 0;
let totalLoads = 0;
this.statistics.forEach(stats => {
totalSize += stats.totalSize;
totalLoads += stats.loads;
});
return {
totalResources,
totalSize,
hitRate: totalLoads > 0 ? totalLoads / this.statistics.size : 0,
topResources: this.getTopResources(),
memoryUsage: this.getMemoryUsage()
};
}
private getTopResources(): TopResource[] {
return Array.from(this.statistics.entries())
.sort((a, b) => b[1].loads - a[1].loads)
.slice(0, 10)
.map(([path, stats]) => ({ path, ...stats }));
}
}
性能监控
加载性能分析
class PerformanceMonitor {
private loadTimes: Map<string, number[]> = new Map();
// 开始测量加载时间
startMeasure(path: string): () => void {
const startTime = performance.now();
return () => {
const endTime = performance.now();
const duration = endTime - startTime;
let times = this.loadTimes.get(path) || [];
times.push(duration);
if (times.length > 10) {
times.shift(); // 保留最近10次记录
}
this.loadTimes.set(path, times);
};
}
// 获取平均加载时间
getAverageLoadTime(path: string): number {
const times = this.loadTimes.get(path);
if (!times || times.length === 0) {
return 0;
}
return times.reduce((sum, time) => sum + time, 0) / times.length;
}
// 性能警告
checkPerformanceWarnings() {
this.loadTimes.forEach((times, path) => {
const avgTime = times.reduce((sum, time) => sum + time, 0) / times.length;
if (avgTime > 1000) { // 1秒以上
console.warn(`资源加载缓慢: ${path}, 平均耗时: ${avgTime.toFixed(2)}ms`);
}
});
}
}
章节来源
常见问题与解决方案
资源未释放导致内存溢出
问题描述
资源加载后没有及时释放,导致内存持续增长,最终引发内存溢出。
解决方案
// 错误示例:忘记释放资源
class BadResourceManager {
loadManyResources() {
for (let i = 0; i < 1000; i++) {
oops.res.load(`resource_${i}`, Asset, (err, asset) => {
if (err) return;
// 忘记释放资源!
this.processAsset(asset);
});
}
}
}
// 正确示例:及时释放资源
class GoodResourceManager {
private loadedAssets: Set<string> = new Set();
loadManyResources() {
for (let i = 0; i < 1000; i++) {
const path = `resource_${i}`;
oops.res.load(path, Asset, (err, asset) => {
if (err) return;
this.processAsset(asset);
this.loadedAssets.add(path);
});
}
}
unloadResources() {
this.loadedAssets.forEach(path => {
oops.res.release(path);
});
this.loadedAssets.clear();
}
}
自动化资源管理
class AutoResourceManager {
private resourceTracker: Map<string, {refCount: number, lastAccess: number}> = new Map();
// 自动跟踪资源使用
trackResourceUsage(path: string) {
const info = this.resourceTracker.get(path) || {
refCount: 0,
lastAccess: Date.now()
};
info.refCount++;
info.lastAccess = Date.now();
this.resourceTracker.set(path, info);
}
// 定期清理未使用的资源
cleanupUnusedResources() {
const now = Date.now();
const cleanupThreshold = 300000; // 5分钟
this.resourceTracker.forEach((info, path) => {
if (info.refCount === 0 && (now - info.lastAccess) > cleanupThreshold) {
oops.res.release(path);
this.resourceTracker.delete(path);
}
});
}
}
异步加载顺序错乱
问题描述
多个异步加载任务同时进行,导致资源加载完成后处理顺序混乱。
解决方案
// 错误示例:异步顺序错乱
class BadAsyncLoader {
loadCharacterAssets() {
oops.res.load("hero/skeleton", SkeletonData, this.handleSkeleton);
oops.res.load("hero/idle", AnimationClip, this.handleIdle);
oops.res.load("hero/walk", AnimationClip, this.handleWalk);
// 处理顺序不确定
}
}
// 正确示例:使用Promise链
class GoodAsyncLoader {
async loadCharacterAssets() {
try {
const skeletonData = await oops.res.loadAsync("hero/skeleton", SkeletonData);
const idleClip = await oops.res.loadAsync("hero/idle", AnimationClip);
const walkClip = await oops.res.loadAsync("hero/walk", AnimationClip);
// 确保顺序执行
this.setupCharacterAnimations(skeletonData, idleClip, walkClip);
} catch (error) {
console.error("角色资源加载失败:", error);
}
}
}
// 另一种解决方案:使用队列
class QueueAsyncLoader {
private loadQueue: LoadTask[] = [];
enqueueLoad(path: string, type: AssetType<any>, callback: CompleteCallback<any>) {
this.loadQueue.push({path, type, callback});
this.processQueue();
}
private processQueue() {
if (this.loadQueue.length === 0) return;
const task = this.loadQueue[0];
oops.res.load(task.path, task.type, (err, asset) => {
task.callback(err, asset);
this.loadQueue.shift();
this.processQueue();
});
}
}
Bundle加载失败
问题诊断
class BundleErrorHandler {
async loadBundleWithRetry(url: string, md5: string, maxRetries: number = 3) {
let retries = 0;
while (retries < maxRetries) {
try {
const bundle = await oops.res.loadBundle(url, md5);
console.log(`Bundle加载成功: ${bundle.name}`);
return bundle;
} catch (error) {
retries++;
console.warn(`Bundle加载失败,重试${retries}/${maxRetries}:`, error);
if (retries >= maxRetries) {
console.error("Bundle加载多次失败,使用本地资源");
return this.fallbackToLocalBundle();
}
// 等待后重试
await this.delay(2000 * retries);
}
}
}
private fallbackToLocalBundle() {
// 回退到本地Bundle
return oops.res.loadBundle("assets/resources");
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
资源路径管理问题
路径规范化
class PathManager {
// 规范化路径
normalizePath(path: string): string {
// 移除开头和结尾的斜杠
return path.replace(/^\/|\/$/g, '');
}
// 获取Bundle内的资源路径
getResourcePath(bundle: string, path: string): string {
if (bundle === 'resources') {
return this.normalizePath(path);
}
return `${bundle}/${this.normalizePath(path)}`;
}
// 检查路径有效性
validateResourcePath(path: string): boolean {
// 不允许特殊字符
if (/[\*\?\"\<\>\|]/.test(path)) {
return false;
}
// 不允许相对路径
if (path.includes('..')) {
return false;
}
return true;
}
}
章节来源
最佳实践指南
资源加载最佳实践
1. 分阶段加载策略
class StagedLoader {
private readonly STAGES = [
{ name: 'core', priority: 1, bundles: ['resources', 'common'] },
{ name: 'ui', priority: 2, bundles: ['ui', 'fonts'] },
{ name: 'game', priority: 3, bundles: ['game', 'heroes'] },
{ name: 'audio', priority: 4, bundles: ['audio'] }
];
async loadGameResources() {
for (const stage of this.STAGES) {
console.log(`开始加载阶段: ${stage.name}`);
const promises = stage.bundles.map(bundle =>
oops.res.loadBundle(bundle)
);
await Promise.all(promises);
console.log(`阶段 ${stage.name} 加载完成`);
// 可选:添加视觉反馈
this.updateLoadingScreen(stage.name);
}
}
}
2. 资源预加载策略
class Preloader {
private preloadedResources: Map<string, Asset> = new Map();
// 预加载常用资源
async preloadCommonResources() {
const commonPaths = [
'ui/button',
'ui/background',
'common/icons',
'common/fonts'
];
await Promise.all(commonPaths.map(path =>
this.preloadResource(path)
));
}
// 预加载单个资源
private async preloadResource(path: string) {
try {
const asset = await oops.res.loadAsync(path, Asset);
this.preloadedResources.set(path, asset);
} catch (error) {
console.warn(`预加载失败: ${path}`, error);
}
}
// 获取预加载资源
getResource(path: string): Asset | null {
return this.preloadedResources.get(path) || null;
}
}
3. 错误处理和降级策略
class RobustLoader {
private readonly RETRY_COUNT = 3;
private readonly TIMEOUT = 10000;
async loadResourceWithFallback(path: string, type: AssetType<any>) {
let lastError: Error;
for (let i = 0; i < this.RETRY_COUNT; i++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.TIMEOUT);
const asset = await oops.res.loadAsync(path, type);
clearTimeout(timeoutId);
return asset;
} catch (error) {
lastError = error as Error;
console.warn(`资源加载失败(${i + 1}/${this.RETRY_COUNT}): ${path}`, error);
if (i < this.RETRY_COUNT - 1) {
await this.delay(1000 * (i + 1));
}
}
}
// 降级处理
return this.provideFallbackResource(path, type);
}
private provideFallbackResource(path: string, type: AssetType<any>): Asset {
// 提供默认资源
console.warn(`使用降级资源: ${path}`);
return this.createDefaultAsset(type);
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
性能优化最佳实践
1. 资源压缩和优化
class ResourceOptimizer {
// 根据设备性能调整资源质量
getOptimizedResourcePath(basePath: string, deviceInfo: DeviceInfo): string {
const qualitySuffix = this.getQualitySuffix(deviceInfo);
return `${basePath}_${qualitySuffix}`;
}
private getQualitySuffix(deviceInfo: DeviceInfo): string {
if (deviceInfo.gpuPerformance === 'high') {
return 'hd';
} else if (deviceInfo.gpuPerformance === 'medium') {
return 'sd';
} else {
return 'ld';
}
}
// 动态调整资源分辨率
async loadOptimizedImage(path: string, targetWidth: number): Promise<SpriteFrame> {
const original = await oops.res.loadAsync(path, ImageAsset);
// 根据目标宽度调整图片
const resized = this.resizeImage(original, targetWidth);
return this.createSpriteFrame(resized);
}
}
2. 缓存策略优化
class CacheManager {
private readonly CACHE_CONFIG = {
maxItems: 100,
maxSize: 1024 * 1024 * 50, // 50MB
ttl: 300000, // 5分钟
evictionPolicy: 'lru' as 'lru' | 'lfu'
};
private cache: Map<string, CachedItem> = new Map();
// 智能缓存策略
getCachedOrLoad<T extends Asset>(path: string, type: AssetType<T>): T | null {
const cached = this.cache.get(path);
if (cached && !this.isExpired(cached)) {
// 更新访问时间
cached.lastAccess = Date.now();
return cached.asset as T;
}
// 加载新资源
const asset = oops.res.get(path, type);
if (asset) {
this.cache.set(path, {
asset,
lastAccess: Date.now(),
size: this.estimateAssetSize(asset)
});
this.enforceCacheLimits();
}
return asset;
}
private enforceCacheLimits() {
if (this.cache.size > this.CACHE_CONFIG.maxItems) {
this.evictLeastUsed();
}
}
private evictLeastUsed() {
const sorted = Array.from(this.cache.entries())
.sort((a, b) => a[1].lastAccess - b[1].lastAccess);
const itemsToEvict = Math.floor(sorted.length * 0.2); // 清理20%
for (let i = 0; i < itemsToEvict; i++) {
this.cache.delete(sorted[i][0]);
}
}
}
开发调试最佳实践
1. 资源加载日志
class ResourceLogger {
private static instance: ResourceLogger;
private constructor() {}
static getInstance(): ResourceLogger {
if (!ResourceLogger.instance) {
ResourceLogger.instance = new ResourceLogger();
}
return ResourceLogger.instance;
}
logLoadStart(path: string, type: string) {
console.group(`🔍 加载资源: ${path}`);
console.log(`类型: ${type}`);
console.time(`加载时间`);
}
logLoadEnd(path: string, success: boolean, duration: number) {
console.timeEnd(`加载时间`);
console.log(`结果: ${success ? '✅ 成功' : '❌ 失败'}`);
console.log(`耗时: ${duration.toFixed(2)}ms`);
console.groupEnd();
}
logMemoryUsage() {
const memory = performance.memory;
if (memory) {
console.log(`内存使用情况:`);
console.log(`- 已使用: ${(memory.usedJSHeapSize / 1024 / 1024).toFixed(2)} MB`);
console.log(`- 总可用: ${(memory.totalJSHeapSize / 1024 / 1024).toFixed(2)} MB`);
console.log(`- 限制: ${(memory.jsHeapSizeLimit / 1024 / 1024).toFixed(2)} MB`);
}
}
}
2. 资源完整性检查
class IntegrityChecker {
// 检查资源完整性
async checkResourceIntegrity(path: string, expectedHash: string): Promise<boolean> {
try {
const asset = await oops.res.loadAsync(path, Asset);
const actualHash = this.calculateHash(asset);
const isValid = actualHash === expectedHash;
if (!isValid) {
console.error(`资源完整性检查失败: ${path}`);
console.log(`期望哈希: ${expectedHash}`);
console.log(`实际哈希: ${actualHash}`);
}
return isValid;
} catch (error) {
console.error(`资源完整性检查失败: ${path}`, error);
return false;
}
}
private calculateHash(asset: Asset): string {
// 实现哈希计算逻辑
return '';
}
// 批量检查
async checkMultipleResources(checks: ResourceCheck[]): Promise<ResourceValidationResult> {
const results = await Promise.all(checks.map(check =>
this.checkResourceIntegrity(check.path, check.hash)
));
return {
passed: results.filter(result => result).length,
failed: results.filter(result => !result).length,
total: checks.length
};
}
}
通过遵循这些最佳实践,开发者可以构建一个高效、稳定、易于维护的资源管理系统,确保游戏在各种环境下都能提供优秀的用户体验。