17 KiB
17 KiB
04. 资源管理
Extra2D 提供了统一的资源管理系统,用于加载和管理游戏中的各种资源。
资源管理器
通过 Application::instance().resources() 访问资源管理器:
auto& resources = Application::instance().resources();
支持的资源类型
| 资源类型 | 加载方法 | 说明 |
|---|---|---|
| 纹理 | loadTexture() |
图片文件 (PNG, JPG, etc.) |
| 字体 | loadFont() |
TrueType 字体文件 |
| 音频 | loadSound() / loadMusic() |
音频文件 |
纹理加载
基本用法
// 加载纹理
auto texture = resources.loadTexture("assets/images/player.png");
if (texture) {
// 创建精灵
auto sprite = Sprite::create(texture);
addChild(sprite);
}
异步加载
Extra2D 支持异步加载纹理,避免阻塞主线程:
// 同步加载(默认)
auto texture = resources.loadTexture("assets/images/player.png");
// 异步加载
auto texture = resources.loadTexture("assets/images/player.png", true);
// 使用回调函数处理异步加载完成
resources.loadTextureAsync("assets/images/player.png",
TextureFormat::Auto,
[](Ptr<Texture> texture, const std::string& path) {
if (texture) {
// 加载成功,可以安全使用
auto sprite = Sprite::create(texture);
// ...
}
});
纹理压缩格式
Extra2D 支持多种纹理压缩格式,可显著减少显存占用:
// 支持的纹理格式
enum class TextureFormat {
Auto, // 自动选择最佳格式
RGBA8, // 32位 RGBA(无压缩)
RGB8, // 24位 RGB(无压缩)
DXT1, // DXT1 压缩(适用于不透明纹理)
DXT5, // DXT5 压缩(适用于透明纹理)
ETC2, // ETC2 压缩(移动平台)
ASTC4x4, // ASTC 4x4 高质量压缩
ASTC8x8 // ASTC 8x8 高压缩率
};
// 使用压缩格式加载纹理
auto texture = resources.loadTexture("assets/images/player.png", false, TextureFormat::DXT5);
// 异步加载 + 压缩
auto texture = resources.loadTexture("assets/images/player.png", true, TextureFormat::ASTC4x4);
格式选择建议:
| 格式 | 压缩比 | 质量 | 适用场景 |
|---|---|---|---|
| RGBA8 | 1:1 | 最高 | 小图标、需要最高质量 |
| DXT1 | 1:8 | 高 | 不透明纹理、大背景图 |
| DXT5 | 1:4 | 高 | 透明纹理、角色精灵 |
| ETC2 | 1:4 | 高 | 移动设备、跨平台 |
| ASTC4x4 | 1:4 | 很高 | 高质量透明纹理 |
| ASTC8x8 | 1:16 | 中等 | 大纹理、远景贴图 |
纹理缓存
资源管理器会自动缓存已加载的纹理,多次加载同一文件会返回缓存的实例:
// 第一次加载 - 从文件读取
auto tex1 = resources.loadTexture("assets/image.png");
// 第二次加载 - 返回缓存
auto tex2 = resources.loadTexture("assets/image.png");
// tex1 和 tex2 指向同一个纹理对象
LRU 缓存机制
Extra2D 使用 LRU (Least Recently Used) 算法管理纹理缓存,自动清理最久未使用的纹理:
// 配置纹理缓存参数
auto& resources = Application::instance().resources();
// 设置缓存参数:最大缓存大小(字节)、最大纹理数量、自动清理间隔(秒)
resources.setTextureCache(
128 * 1024 * 1024, // 128MB 最大缓存
512, // 最多 512 个纹理
30.0f // 每 30 秒检查一次
);
// 在主循环中更新资源管理器(用于自动清理)
void GameScene::update(float dt) {
// 这会触发缓存清理检查
resources.update(dt);
}
缓存统计
// 获取缓存使用情况
size_t memoryUsage = resources.getTextureCacheMemoryUsage(); // 当前缓存大小(字节)
float hitRate = resources.getTextureCacheHitRate(); // 缓存命中率 (0.0 - 1.0)
size_t cacheSize = resources.getTextureCacheSize(); // 缓存中的纹理数量
// 打印详细统计信息
resources.printTextureCacheStats();
// 输出示例:
// [INFO] 纹理缓存统计:
// [INFO] 缓存纹理数: 45/512
// [INFO] 缓存大小: 32 / 128 MB
// [INFO] 缓存命中: 1024
// [INFO] 缓存未命中: 56
// [INFO] 命中率: 94.8%
自动清理策略
LRU 缓存会自动执行以下清理策略:
- 容量限制:当缓存超过
maxCacheSize或maxTextureCount时,自动驱逐最久未使用的纹理 - 定时清理:每
unloadInterval秒检查一次,如果缓存超过 80%,清理到 50% - 访问更新:每次访问纹理时,自动将其移到 LRU 链表头部(标记为最近使用)
// 手动清理缓存
resources.clearTextureCache(); // 清空所有纹理缓存
resources.clearAllCaches(); // 清空所有资源缓存(纹理、字体、音效)
// 手动卸载特定纹理
resources.unloadTexture("assets/images/old_texture.png");
缓存配置建议
| 平台 | 最大缓存大小 | 最大纹理数 | 清理间隔 | 说明 |
|---|---|---|---|---|
| Switch 掌机模式 | 64-128 MB | 256-512 | 30s | 内存有限,保守设置 |
| Switch 主机模式 | 128-256 MB | 512-1024 | 30s | 内存充足,可以更大 |
| PC (MinGW) | 256-512 MB | 1024+ | 60s | 内存充足,可以更大 |
// 根据平台设置不同的缓存策略
void setupCache() {
auto& resources = Application::instance().resources();
#ifdef __SWITCH__
// Switch 平台使用保守设置
resources.setTextureCache(64 * 1024 * 1024, 256, 30.0f);
#else
// PC 平台可以使用更大的缓存
resources.setTextureCache(256 * 1024 * 1024, 1024, 60.0f);
#endif
}
纹理图集(Texture Atlas)
Extra2D 自动使用纹理图集优化渲染性能:
// 获取纹理图集管理器
auto& atlasManager = resources.getTextureAtlasManager();
// 将多个纹理打包到图集(自动进行)
// 渲染时,相同图集的精灵会自动批处理
// 手动创建图集(高级用法)
auto atlas = atlasManager.createAtlas("ui_atlas", 2048, 2048);
atlas->addTexture("button", buttonTexture);
atlas->addTexture("icon", iconTexture);
atlas->pack(); // 执行打包
精灵帧(SpriteFrame)
什么是精灵帧?
精灵帧是纹理图集中的一个矩形区域,用于从单个大纹理中提取小图像。使用精灵帧可以:
- 减少纹理切换,提高渲染性能
- 方便管理动画帧和 UI 元素
- 支持从 JSON 文件加载精灵帧数据
完整示例:资源加载器
参考 examples/flappy_bird/ResLoader.h/cpp:
// ResLoader.h
#pragma once
#include <extra2d/extra2d.h>
#include <map>
#include <string>
namespace flappybird {
/**
* @brief 音频类型枚举
*/
enum class MusicType {
Click, // 按键声音
Hit, // 小鸟死亡声音
Fly, // 小鸟飞翔声音
Point, // 得分声音
Swoosh // 转场声音
};
/**
* @brief 资源加载器类
* 管理纹理图集、精灵帧和音频资源的加载
*/
class ResLoader {
public:
static void init();
static extra2d::Ptr<extra2d::SpriteFrame> getKeyFrame(const std::string& name);
static void playMusic(MusicType type);
private:
struct ImageInfo {
float width, height, x, y;
};
static extra2d::Ptr<extra2d::Texture> atlasTexture_;
static std::map<std::string, ImageInfo> imageMap_;
static std::map<MusicType, extra2d::Ptr<extra2d::Sound>> soundMap_;
};
} // namespace flappybird
// ResLoader.cpp
#include "ResLoader.h"
#include <json/json.hpp>
namespace flappybird {
extra2d::Ptr<extra2d::Texture> ResLoader::atlasTexture_;
std::map<std::string, ResLoader::ImageInfo> ResLoader::imageMap_;
std::map<MusicType, extra2d::Ptr<extra2d::Sound>> ResLoader::soundMap_;
void ResLoader::init() {
auto &resources = extra2d::Application::instance().resources();
// 加载图集纹理
atlasTexture_ = resources.loadTexture("assets/images/atlas.png");
if (!atlasTexture_) {
E2D_LOG_ERROR("无法加载图集纹理 atlas.png");
return;
}
// 加载 JSON 文件
std::string jsonContent = resources.loadJsonFile("assets/images/atlas.json");
if (jsonContent.empty()) {
E2D_LOG_ERROR("无法加载 atlas.json 文件");
return;
}
// 解析 JSON 图集数据
try {
nlohmann::json jsonData = nlohmann::json::parse(jsonContent);
for (const auto &sprite : jsonData["sprites"]) {
std::string name = sprite["name"];
float x = sprite["x"];
float y = sprite["y"];
float width = sprite["width"];
float height = sprite["height"];
ImageInfo info = {width, height, x, y};
imageMap_[name] = info;
}
E2D_LOG_INFO("成功加载 {} 个精灵帧", imageMap_.size());
} catch (const std::exception &e) {
E2D_LOG_ERROR("解析 atlas.json 失败: {}", e.what());
return;
}
// 加载音效
soundMap_[MusicType::Click] = resources.loadSound("assets/sound/click.wav");
soundMap_[MusicType::Hit] = resources.loadSound("assets/sound/hit.wav");
soundMap_[MusicType::Fly] = resources.loadSound("assets/sound/fly.wav");
soundMap_[MusicType::Point] = resources.loadSound("assets/sound/point.wav");
soundMap_[MusicType::Swoosh] = resources.loadSound("assets/sound/swoosh.wav");
E2D_LOG_INFO("资源加载完成");
}
extra2d::Ptr<extra2d::SpriteFrame>
ResLoader::getKeyFrame(const std::string &name) {
auto it = imageMap_.find(name);
if (it == imageMap_.end()) {
E2D_LOG_WARN("找不到精灵帧: %s", name.c_str());
return nullptr;
}
const ImageInfo &info = it->second;
return extra2d::makePtr<extra2d::SpriteFrame>(
atlasTexture_, extra2d::Rect(info.x, info.y, info.width, info.height));
}
void ResLoader::playMusic(MusicType type) {
auto it = soundMap_.find(type);
if (it != soundMap_.end() && it->second) {
it->second->play();
}
}
} // namespace flappybird
使用精灵帧创建精灵
// 参考 examples/flappy_bird/GameScene.cpp
void GameScene::onEnter() {
BaseScene::onEnter();
// 从图集获取精灵帧
auto bgFrame = ResLoader::getKeyFrame("bg_day");
if (bgFrame) {
// 使用精灵帧创建精灵
auto background = extra2d::Sprite::create(
bgFrame->getTexture(),
bgFrame->getRect());
background->setAnchor(extra2d::Vec2(0.0f, 0.0f));
background->setPosition(extra2d::Vec2(0.0f, 0.0f));
addChild(background);
}
// 创建按钮
auto buttonFrame = ResLoader::getKeyFrame("button_play");
if (buttonFrame) {
auto button = extra2d::Button::create();
button->setBackgroundImage(buttonFrame->getTexture(), buttonFrame->getRect());
button->setAnchor(extra2d::Vec2(0.5f, 0.5f));
button->setPosition(extra2d::Vec2(screenWidth / 2.0f, 300.0f));
button->setOnClick([]() {
// 处理点击
});
addChild(button);
}
}
JSON 图集格式
{
"sprites": [
{ "name": "bg_day", "x": 0, "y": 0, "width": 288, "height": 512 },
{ "name": "bird0_0", "x": 288, "y": 0, "width": 34, "height": 24 },
{ "name": "button_play", "x": 322, "y": 0, "width": 116, "height": 70 }
]
}
字体加载
基本用法
// 加载字体(指定字号)
auto font24 = resources.loadFont("assets/font.ttf", 24, true);
// 创建文本
auto text = Text::create("Hello World", font24);
addChild(text);
字体后备
支持设置后备字体,当主字体缺少某些字符时自动使用后备字体:
// 加载主字体和后备字体
auto mainFont = resources.loadFont("assets/main.ttf", 24, true);
auto fallbackFont = resources.loadFont("assets/fallback.ttf", 24, true);
// 设置后备字体
mainFont->setFallback(fallbackFont);
资源路径
路径格式
// 相对路径(相对于工作目录)
auto tex = resources.loadTexture("assets/images/player.png");
// Switch 平台使用 romfs
auto tex = resources.loadTexture("romfs:/images/player.png");
// SD 卡路径
auto tex = resources.loadTexture("sdmc:/switch/game/images/player.png");
路径辅助函数
// 获取平台特定的资源路径
std::string path = ResourceManager::getPlatformPath("images/player.png");
// Windows: "assets/images/player.png"
// Switch: "romfs:/images/player.png"
资源释放
自动释放
资源使用智能指针管理,当没有引用时会自动释放:
{
auto tex = resources.loadTexture("assets/temp.png");
// 使用纹理...
} // 超出作用域,如果没有其他引用,纹理自动释放
手动清理缓存
// 清理未使用的资源(清理字体和音效缓存中已失效的弱引用)
resources.purgeUnused();
// 清空特定类型的缓存
resources.clearTextureCache(); // 清空纹理缓存
resources.clearFontCache(); // 清空字体缓存
resources.clearSoundCache(); // 清空音效缓存
// 清空所有缓存(谨慎使用)
resources.clearAllCaches();
// 检查是否有正在进行的异步加载
if (resources.hasPendingAsyncLoads()) {
// 等待所有异步加载完成
resources.waitForAsyncLoads();
}
内存管理
内存池(内部自动管理)
Extra2D 使用内存池优化小对象分配,无需用户干预:
// 内存池自动管理以下对象:
// - 场景节点
// - 渲染命令
// - 碰撞形状
// - 事件对象
// 用户代码无需特殊处理,正常使用即可
auto node = Node::create(); // 自动使用内存池
auto sprite = Sprite::create(texture); // 自动使用内存池
批量更新(内部自动进行)
Extra2D 自动批量更新节点变换,优化性能:
// 以下操作会自动批处理:
// - 节点变换更新
// - 渲染命令提交
// - 纹理绑定
// 用户代码无需特殊处理
for (int i = 0; i < 1000; ++i) {
auto sprite = Sprite::create(texture);
sprite->setPosition(i * 10, 100);
addChild(sprite); // 变换更新会自动批处理
}
渲染批处理
自动批处理
Extra2D 自动将渲染命令批处理以优化性能:
// 以下情况会自动批处理:
// 1. 相同纹理的精灵
// 2. 相同图层的节点
// 3. 相同混合模式
// 示例:1000 个相同纹理的精灵会自动批处理为少量 draw call
for (int i = 0; i < 1000; ++i) {
auto sprite = Sprite::create(texture);
addChild(sprite);
}
手动控制渲染顺序
// 设置节点的渲染层级(z-order)
sprite->setZOrder(10); // 值越大,渲染越靠前
// 同层级的节点会自动批处理
完整示例
参考 examples/push_box/StartScene.cpp:
void StartScene::onEnter() {
Scene::onEnter();
auto& app = Application::instance();
auto& resources = app.resources();
// 加载背景纹理(异步 + 压缩)
auto bgTex = resources.loadTexture("assets/images/start.jpg", true, TextureFormat::DXT1);
if (bgTex) {
auto background = Sprite::create(bgTex);
background->setAnchor(0.0f, 0.0f);
addChild(background);
}
// 加载音效图标纹理(异步 + DXT5 压缩支持透明)
auto soundOn = resources.loadTexture("assets/images/soundon.png", true, TextureFormat::DXT5);
auto soundOff = resources.loadTexture("assets/images/soundoff.png", true, TextureFormat::DXT5);
if (soundOn && soundOff) {
soundIcon_ = Sprite::create(g_SoundOpen ? soundOn : soundOff);
addChild(soundIcon_);
}
// 加载字体
font_ = resources.loadFont("assets/font.ttf", 28, true);
// 创建按钮...
}
性能优化建议
纹理优化
- 使用纹理压缩 - 对大型纹理使用 DXT/ASTC 压缩减少显存占用
- 使用纹理图集 - 将多个小纹理打包到图集,减少 draw call
- 异步加载大纹理 - 避免在主线程加载大型资源造成卡顿
- 合理设置纹理尺寸 - 避免使用过大的纹理(建议最大 2048x2048)
- 配置合适的缓存大小 - 根据平台内存设置合理的 LRU 缓存参数
- 监控缓存命中率 - 使用
printTextureCacheStats()检查缓存效率
资源加载策略
// 场景预加载
void GameScene::onEnter() {
auto& resources = Application::instance().resources();
// 预加载关键资源
resources.loadTexture("assets/textures/player.png", true);
resources.loadTexture("assets/textures/enemy.png", true);
resources.loadFont("assets/fonts/main.ttf", 24, true);
}
// 异步加载非关键资源
void GameScene::loadOptionalResources() {
resources.loadTextureAsync("assets/textures/background.jpg",
TextureFormat::DXT1,
[](Ptr<Texture> tex, const std::string& path) {
if (tex) {
// 加载完成后创建背景
}
});
}
最佳实践
- 预加载资源 - 在场景
onEnter()中加载所需资源 - 检查资源有效性 - 始终检查加载结果是否为 nullptr
- 复用资源 - 多次使用同一资源时保存指针,避免重复加载
- 合理设置字号 - 字体加载时会生成对应字号的图集
- 使用异步加载 - 对大型资源使用异步加载避免卡顿
- 选择合适的压缩格式 - 根据纹理用途选择最佳压缩格式
- 利用自动批处理 - 相同纹理的精灵会自动批处理,无需手动优化
- 配置 LRU 缓存 - 根据平台内存配置合适的缓存大小
- 定期监控缓存 - 在开发阶段定期检查缓存命中率和内存使用
- 在主循环更新资源管理器 - 确保调用
resources.update(dt)以触发自动清理