1602 lines
44 KiB
Markdown
1602 lines
44 KiB
Markdown
# 基于Oops Plugin Framework的资源管理系统
|
||
|
||
<cite>
|
||
**本文档引用的文件**
|
||
- [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)
|
||
</cite>
|
||
|
||
## 目录
|
||
1. [简介](#简介)
|
||
2. [系统架构](#系统架构)
|
||
3. [核心API详解](#核心api详解)
|
||
4. [资源加载场景](#资源加载场景)
|
||
5. [Bundle管理策略](#bundle管理策略)
|
||
6. [性能优化技术](#性能优化技术)
|
||
7. [内存管理机制](#内存管理机制)
|
||
8. [调试与监控](#调试与监控)
|
||
9. [常见问题与解决方案](#常见问题与解决方案)
|
||
10. [最佳实践指南](#最佳实践指南)
|
||
|
||
## 简介
|
||
|
||
Oops Plugin Framework的资源管理系统是一个功能强大的资源加载与管理框架,专为Cocos Creator游戏开发而设计。该系统提供了完整的资源生命周期管理,包括加载、缓存、释放和监控等功能,支持Spine骨骼动画、SpriteFrame、配置文件等多种资源类型的高效管理。
|
||
|
||
### 核心特性
|
||
|
||
- **多格式支持**:支持Spine骨骼动画、SpriteFrame、音频、纹理等多种资源类型
|
||
- **智能缓存**:自动管理资源缓存,避免重复加载
|
||
- **异步加载**:支持进度回调和错误处理
|
||
- **Bundle管理**:灵活的资源包划分和热更新支持
|
||
- **内存优化**:自动垃圾回收和手动释放机制
|
||
- **调试工具**:内置资源状态监控和调试功能
|
||
|
||
## 系统架构
|
||
|
||
```mermaid
|
||
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
|
||
```
|
||
|
||
**图表来源**
|
||
- [ResLoader.ts](file://extensions/oops-plugin-framework/assets/core/common/loader/ResLoader.ts#L22-L335)
|
||
|
||
**章节来源**
|
||
- [ResLoader.ts](file://extensions/oops-plugin-framework/assets/core/common/loader/ResLoader.ts#L1-L50)
|
||
|
||
## 核心API详解
|
||
|
||
### load - 单资源加载
|
||
|
||
`load`方法是最基础的资源加载API,支持多种重载形式以适应不同的使用场景。
|
||
|
||
#### 基本语法
|
||
```typescript
|
||
// 加载默认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骨骼动画:**
|
||
```typescript
|
||
// 加载角色动画资源
|
||
oops.res.load("hero/skeleton", sp.SkeletonData, (err, skeletonData) => {
|
||
if (err) {
|
||
console.error("角色动画加载失败:", err);
|
||
return;
|
||
}
|
||
this.skeletonData = skeletonData;
|
||
this.updateSkeleton();
|
||
});
|
||
```
|
||
|
||
**加载SpriteFrame:**
|
||
```typescript
|
||
// 加载UI按钮图片
|
||
oops.res.load("ui/button_normal", SpriteFrame, (err, spriteFrame) => {
|
||
if (err) {
|
||
console.error("按钮图片加载失败:", err);
|
||
return;
|
||
}
|
||
this.button.spriteFrame = spriteFrame;
|
||
});
|
||
```
|
||
|
||
### loadDir - 文件夹批量加载
|
||
|
||
`loadDir`方法用于批量加载指定目录下的所有资源,特别适合加载同一类型的资源集合。
|
||
|
||
#### 语法结构
|
||
```typescript
|
||
oops.res.loadDir(bundleName, dir, type, onProgress, onComplete);
|
||
```
|
||
|
||
#### 进度回调机制
|
||
|
||
```mermaid
|
||
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[])
|
||
```
|
||
|
||
**图表来源**
|
||
- [ResLoader.ts](file://extensions/oops-plugin-framework/assets/core/common/loader/ResLoader.ts#L150-L200)
|
||
- [LoadingViewComp.ts](file://assets/script/game/initialize/view/LoadingViewComp.ts#L70-L90)
|
||
|
||
#### 实际应用案例
|
||
|
||
**游戏资源加载流程:**
|
||
```typescript
|
||
// 加载游戏主界面资源
|
||
oops.res.loadDir("game", (finished, total, item) => {
|
||
// 更新加载进度
|
||
this.updateProgressBar(finished, total);
|
||
console.log(`正在加载: ${item.uuid} (${finished}/${total})`);
|
||
}, () => {
|
||
// 所有资源加载完成
|
||
this.onGameResourcesLoaded();
|
||
});
|
||
```
|
||
|
||
**章节来源**
|
||
- [ResLoader.ts](file://extensions/oops-plugin-framework/assets/core/common/loader/ResLoader.ts#L150-L220)
|
||
- [LoadingViewComp.ts](file://assets/script/game/initialize/view/LoadingViewComp.ts#L60-L90)
|
||
|
||
### loadRemote - 远程资源加载
|
||
|
||
`loadRemote`方法专门用于加载网络上的远程资源,支持图片、音频等文件的动态下载。
|
||
|
||
#### 方法签名
|
||
```typescript
|
||
oops.res.loadRemote<T extends Asset>(url: string, options: IRemoteOptions, onComplete: CompleteCallback<T>);
|
||
```
|
||
|
||
#### 配置选项
|
||
|
||
| 选项 | 类型 | 描述 |
|
||
|------|------|------|
|
||
| `ext` | `string` | 文件扩展名,如".png"、".mp3" |
|
||
| `timeout` | `number` | 请求超时时间(毫秒) |
|
||
| `headers` | `Record<string, string>` | 自定义请求头 |
|
||
|
||
#### 完整实现示例
|
||
|
||
```typescript
|
||
// 加载远程图片资源
|
||
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`方法支持从服务器动态加载资源包,实现游戏的热更新功能。
|
||
|
||
#### 使用流程
|
||
|
||
```mermaid
|
||
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
|
||
```
|
||
|
||
**图表来源**
|
||
- [ResLoader.ts](file://extensions/oops-plugin-framework/assets/core/common/loader/ResLoader.ts#L60-L80)
|
||
|
||
#### 热更新实现
|
||
|
||
```typescript
|
||
// 热更新资源包
|
||
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();
|
||
}
|
||
}
|
||
```
|
||
|
||
**章节来源**
|
||
- [ResLoader.ts](file://extensions/oops-plugin-framework/assets/core/common/loader/ResLoader.ts#L60-L85)
|
||
|
||
### release & releaseDir - 资源释放
|
||
|
||
资源释放是内存管理的关键环节,系统提供了精确的资源释放机制。
|
||
|
||
#### release - 单资源释放
|
||
|
||
```typescript
|
||
// 释放单个资源
|
||
oops.res.release("model/player", "resources");
|
||
|
||
// 释放其他Bundle中的资源
|
||
oops.res.release("ui/button", "assets");
|
||
```
|
||
|
||
#### releaseDir - 批量资源释放
|
||
|
||
```typescript
|
||
// 释放整个目录的资源
|
||
oops.res.releaseDir("game", "resources");
|
||
|
||
// 释放后清理Bundle(如果需要)
|
||
oops.res.releaseDir("temp", "temporary");
|
||
```
|
||
|
||
#### 内存管理原理
|
||
|
||
```mermaid
|
||
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
|
||
```
|
||
|
||
**图表来源**
|
||
- [ResLoader.ts](file://extensions/oops-plugin-framework/assets/core/common/loader/ResLoader.ts#L240-L280)
|
||
|
||
**章节来源**
|
||
- [ResLoader.ts](file://extensions/oops-plugin-framework/assets/core/common/loader/ResLoader.ts#L240-L300)
|
||
|
||
## 资源加载场景
|
||
|
||
### Spine骨骼动画加载
|
||
|
||
Spine骨骼动画是游戏中常见的动画表现形式,系统提供了专门的加载和管理机制。
|
||
|
||
#### 加载流程
|
||
|
||
```mermaid
|
||
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.ts](file://assets/script/game/hero/HeroSpine.ts#L1-L50)
|
||
- [HeroAnmComp.ts](file://assets/script/game/hero/HeroAnmComp.ts#L1-L30)
|
||
|
||
#### 实际应用代码
|
||
|
||
**HeroSpine组件实现:**
|
||
```typescript
|
||
// 在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动画控制器:**
|
||
```typescript
|
||
// 动画播放控制
|
||
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';
|
||
}
|
||
```
|
||
|
||
**章节来源**
|
||
- [HeroSpine.ts](file://assets/script/game/hero/HeroSpine.ts#L20-L80)
|
||
- [HeroAnmComp.ts](file://assets/script/game/hero/HeroAnmComp.ts#L20-L50)
|
||
|
||
### SpriteFrame精灵图加载
|
||
|
||
SpriteFrame是UI和游戏元素的基础资源类型,系统提供了高效的加载和管理机制。
|
||
|
||
#### 加载策略
|
||
|
||
| 场景 | 推荐策略 | 示例 |
|
||
|------|----------|------|
|
||
| UI按钮 | 同步加载 | `oops.res.load("ui/button", SpriteFrame)` |
|
||
| 背景图片 | 异步加载 | `oops.res.loadDir("background", ...)` |
|
||
| 角色皮肤 | 延迟加载 | 游戏开始后按需加载 |
|
||
| 动态图标 | 缓存复用 | 首次加载后缓存 |
|
||
|
||
#### 实际应用
|
||
|
||
```typescript
|
||
// 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;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 配置文件加载
|
||
|
||
配置文件是游戏运行时的重要数据源,系统提供了灵活的配置加载机制。
|
||
|
||
#### 配置文件组织结构
|
||
|
||
```mermaid
|
||
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]
|
||
```
|
||
|
||
**图表来源**
|
||
- [config.json](file://assets/resources/config.json#L1-L21)
|
||
|
||
#### 加载实现
|
||
|
||
```typescript
|
||
// 配置文件加载器
|
||
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));
|
||
});
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
**章节来源**
|
||
- [config.json](file://assets/resources/config.json#L1-L21)
|
||
- [Initialize.ts](file://assets/script/game/initialize/Initialize.ts#L40-L70)
|
||
|
||
## Bundle管理策略
|
||
|
||
### Bundle划分原则
|
||
|
||
合理的Bundle划分是资源管理的基础,遵循以下原则:
|
||
|
||
#### 1. 功能模块划分
|
||
```mermaid
|
||
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. 加载优先级划分
|
||
```typescript
|
||
// 高优先级资源(立即加载)
|
||
const highPriorityBundles = [
|
||
"resources", // 核心引擎资源
|
||
"common", // 公共组件资源
|
||
"ui/loading" // 加载界面资源
|
||
];
|
||
|
||
// 中优先级资源(预加载)
|
||
const mediumPriorityBundles = [
|
||
"game", // 游戏主资源
|
||
"language" // 语言包资源
|
||
];
|
||
|
||
// 低优先级资源(延迟加载)
|
||
const lowPriorityBundles = [
|
||
"heroes", // 英雄资源
|
||
"skills" // 技能资源
|
||
];
|
||
```
|
||
|
||
### Bundle配置管理
|
||
|
||
#### 构建配置
|
||
在`builder.json`中配置Bundle压缩和分发策略:
|
||
|
||
```json
|
||
{
|
||
"bundleConfig": {
|
||
"custom": {
|
||
"auto_98AwgBoURL0KsT0uXAsTE4": {
|
||
"displayName": "resources",
|
||
"configs": {
|
||
"native": {
|
||
"preferredOptions": {
|
||
"compressionType": "merge_dep",
|
||
"isRemote": false
|
||
}
|
||
},
|
||
"wechatgame": {
|
||
"compressionType": "subpackage",
|
||
"isRemote": false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 运行时Bundle管理
|
||
|
||
```typescript
|
||
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);
|
||
}
|
||
}
|
||
```
|
||
|
||
**章节来源**
|
||
- [builder.json](file://settings/v2/packages/builder.json#L1-L84)
|
||
- [Initialize.ts](file://assets/script/game/initialize/Initialize.ts#L80-L105)
|
||
|
||
## 性能优化技术
|
||
|
||
### 批量加载优化
|
||
|
||
#### 并行加载策略
|
||
|
||
```typescript
|
||
// 使用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);
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 分批加载策略
|
||
|
||
```typescript
|
||
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));
|
||
}
|
||
}
|
||
```
|
||
|
||
### 延迟加载策略
|
||
|
||
#### 按需加载实现
|
||
|
||
```typescript
|
||
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);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
### 内存预分配优化
|
||
|
||
```typescript
|
||
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垃圾回收器处理
|
||
}
|
||
}
|
||
```
|
||
|
||
**章节来源**
|
||
- [LoadingViewComp.ts](file://assets/script/game/initialize/view/LoadingViewComp.ts#L60-L90)
|
||
|
||
## 内存管理机制
|
||
|
||
### 引用计数系统
|
||
|
||
系统采用自动引用计数机制管理资源生命周期。
|
||
|
||
#### 引用计数原理
|
||
|
||
```mermaid
|
||
stateDiagram-v2
|
||
[*] --> Created : 创建资源
|
||
Created --> Referenced : addRef()
|
||
Referenced --> Referenced : addRef()
|
||
Referenced --> Released : decRef()
|
||
Released --> [*] : refCount === 0
|
||
Referenced --> Destroyed : destroy()
|
||
Destroyed --> [*] : 清理资源
|
||
```
|
||
|
||
#### 自动释放机制
|
||
|
||
```typescript
|
||
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();
|
||
}
|
||
}
|
||
```
|
||
|
||
### 垃圾回收策略
|
||
|
||
#### 自动垃圾回收
|
||
|
||
```typescript
|
||
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);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 手动释放策略
|
||
|
||
#### 场景化释放
|
||
|
||
```typescript
|
||
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);
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
**章节来源**
|
||
- [ResLoader.ts](file://extensions/oops-plugin-framework/assets/core/common/loader/ResLoader.ts#L280-L320)
|
||
|
||
## 调试与监控
|
||
|
||
### 资源状态监控
|
||
|
||
系统提供了完整的资源状态监控和调试功能。
|
||
|
||
#### dump方法使用
|
||
|
||
```typescript
|
||
// 打印所有缓存资源信息
|
||
oops.res.dump();
|
||
|
||
// 输出示例:
|
||
// Asset {uuid: "123456", nativeUrl: "assets/resources/ui/button.png"}
|
||
// Asset {uuid: "789012", nativeUrl: "assets/resources/audio/bg_music.mp3"}
|
||
// 当前资源总数:156
|
||
```
|
||
|
||
#### 资源统计工具
|
||
|
||
```typescript
|
||
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 }));
|
||
}
|
||
}
|
||
```
|
||
|
||
### 性能监控
|
||
|
||
#### 加载性能分析
|
||
|
||
```typescript
|
||
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`);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
**章节来源**
|
||
- [loader.md](file://doc/core/common/loader.md#L86-L90)
|
||
|
||
## 常见问题与解决方案
|
||
|
||
### 资源未释放导致内存溢出
|
||
|
||
#### 问题描述
|
||
资源加载后没有及时释放,导致内存持续增长,最终引发内存溢出。
|
||
|
||
#### 解决方案
|
||
|
||
```typescript
|
||
// 错误示例:忘记释放资源
|
||
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();
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 自动化资源管理
|
||
|
||
```typescript
|
||
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);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
### 异步加载顺序错乱
|
||
|
||
#### 问题描述
|
||
多个异步加载任务同时进行,导致资源加载完成后处理顺序混乱。
|
||
|
||
#### 解决方案
|
||
|
||
```typescript
|
||
// 错误示例:异步顺序错乱
|
||
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加载失败
|
||
|
||
#### 问题诊断
|
||
|
||
```typescript
|
||
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));
|
||
}
|
||
}
|
||
```
|
||
|
||
### 资源路径管理问题
|
||
|
||
#### 路径规范化
|
||
|
||
```typescript
|
||
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;
|
||
}
|
||
}
|
||
```
|
||
|
||
**章节来源**
|
||
- [ResLoader.ts](file://extensions/oops-plugin-framework/assets/core/common/loader/ResLoader.ts#L240-L335)
|
||
|
||
## 最佳实践指南
|
||
|
||
### 资源加载最佳实践
|
||
|
||
#### 1. 分阶段加载策略
|
||
|
||
```typescript
|
||
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. 资源预加载策略
|
||
|
||
```typescript
|
||
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. 错误处理和降级策略
|
||
|
||
```typescript
|
||
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. 资源压缩和优化
|
||
|
||
```typescript
|
||
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. 缓存策略优化
|
||
|
||
```typescript
|
||
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. 资源加载日志
|
||
|
||
```typescript
|
||
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. 资源完整性检查
|
||
|
||
```typescript
|
||
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
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
通过遵循这些最佳实践,开发者可以构建一个高效、稳定、易于维护的资源管理系统,确保游戏在各种环境下都能提供优秀的用户体验。 |