Extra2D/docs/API_Tutorial/04_Resource_Management.md

17 KiB
Raw Blame History

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 缓存会自动执行以下清理策略:

  1. 容量限制:当缓存超过 maxCacheSizemaxTextureCount 时,自动驱逐最久未使用的纹理
  2. 定时清理:每 unloadInterval 秒检查一次,如果缓存超过 80%,清理到 50%
  3. 访问更新:每次访问纹理时,自动将其移到 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);
    
    // 创建按钮...
}

性能优化建议

纹理优化

  1. 使用纹理压缩 - 对大型纹理使用 DXT/ASTC 压缩减少显存占用
  2. 使用纹理图集 - 将多个小纹理打包到图集,减少 draw call
  3. 异步加载大纹理 - 避免在主线程加载大型资源造成卡顿
  4. 合理设置纹理尺寸 - 避免使用过大的纹理(建议最大 2048x2048
  5. 配置合适的缓存大小 - 根据平台内存设置合理的 LRU 缓存参数
  6. 监控缓存命中率 - 使用 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) {
                // 加载完成后创建背景
            }
        });
}

最佳实践

  1. 预加载资源 - 在场景 onEnter() 中加载所需资源
  2. 检查资源有效性 - 始终检查加载结果是否为 nullptr
  3. 复用资源 - 多次使用同一资源时保存指针,避免重复加载
  4. 合理设置字号 - 字体加载时会生成对应字号的图集
  5. 使用异步加载 - 对大型资源使用异步加载避免卡顿
  6. 选择合适的压缩格式 - 根据纹理用途选择最佳压缩格式
  7. 利用自动批处理 - 相同纹理的精灵会自动批处理,无需手动优化
  8. 配置 LRU 缓存 - 根据平台内存配置合适的缓存大小
  9. 定期监控缓存 - 在开发阶段定期检查缓存命中率和内存使用
  10. 在主循环更新资源管理器 - 确保调用 resources.update(dt) 以触发自动清理

下一步