强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Pixi.js 游戏开发教程 / 资源加载与管理(Assets)

资源加载与管理(Assets)

概述

资源管理是任何游戏或图形应用的基础设施。Pixi.js v8 全新的 Assets 模块取代了 v7 的 Loader,提供更简洁的 API、自动缓存、Bundle 分组、进度追踪等功能。合理管理资源直接影响加载速度、内存占用和用户体验。


Assets 模块(v8)

核心特性

特性说明
统一加载接口图片、音频、JSON、字体等统一处理
自动缓存相同 URL 不重复加载
Bundle 分组按场景/关卡分组加载资源
进度追踪支持 onProgress 回调
异步/按需支持预加载和懒加载
资源卸载可手动释放不再使用的资源

Assets.load 批量加载

单个资源加载

import { Application, Assets, Sprite } from 'pixi.js';

const app = new Application();
await app.init({ width: 800, height: 600, background: '#1a1a2e' });
document.body.appendChild(app.canvas);

// 加载单个纹理
const texture = await Assets.load('/image/hero.png');
const hero = new Sprite(texture);
app.stage.addChild(hero);

批量加载

// 通过对象加载多个资源,返回带名称的映射
const textures = await Assets.load([
  '/image/hero.png',
  '/image/enemy.png',
  '/image/bullet.png',
  '/image/bg.png',
]);

console.log(textures); // { '/image/hero.png': Texture, ... }

// 使用路径引用
const heroTex = textures['/image/hero.png'];

加载 Spritesheet

// Assets 自动识别 Spritesheet JSON
const sheet = await Assets.load('/image/hero-sheet.json');
// sheet 已解析完成,可直接使用
const frame = sheet.textures['walk-0.png'];

Bundle 分组加载

将资源按场景或功能分组,实现按需加载:

定义 Bundle

// 方式一:代码中定义
Assets.addBundle('menu', {
  logo: '/image/logo.png',
  bg: '/image/menu-bg.png',
  buttonStart: '/image/btn-start.png',
  buttonSettings: '/image/btn-settings.png',
});

Assets.addBundle('level1', {
  tileset: '/image/tileset.json',
  hero: '/image/hero.json',
  enemies: '/image/enemies.json',
  music: '/audio/level1.mp3',
});

Assets.addBundle('level2', {
  tileset: '/image/tileset-l2.json',
  boss: '/image/boss.json',
  music: '/audio/level2.mp3',
});

加载 Bundle

// 加载菜单资源
const menuAssets = await Assets.loadBundle('menu');
const logo = new Sprite(menuAssets.logo);
app.stage.addChild(logo);

// 加载关卡资源(带进度)
const levelAssets = await Assets.loadBundle('level1', (progress) => {
  console.log(`加载进度: ${(progress * 100).toFixed(1)}%`);
  loadingBar.width = 300 * progress;
});

加载进度(onProgress)

进度条实现

class LoadingScreen {
  private bg: Graphics;
  private bar: Graphics;
  private text: Text;
  private maxWidth: number;

  constructor(app: Application) {
    this.maxWidth = 400;

    this.bg = new Graphics();
    this.bg.rect(0, 0, this.maxWidth, 30);
    this.bg.fill(0x333333);
    this.bg.stroke({ width: 2, color: 0x666666 });
    this.bg.position.set(200, 285);
    app.stage.addChild(this.bg);

    this.bar = new Graphics();
    this.bar.position.set(200, 285);
    app.stage.addChild(this.bar);

    this.text = new Text({
      text: '加载中... 0%',
      style: { fontSize: 18, fill: '#ffffff' },
    });
    this.text.anchor.set(0.5);
    this.text.position.set(400, 320);
    app.stage.addChild(this.text);
  }

  update(progress: number) {
    const width = this.maxWidth * progress;
    this.bar.clear();
    this.bar.rect(0, 0, width, 30);
    this.bar.fill(0x2ecc71);
    this.text.text = `加载中... ${(progress * 100).toFixed(1)}%`;
  }

  destroy() {
    this.bg.destroy();
    this.bar.destroy();
    this.text.destroy();
  }
}

// 使用
const loadingScreen = new LoadingScreen(app);

await Assets.loadBundle('level1', (progress) => {
  loadingScreen.update(progress);
});

loadingScreen.destroy(); // 加载完成后移除加载界面

资源缓存

Pixi.js 自动缓存所有已加载资源,后续 Assets.load()Assets.get() 直接返回缓存:

// 首次加载 —— 从网络获取
const tex1 = await Assets.load('/image/hero.png');

// 再次获取 —— 从缓存返回(O(1))
const tex2 = await Assets.load('/image/hero.png');
console.log(tex1 === tex2); // true

// 同步获取缓存资源
const tex3 = Assets.get('/image/hero.png'); // 必须已加载过

// 检查缓存
const isLoaded = Assets.cache.has('/image/hero.png');
console.log(isLoaded); // true

手动设置缓存

// 手动添加缓存
Assets.set('/image/custom', myTexture);

// 移除缓存
Assets.cache.remove('/image/hero.png');

// 清除所有缓存
Assets.cache.reset();

纹理格式选择

不同格式在质量、体积和兼容性上有显著差异:

格式透明通道压缩体积(相对)浏览器支持适用场景
PNG无损全部UI、需要透明的图
JPEG有损全部背景、照片
WebP有/无损95%+通用(推荐)
AVIF有/无损最小85%+现代浏览器
SVG矢量极小全部图标、简单图形

纹理压缩(GPU 压缩)

对于高性能需求场景,使用 GPU 压缩纹理:

格式平台说明
Basis跨平台推荐,自动转码
ETC2AndroidAndroid 标准
ASTC现代移动设备高质量,灵活性强
PVRTCiOS (旧)已过时
// 使用 Basis 纹理
import { extensions, BasisParser } from 'pixi.js';

