第10章:纹理管理与资源加载
10.1 章节概述
资源加载和纹理管理是游戏和应用开发中的核心问题。PixiJS 提供了强大的 Assets 系统来处理各种资源的加载、缓存和管理。
本章将深入讲解:
- Assets 系统:加载、缓存、卸载
- 资源类型:图片、纹理图集、字体、音频
- 加载策略:预加载、懒加载、优先级
- 进度监控:加载进度、错误处理
- 纹理管理:缓存、复用、内存优化
- 最佳实践:资源组织、性能优化
10.2 Assets 系统基础
10.2.1 Assets 系统架构
Assets 系统架构
┌─────────────────────────────────────────────────────────────┐
│ PIXI.Assets │
├─────────────────────────────────────────────────────────────┤
│ - 统一的资源加载入口 │
│ - 自动识别资源类型 │
│ - 内置缓存机制 │
│ - 支持 Bundle 分组 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Resolver │
├─────────────────────────────────────────────────────────────┤
│ - 解析资源路径 │
│ - 处理别名 │
│ - 选择最佳格式 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Loader │
├─────────────────────────────────────────────────────────────┤
│ - 实际加载资源 │
│ - 解析资源数据 │
│ - 创建 PixiJS 对象 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Cache │
├─────────────────────────────────────────────────────────────┤
│ - 存储已加载资源 │
│ - 防止重复加载 │
│ - 管理资源生命周期 │
└─────────────────────────────────────────────────────────────┘10.2.2 基本加载
typescript
/**
* 基本资源加载
*/
// 加载单个资源
const texture = await PIXI.Assets.load('image.png');
const sprite = new PIXI.Sprite(texture);
// 加载多个资源
const textures = await PIXI.Assets.load(['image1.png', 'image2.png', 'image3.png']);
// textures 是一个对象: { 'image1.png': Texture, 'image2.png': Texture, ... }
// 使用别名
PIXI.Assets.add('hero', 'assets/characters/hero.png');
const heroTexture = await PIXI.Assets.load('hero');
// 加载并使用
const sprite = PIXI.Sprite.from('image.png'); // 自动加载10.2.3 资源类型
typescript
/**
* 支持的资源类型
*/
// 图片
const texture = await PIXI.Assets.load('image.png');
const texture = await PIXI.Assets.load('image.jpg');
const texture = await PIXI.Assets.load('image.webp');
// 纹理图集(Spritesheet)
const sheet = await PIXI.Assets.load('spritesheet.json');
// sheet.textures: { 'frame1': Texture, 'frame2': Texture, ... }
// sheet.animations: { 'walk': [Texture, ...], 'run': [Texture, ...] }
// 位图字体
await PIXI.Assets.load('font.fnt');
const bitmapText = new PIXI.BitmapText('Hello', { fontName: 'MyFont' });
// JSON 数据
const data = await PIXI.Assets.load('data.json');
// 文本文件
const text = await PIXI.Assets.load('file.txt');
// SVG
const texture = await PIXI.Assets.load('icon.svg');
// 视频
const texture = await PIXI.Assets.load('video.mp4');10.3 资源配置
10.3.1 添加资源
typescript
/**
* 添加资源到 Assets
*/
// 单个资源
PIXI.Assets.add('hero', 'assets/hero.png');
// 带选项
PIXI.Assets.add({
alias: 'hero',
src: 'assets/hero.png',
data: {
scaleMode: PIXI.SCALE_MODES.NEAREST
}
});
// 多个资源
PIXI.Assets.add([
{ alias: 'hero', src: 'assets/hero.png' },
{ alias: 'enemy', src: 'assets/enemy.png' },
{ alias: 'background', src: 'assets/bg.png' }
]);
// 多分辨率资源
PIXI.Assets.add({
alias: 'hero',
src: {
default: 'assets/hero.png',
low: 'assets/hero@0.5x.png',
high: 'assets/hero@2x.png'
}
});10.3.2 Bundle 资源组
typescript
/**
* Bundle 资源分组
*/
// 定义 Bundle
PIXI.Assets.addBundle('game-assets', {
hero: 'assets/hero.png',
enemy: 'assets/enemy.png',
background: 'assets/bg.png',
spritesheet: 'assets/sprites.json'
});
PIXI.Assets.addBundle('ui-assets', {
button: 'assets/ui/button.png',
panel: 'assets/ui/panel.png',
font: 'assets/fonts/game.fnt'
});
// 加载 Bundle
const gameAssets = await PIXI.Assets.loadBundle('game-assets');
const uiAssets = await PIXI.Assets.loadBundle('ui-assets');
// 加载多个 Bundle
const assets = await PIXI.Assets.loadBundle(['game-assets', 'ui-assets']);
// 后台加载 Bundle
PIXI.Assets.backgroundLoadBundle('next-level');10.3.3 Manifest 清单
typescript
/**
* 使用 Manifest 配置
*/
// manifest.json
const manifest = {
bundles: [
{
name: 'load-screen',
assets: [
{ alias: 'logo', src: 'assets/logo.png' },
{ alias: 'loading-bar', src: 'assets/loading-bar.png' }
]
},
{
name: 'game',
assets: [
{ alias: 'hero', src: 'assets/hero.png' },
{ alias: 'tileset', src: 'assets/tileset.json' },
{ alias: 'music', src: 'assets/music.mp3' }
]
}
]
};
// 初始化
await PIXI.Assets.init({ manifest });
// 或从文件加载
await PIXI.Assets.init({ manifest: 'assets/manifest.json' });
// 加载 Bundle
const loadScreenAssets = await PIXI.Assets.loadBundle('load-screen');
const gameAssets = await PIXI.Assets.loadBundle('game');10.4 加载策略
10.4.1 预加载
typescript
/**
* 预加载策略
*/
// 显示加载界面
const loadingScreen = new LoadingScreen();
app.stage.addChild(loadingScreen);
// 预加载所有资源
const assets = await PIXI.Assets.loadBundle('game', (progress) => {
loadingScreen.setProgress(progress);
});
// 移除加载界面
app.stage.removeChild(loadingScreen);
loadingScreen.destroy();
// 开始游戏
startGame(assets);10.4.2 懒加载
typescript
/**
* 懒加载策略
*/
// 只在需要时加载
async function loadLevel(levelId: number) {
// 加载关卡资源
const levelAssets = await PIXI.Assets.loadBundle(`level-${levelId}`);
return levelAssets;
}
// 使用
const level1Assets = await loadLevel(1);
// ... 玩完第一关
const level2Assets = await loadLevel(2);10.4.3 后台加载
typescript
/**
* 后台加载
*/
// 当前关卡加载完成后,后台预加载下一关
async function loadCurrentLevel(levelId: number) {
// 加载当前关卡
const assets = await PIXI.Assets.loadBundle(`level-${levelId}`);
// 后台加载下一关
PIXI.Assets.backgroundLoadBundle(`level-${levelId + 1}`);
return assets;
}
// 后台加载不会阻塞主线程
// 当需要时,资源可能已经加载完成10.4.4 优先级加载
typescript
/**
* 优先级加载
*/
class PriorityLoader {
private queue: { url: string; priority: number; resolve: Function }[] = [];
private loading = false;
async load(url: string, priority: number = 0): Promise<any> {
return new Promise((resolve) => {
this.queue.push({ url, priority, resolve });
this.queue.sort((a, b) => b.priority - a.priority);
this.processQueue();
});
}
private async processQueue() {
if (this.loading || this.queue.length === 0) return;
this.loading = true;
const item = this.queue.shift()!;
const asset = await PIXI.Assets.load(item.url);
item.resolve(asset);
this.loading = false;
this.processQueue();
}
}
// 使用
const loader = new PriorityLoader();
loader.load('critical.png', 10); // 高优先级
loader.load('optional.png', 1); // 低优先级10.5 进度监控
10.5.1 加载进度
typescript
/**
* 加载进度监控
*/
// 单个资源进度
const texture = await PIXI.Assets.load('large-image.png', (progress) => {
console.log(`加载进度: ${Math.round(progress * 100)}%`);
});
// Bundle 进度
const assets = await PIXI.Assets.loadBundle('game', (progress) => {
console.log(`Bundle 加载进度: ${Math.round(progress * 100)}%`);
updateProgressBar(progress);
});
// 多个资源进度
const urls = ['a.png', 'b.png', 'c.png', 'd.png', 'e.png'];
const assets = await PIXI.Assets.load(urls, (progress) => {
console.log(`总进度: ${Math.round(progress * 100)}%`);
});10.5.2 加载界面
typescript
/**
* 加载界面实现
*/
class LoadingScreen extends PIXI.Container {
private background: PIXI.Graphics;
private progressBar: PIXI.Graphics;
private progressText: PIXI.Text;
private logo: PIXI.Sprite | null = null;
constructor(width: number, height: number) {
super();
// 背景
this.background = new PIXI.Graphics();
this.background.beginFill(0x1a1a2e);
this.background.drawRect(0, 0, width, height);
this.background.endFill();
// 进度条背景
const barBg = new PIXI.Graphics();
barBg.beginFill(0x333333);
barBg.drawRoundedRect(width / 2 - 150, height / 2, 300, 20, 10);
barBg.endFill();
// 进度条
this.progressBar = new PIXI.Graphics();
// 进度文本
this.progressText = new PIXI.Text('0%', {
fontFamily: 'Arial',
fontSize: 24,
fill: 0xFFFFFF
});
this.progressText.anchor.set(0.5);
this.progressText.position.set(width / 2, height / 2 + 50);
this.addChild(this.background, barBg, this.progressBar, this.progressText);
}
setProgress(progress: number) {
const width = 300 * progress;
this.progressBar.clear();
this.progressBar.beginFill(0x4CAF50);
this.progressBar.drawRoundedRect(
this.background.width / 2 - 150,
this.background.height / 2,
width,
20,
10
);
this.progressBar.endFill();
this.progressText.text = `${Math.round(progress * 100)}%`;
}
async setLogo(url: string) {
const texture = await PIXI.Assets.load(url);
this.logo = new PIXI.Sprite(texture);
this.logo.anchor.set(0.5);
this.logo.position.set(
this.background.width / 2,
this.background.height / 2 - 100
);
this.addChild(this.logo);
}
}
// 使用
const loadingScreen = new LoadingScreen(800, 600);
app.stage.addChild(loadingScreen);
await loadingScreen.setLogo('logo.png');
const assets = await PIXI.Assets.loadBundle('game', (progress) => {
loadingScreen.setProgress(progress);
});
app.stage.removeChild(loadingScreen);10.5.3 错误处理
typescript
/**
* 加载错误处理
*/
// try-catch
try {
const texture = await PIXI.Assets.load('missing.png');
} catch (error) {
console.error('加载失败:', error);
// 使用备用资源
const fallbackTexture = PIXI.Texture.WHITE;
}
// 批量加载错误处理
async function loadWithFallback(urls: string[], fallback: string) {
const results: Record<string, PIXI.Texture> = {};
for (const url of urls) {
try {
results[url] = await PIXI.Assets.load(url);
} catch (error) {
console.warn(`加载 ${url} 失败,使用备用资源`);
results[url] = await PIXI.Assets.load(fallback);
}
}
return results;
}
// 重试机制
async function loadWithRetry(url: string, maxRetries: number = 3): Promise<any> {
let lastError: Error | null = null;
for (let i = 0; i < maxRetries; i++) {
try {
return await PIXI.Assets.load(url);
} catch (error) {
lastError = error as Error;
console.warn(`加载失败,重试 ${i + 1}/${maxRetries}`);
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
throw lastError;
}10.6 纹理管理
10.6.1 纹理缓存
typescript
/**
* 纹理缓存
*/
// Assets 自动缓存
const texture1 = await PIXI.Assets.load('image.png');
const texture2 = await PIXI.Assets.load('image.png');
console.log(texture1 === texture2); // true
// 检查缓存
const cached = PIXI.Assets.cache.has('image.png');
// 从缓存获取
const texture = PIXI.Assets.cache.get('image.png');
// 手动添加到缓存
PIXI.Assets.cache.set('custom-key', texture);
// 从缓存移除
PIXI.Assets.cache.remove('image.png');
// 清空缓存
PIXI.Assets.cache.reset();10.6.2 卸载资源
typescript
/**
* 卸载资源
*/
// 卸载单个资源
await PIXI.Assets.unload('image.png');
// 卸载多个资源
await PIXI.Assets.unload(['image1.png', 'image2.png']);
// 卸载 Bundle
await PIXI.Assets.unloadBundle('game-assets');
// 完全清理
function cleanupResources() {
// 卸载所有资源
PIXI.Assets.cache.reset();
// 清理纹理缓存
PIXI.utils.clearTextureCache();
// 清理 BaseTexture
// 注意:这会影响所有使用这些纹理的对象
}10.6.3 内存管理
typescript
/**
* 内存管理
*/
// 估算纹理内存使用
function estimateTextureMemory(texture: PIXI.Texture): number {
const baseTexture = texture.baseTexture;
const bytesPerPixel = 4; // RGBA
return baseTexture.width * baseTexture.height * bytesPerPixel;
}
// 纹理管理器
class TextureManager {
private textures: Map<string, PIXI.Texture> = new Map();
private memoryLimit: number;
private currentMemory: number = 0;
constructor(memoryLimitMB: number = 100) {
this.memoryLimit = memoryLimitMB * 1024 * 1024;
}
async load(key: string, url: string): Promise<PIXI.Texture> {
if (this.textures.has(key)) {
return this.textures.get(key)!;
}
const texture = await PIXI.Assets.load(url);
const memory = estimateTextureMemory(texture);
// 检查内存限制
while (this.currentMemory + memory > this.memoryLimit && this.textures.size > 0) {
this.unloadOldest();
}
this.textures.set(key, texture);
this.currentMemory += memory;
return texture;
}
unload(key: string) {
const texture = this.textures.get(key);
if (texture) {
this.currentMemory -= estimateTextureMemory(texture);
texture.destroy(true);
this.textures.delete(key);
}
}
private unloadOldest() {
const firstKey = this.textures.keys().next().value;
if (firstKey) {
this.unload(firstKey);
}
}
getMemoryUsage(): { used: number; limit: number } {
return {
used: this.currentMemory,
limit: this.memoryLimit
};
}
}10.7 高级功能
10.7.1 自定义加载器
typescript
/**
* 自定义加载器
*/
// 自定义加载器
const customLoader = {
extension: {
type: PIXI.ExtensionType.LoadParser,
priority: PIXI.LoaderParserPriority.High
},
test(url: string): boolean {
return url.endsWith('.custom');
},
async load(url: string): Promise<any> {
const response = await fetch(url);
const data = await response.json();
// 自定义处理
return processCustomData(data);
}
};
// 注册加载器
PIXI.extensions.add(customLoader);
// 使用
const customData = await PIXI.Assets.load('data.custom');10.7.2 资源解析器
typescript
/**
* 资源解析器
*/
// 自定义解析器
const customResolver = {
extension: {
type: PIXI.ExtensionType.ResolveParser,
priority: PIXI.ResolverParserPriority.High
},
test(value: string): boolean {
return value.startsWith('custom://');
},
parse(value: string): PIXI.ResolvedAsset {
const path = value.replace('custom://', '');
return {
src: `https://cdn.example.com/assets/${path}`,
format: 'png'
};
}
};
// 注册解析器
PIXI.extensions.add(customResolver);
// 使用
const texture = await PIXI.Assets.load('custom://hero.png');
// 实际加载: https://cdn.example.com/assets/hero.png10.7.3 多分辨率支持
typescript
/**
* 多分辨率支持
*/
// 配置分辨率
PIXI.Assets.resolver.prefer({
resolution: window.devicePixelRatio,
format: 'webp'
});
// 添加多分辨率资源
PIXI.Assets.add({
alias: 'hero',
src: {
default: 'hero.png',
low: 'hero@0.5x.png',
high: 'hero@2x.png'
},
data: {
resolution: {
default: 1,
low: 0.5,
high: 2
}
}
});
// 自动选择最佳分辨率
const texture = await PIXI.Assets.load('hero');10.8 最佳实践
10.8.1 资源组织
推荐的资源目录结构
assets/
├── manifest.json # 资源清单
├── images/
│ ├── characters/
│ │ ├── hero.png
│ │ └── enemy.png
│ ├── backgrounds/
│ │ └── sky.png
│ └── ui/
│ ├── button.png
│ └── panel.png
├── spritesheets/
│ ├── characters.json
│ └── effects.json
├── fonts/
│ ├── game.fnt
│ └── game.png
├── audio/
│ ├── music/
│ └── sfx/
└── data/
├── levels/
└── config.json10.8.2 加载最佳实践
typescript
/**
* 加载最佳实践
*/
// 1. 使用 Manifest
await PIXI.Assets.init({ manifest: 'assets/manifest.json' });
// 2. 分阶段加载
// 第一阶段:加载界面资源
await PIXI.Assets.loadBundle('loading-screen');
// 第二阶段:核心资源
await PIXI.Assets.loadBundle('core');
// 第三阶段:后台加载其他资源
PIXI.Assets.backgroundLoadBundle('level-1');
// 3. 使用纹理图集
// 减少 HTTP 请求和 Draw Call
// 4. 压缩资源
// 使用 WebP、压缩纹理等
// 5. 懒加载非关键资源
// 只在需要时加载
// 6. 实现资源卸载
// 关卡切换时卸载不需要的资源10.8.3 性能优化
typescript
/**
* 资源加载性能优化
*/
// 1. 并行加载
const [texture1, texture2, texture3] = await Promise.all([
PIXI.Assets.load('image1.png'),
PIXI.Assets.load('image2.png'),
PIXI.Assets.load('image3.png')
]);
// 2. 使用 CDN
PIXI.Assets.resolver.basePath = 'https://cdn.example.com/assets/';
// 3. 启用缓存
// Assets 默认启用缓存
// 4. 使用合适的图片格式
// - PNG: 需要透明度
// - JPEG: 照片、背景
// - WebP: 现代浏览器,更小体积
// 5. 压缩纹理
// 使用 Basis Universal 或平台特定格式
// 6. 限制纹理尺寸
// 移动设备:最大 2048x2048
// 桌面设备:最大 4096x409610.9 本章小结
核心概念
| 概念 | 说明 |
|---|---|
| PIXI.Assets | 统一的资源加载系统 |
| Bundle | 资源分组 |
| Manifest | 资源清单配置 |
| Cache | 资源缓存 |
| Resolver | 资源路径解析 |
| backgroundLoad | 后台加载 |
关键代码
typescript
// 初始化
await PIXI.Assets.init({ manifest: 'manifest.json' });
// 加载资源
const texture = await PIXI.Assets.load('image.png');
// 加载 Bundle
const assets = await PIXI.Assets.loadBundle('game', (progress) => {
console.log(progress);
});
// 后台加载
PIXI.Assets.backgroundLoadBundle('next-level');
// 卸载资源
await PIXI.Assets.unload('image.png');
await PIXI.Assets.unloadBundle('game');
// 缓存操作
PIXI.Assets.cache.has('key');
PIXI.Assets.cache.get('key');
PIXI.Assets.cache.remove('key');10.10 练习题
基础练习
实现一个带进度条的加载界面
使用 Bundle 组织游戏资源
实现资源的懒加载
进阶练习
实现一个带内存限制的纹理管理器
实现资源加载重试机制
挑战练习
- 实现一个完整的资源管理系统,支持预加载、懒加载、卸载、内存管理
下一章预告:在第11章中,我们将深入学习滤镜与特效。
文档版本:v1.0
字数统计:约 11,000 字
代码示例:45+ 个
