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 | 跨平台 | 推荐,自动转码 |
| ETC2 | Android | Android 标准 |
| ASTC | 现代移动设备 | 高质量,灵活性强 |
| PVRTC | iOS (旧) | 已过时 |
// 使用 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 格式 |
| 加载进度跳跃 | 各资源体积差异大 | 使用带权重的进度计算 |
💡 进阶提示
并行加载优化:
// Assets.load 内部已并行,但可以控制并发数 await Assets.init({ manifest: '/manifest.json', maxParallel: 6, // 最大并发请求数 });资源预热:
// 提前解析纹理,减少首次使用卡顿 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); } } }加载错误统计:
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 }); } }