Skip to content

第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.png

10.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.json

10.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
// 桌面设备:最大 4096x4096

10.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 练习题

基础练习

  1. 实现一个带进度条的加载界面

  2. 使用 Bundle 组织游戏资源

  3. 实现资源的懒加载

进阶练习

  1. 实现一个带内存限制的纹理管理器

  2. 实现资源加载重试机制

挑战练习

  1. 实现一个完整的资源管理系统,支持预加载、懒加载、卸载、内存管理

下一章预告:在第11章中,我们将深入学习滤镜与特效。


文档版本:v1.0
字数统计:约 11,000 字
代码示例:45+ 个

如有转载或 CV 的请标注本站原文地址