extensions.add(BasisParser);

const texture = await Assets.load('/image/hero.basis');

💡 提示: 对于 Web 游戏,WebP 格式是当前最佳选择,在质量和体积间取得平衡。


资源卸载

不再使用的资源应及时释放,避免内存泄漏:

// 卸载单个资源
await Assets.unload('/image/hero.png');

// 卸载 Bundle
await Assets.unloadBundle('level1');

// 销毁纹理(从 GPU 释放)
texture.destroy(); // 仅销毁纹理
texture.destroy(true); // 同时销毁 BaseTexture

// 场景切换时批量清理
async function cleanupLevel() {
  await Assets.unloadBundle('level1');
  // 强制垃圾回收(需要浏览器支持)
  if (globalThis.gc) globalThis.gc();
}

⚠️ 注意: Assets.unload() 后,如果再次 Assets.load() 同一 URL,会重新从网络加载。确保确实不再需要时才卸载。


加载策略

预加载(Preload)

async function main() {
  await app.init({ width: 800, height: 600 });
  showLoadingScreen();
  await Assets.load(['/image/hero.png', '/image/ui-atlas.json']); // 核心资源
  hideLoadingScreen();
  showMainMenu();
}
main();

懒加载(Lazy Load)

async function enterLevel(levelId: number) {
  const sheet = await Assets.load(`/image/level${levelId}-sheet.json`);
  // 开始游戏...
}

加载错误处理

// 单个资源错误处理
try {
  const texture = await Assets.load('/image/missing.png');
} catch (error) {
  console.error('加载失败:', error);
  // 使用回退纹理
  const fallback = await Assets.load('/image/default.png');
}

// Bundle 错误处理
try {
  await Assets.loadBundle('level1');
} catch (error) {
  console.error('Bundle 加载失败:', error);
  showErrorScreen('资源加载失败,请刷新页面重试');
}

// 带重试的加载
async function loadWithRetry(url: string, maxRetries: number = 3): Promise<Texture> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await Assets.load(url);
    } catch (error) {
      console.warn(`第 ${attempt} 次加载失败: ${url}`);
      if (attempt === maxRetries) throw error;
      await new Promise(r => setTimeout(r, 1000 * attempt)); // 退避延迟
    }
  }
  throw new Error('加载失败');
}

资源清单(manifest.json)

使用 manifest 文件集中管理所有资源:

manifest 格式

{
  "bundles": [
    {
      "name": "core",
      "assets": [
        { "alias": "hero", "src": "/image/hero.png" },
        { "alias": "ui", "src": "/image/ui-atlas.json" },
        { "alias": "font", "src": "/font/game.woff2" }
      ]
    },
    {
      "name": "level1",
      "assets": [
        { "alias": "level1-tiles", "src": "/image/level1-tiles.json" },
        { "alias": "level1-music", "src": "/audio/level1.mp3" }
      ]
    },
    {
      "name": "level2",
      "assets": [
        { "alias": "level2-tiles", "src": "/image/level2-tiles.json" },
        { "alias": "level2-music", "src": "/audio/level2.mp3" }
      ]
    }
  ]
}

使用 Manifest

// 初始化 Assets 时加载 manifest
await Assets.init({
  manifest: '/manifest.json',
  basePath: '/assets',
});

// 通过 alias 加载
const hero = await Assets.load('hero');
const heroSprite = new Sprite(hero);

// 按 bundle 加载
await Assets.loadBundle('core');
await Assets.loadBundle('level1');

游戏开发场景

场景:完整的游戏资源管理流程

class ResourceManager {
  async init() {
    await Assets.init({ manifest: '/manifest.json' });
  }

  async loadLevel(levelId: number) {
    if (levelId > 1) await Assets.unloadBundle(`level${levelId - 1}`);
    return await Assets.loadBundle(`level${levelId}`, (p) => updateLoadingBar(p));
  }
}

// 启动流程
const res = new ResourceManager();
await res.init();
await Assets.loadBundle('core');
const levelAssets = await res.loadLevel(1);

⚠️ 常见问题

问题原因解决方案
加载卡住不动大文件无进度回调使用 Bundle 的 onProgress
内存持续增长资源未卸载场景切换时调用 unloadBundle
资源 404路径错误检查 basePath 和相对路径
重复加载同一资源URL 微小差异(如尾部斜杠)统一 URL 格式
Manifest 加载失败JSON 格式错误验证 manifest.json 格式
加载进度跳跃各资源体积差异大使用带权重的进度计算

💡 进阶提示

  1. 并行加载优化:

    // Assets.load 内部已并行,但可以控制并发数
    await Assets.init({
      manifest: '/manifest.json',
      maxParallel: 6, // 最大并发请求数
    });
    
  2. 资源预热:

    // 提前解析纹理,减少首次使用卡顿
    async function warmup(bundleName: string) {
      const assets = await Assets.loadBundle(bundleName);
      for (const asset of Object.values(assets)) {
        if (asset instanceof Texture) {
          // 强制上传到 GPU
          app.renderer.texture.update(asset);
        }
      }
    }
    
  3. 加载错误统计:

    const failedAssets: string[] = [];
    
    async function safeLoad(url: string) {
      try {
        return await Assets.load(url);
      } catch (e) {
        failedAssets.push(url);
        console.error(`加载失败: ${url}`);
        return null;
      }
    }
    
    // 游戏结束时上报
    function reportLoadingErrors() {
      if (failedAssets.length > 0) {
        sendAnalytics({ event: 'load_failures', assets: failedAssets });
      }
    }
    

扩展阅读


上一章:09 - 精灵表与帧动画(Spritesheet)