2026-02-11 19:40:26 +08:00
|
|
|
|
# 04. 资源管理
|
|
|
|
|
|
|
|
|
|
|
|
Extra2D 提供了统一的资源管理系统,用于加载和管理游戏中的各种资源。
|
|
|
|
|
|
|
|
|
|
|
|
## 资源管理器
|
|
|
|
|
|
|
|
|
|
|
|
通过 `Application::instance().resources()` 访问资源管理器:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
auto& resources = Application::instance().resources();
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 支持的资源类型
|
|
|
|
|
|
|
|
|
|
|
|
| 资源类型 | 加载方法 | 说明 |
|
|
|
|
|
|
|---------|---------|------|
|
|
|
|
|
|
| 纹理 | `loadTexture()` | 图片文件 (PNG, JPG, etc.) |
|
|
|
|
|
|
| 字体 | `loadFont()` | TrueType 字体文件 |
|
|
|
|
|
|
| 音频 | `loadSound()` / `loadMusic()` | 音频文件 |
|
|
|
|
|
|
|
|
|
|
|
|
## 纹理加载
|
|
|
|
|
|
|
|
|
|
|
|
### 基本用法
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 加载纹理
|
|
|
|
|
|
auto texture = resources.loadTexture("assets/images/player.png");
|
|
|
|
|
|
|
|
|
|
|
|
if (texture) {
|
|
|
|
|
|
// 创建精灵
|
|
|
|
|
|
auto sprite = Sprite::create(texture);
|
|
|
|
|
|
addChild(sprite);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-02-11 22:30:57 +08:00
|
|
|
|
### 异步加载
|
|
|
|
|
|
|
|
|
|
|
|
Extra2D 支持异步加载纹理,避免阻塞主线程:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 同步加载(默认)
|
|
|
|
|
|
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 支持多种纹理压缩格式,可显著减少显存占用:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 支持的纹理格式
|
|
|
|
|
|
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 | 中等 | 大纹理、远景贴图 |
|
|
|
|
|
|
|
2026-02-11 19:40:26 +08:00
|
|
|
|
### 纹理缓存
|
|
|
|
|
|
|
|
|
|
|
|
资源管理器会自动缓存已加载的纹理,多次加载同一文件会返回缓存的实例:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 第一次加载 - 从文件读取
|
|
|
|
|
|
auto tex1 = resources.loadTexture("assets/image.png");
|
|
|
|
|
|
|
|
|
|
|
|
// 第二次加载 - 返回缓存
|
|
|
|
|
|
auto tex2 = resources.loadTexture("assets/image.png");
|
|
|
|
|
|
|
|
|
|
|
|
// tex1 和 tex2 指向同一个纹理对象
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-02-12 12:31:14 +08:00
|
|
|
|
### LRU 缓存机制
|
|
|
|
|
|
|
|
|
|
|
|
Extra2D 使用 LRU (Least Recently Used) 算法管理纹理缓存,自动清理最久未使用的纹理:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 配置纹理缓存参数
|
|
|
|
|
|
auto& resources = Application::instance().resources();
|
|
|
|
|
|
|
|
|
|
|
|
// 设置缓存参数:最大缓存大小(字节)、最大纹理数量、自动清理间隔(秒)
|
|
|
|
|
|
resources.setTextureCache(
|
|
|
|
|
|
128 * 1024 * 1024, // 128MB 最大缓存
|
|
|
|
|
|
512, // 最多 512 个纹理
|
|
|
|
|
|
30.0f // 每 30 秒检查一次
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 在主循环中更新资源管理器(用于自动清理)
|
|
|
|
|
|
void GameScene::update(float dt) {
|
|
|
|
|
|
// 这会触发缓存清理检查
|
|
|
|
|
|
resources.update(dt);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 缓存统计
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 获取缓存使用情况
|
|
|
|
|
|
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. **容量限制**:当缓存超过 `maxCacheSize` 或 `maxTextureCount` 时,自动驱逐最久未使用的纹理
|
|
|
|
|
|
2. **定时清理**:每 `unloadInterval` 秒检查一次,如果缓存超过 80%,清理到 50%
|
|
|
|
|
|
3. **访问更新**:每次访问纹理时,自动将其移到 LRU 链表头部(标记为最近使用)
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 手动清理缓存
|
|
|
|
|
|
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 | 内存充足,可以更大 |
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 根据平台设置不同的缓存策略
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-02-11 22:30:57 +08:00
|
|
|
|
### 纹理图集(Texture Atlas)
|
|
|
|
|
|
|
|
|
|
|
|
Extra2D 自动使用纹理图集优化渲染性能:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 获取纹理图集管理器
|
|
|
|
|
|
auto& atlasManager = resources.getTextureAtlasManager();
|
|
|
|
|
|
|
|
|
|
|
|
// 将多个纹理打包到图集(自动进行)
|
|
|
|
|
|
// 渲染时,相同图集的精灵会自动批处理
|
|
|
|
|
|
|
|
|
|
|
|
// 手动创建图集(高级用法)
|
|
|
|
|
|
auto atlas = atlasManager.createAtlas("ui_atlas", 2048, 2048);
|
|
|
|
|
|
atlas->addTexture("button", buttonTexture);
|
|
|
|
|
|
atlas->addTexture("icon", iconTexture);
|
|
|
|
|
|
atlas->pack(); // 执行打包
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-02-13 08:56:27 +08:00
|
|
|
|
## 精灵帧(SpriteFrame)
|
|
|
|
|
|
|
|
|
|
|
|
### 什么是精灵帧?
|
|
|
|
|
|
|
|
|
|
|
|
精灵帧是纹理图集中的一个矩形区域,用于从单个大纹理中提取小图像。使用精灵帧可以:
|
|
|
|
|
|
|
|
|
|
|
|
- 减少纹理切换,提高渲染性能
|
|
|
|
|
|
- 方便管理动画帧和 UI 元素
|
|
|
|
|
|
- 支持从 JSON 文件加载精灵帧数据
|
|
|
|
|
|
|
|
|
|
|
|
### 完整示例:资源加载器
|
|
|
|
|
|
|
|
|
|
|
|
参考 `examples/flappy_bird/ResLoader.h/cpp`:
|
|
|
|
|
|
|
|
|
|
|
|
```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
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 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
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 使用精灵帧创建精灵
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 参考 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 图集格式
|
|
|
|
|
|
|
|
|
|
|
|
```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 }
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-02-11 19:40:26 +08:00
|
|
|
|
## 字体加载
|
|
|
|
|
|
|
|
|
|
|
|
### 基本用法
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 加载字体(指定字号)
|
|
|
|
|
|
auto font24 = resources.loadFont("assets/font.ttf", 24, true);
|
|
|
|
|
|
|
|
|
|
|
|
// 创建文本
|
|
|
|
|
|
auto text = Text::create("Hello World", font24);
|
|
|
|
|
|
addChild(text);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 字体后备
|
|
|
|
|
|
|
|
|
|
|
|
支持设置后备字体,当主字体缺少某些字符时自动使用后备字体:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 加载主字体和后备字体
|
|
|
|
|
|
auto mainFont = resources.loadFont("assets/main.ttf", 24, true);
|
|
|
|
|
|
auto fallbackFont = resources.loadFont("assets/fallback.ttf", 24, true);
|
|
|
|
|
|
|
|
|
|
|
|
// 设置后备字体
|
|
|
|
|
|
mainFont->setFallback(fallbackFont);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 资源路径
|
|
|
|
|
|
|
|
|
|
|
|
### 路径格式
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 相对路径(相对于工作目录)
|
|
|
|
|
|
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");
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 路径辅助函数
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 获取平台特定的资源路径
|
|
|
|
|
|
std::string path = ResourceManager::getPlatformPath("images/player.png");
|
|
|
|
|
|
// Windows: "assets/images/player.png"
|
|
|
|
|
|
// Switch: "romfs:/images/player.png"
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 资源释放
|
|
|
|
|
|
|
|
|
|
|
|
### 自动释放
|
|
|
|
|
|
|
|
|
|
|
|
资源使用智能指针管理,当没有引用时会自动释放:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
{
|
|
|
|
|
|
auto tex = resources.loadTexture("assets/temp.png");
|
|
|
|
|
|
// 使用纹理...
|
|
|
|
|
|
} // 超出作用域,如果没有其他引用,纹理自动释放
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 手动清理缓存
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
2026-02-12 12:31:14 +08:00
|
|
|
|
// 清理未使用的资源(清理字体和音效缓存中已失效的弱引用)
|
|
|
|
|
|
resources.purgeUnused();
|
|
|
|
|
|
|
|
|
|
|
|
// 清空特定类型的缓存
|
|
|
|
|
|
resources.clearTextureCache(); // 清空纹理缓存
|
|
|
|
|
|
resources.clearFontCache(); // 清空字体缓存
|
|
|
|
|
|
resources.clearSoundCache(); // 清空音效缓存
|
2026-02-11 19:40:26 +08:00
|
|
|
|
|
|
|
|
|
|
// 清空所有缓存(谨慎使用)
|
2026-02-12 12:31:14 +08:00
|
|
|
|
resources.clearAllCaches();
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有正在进行的异步加载
|
|
|
|
|
|
if (resources.hasPendingAsyncLoads()) {
|
|
|
|
|
|
// 等待所有异步加载完成
|
|
|
|
|
|
resources.waitForAsyncLoads();
|
|
|
|
|
|
}
|
2026-02-11 19:40:26 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-02-11 22:30:57 +08:00
|
|
|
|
## 内存管理
|
|
|
|
|
|
|
2026-02-13 17:34:46 +08:00
|
|
|
|
### 对象池(Object Pool)
|
|
|
|
|
|
|
|
|
|
|
|
Extra2D 提供高性能的对象池系统,用于高效分配和回收小对象,减少频繁的内存分配/释放开销。
|
|
|
|
|
|
|
|
|
|
|
|
#### 特性
|
|
|
|
|
|
|
|
|
|
|
|
| 特性 | 说明 |
|
|
|
|
|
|
|------|------|
|
|
|
|
|
|
| **自动内存对齐** | 自动使用 `alignof(T)` 确保对象正确对齐 |
|
|
|
|
|
|
| **侵入式空闲链表** | 零额外内存开销管理空闲对象 |
|
|
|
|
|
|
| **线程本地缓存** | 自动为每个线程提供本地缓存,减少锁竞争 |
|
|
|
|
|
|
| **自动容量管理** | 根据使用模式自动扩展和收缩 |
|
|
|
|
|
|
| **自动预热** | 首次使用时智能预分配 |
|
|
|
|
|
|
| **异常安全** | 自动处理析构异常 |
|
|
|
|
|
|
|
|
|
|
|
|
#### 基本用法
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
#include <extra2d/utils/object_pool.h>
|
|
|
|
|
|
|
|
|
|
|
|
// 方式1:直接使用对象池
|
|
|
|
|
|
extra2d::ObjectPool<MyObject> pool;
|
|
|
|
|
|
MyObject* obj = pool.allocate();
|
|
|
|
|
|
pool.deallocate(obj);
|
|
|
|
|
|
|
|
|
|
|
|
// 方式2:使用智能指针自动管理(推荐)
|
|
|
|
|
|
auto obj = E2D_MAKE_POOLED(MyObject, arg1, arg2);
|
|
|
|
|
|
// 离开作用域自动回收
|
|
|
|
|
|
|
|
|
|
|
|
// 方式3:使用 PooledAllocator
|
|
|
|
|
|
extra2d::PooledAllocator<MyObject> allocator;
|
|
|
|
|
|
auto obj = allocator.makeShared(arg1, arg2);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 完整示例:游戏撤销系统
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 定义移动记录结构体
|
|
|
|
|
|
struct MoveRecord {
|
|
|
|
|
|
int fromX, fromY;
|
|
|
|
|
|
int toX, toY;
|
|
|
|
|
|
int boxFromX, boxFromY;
|
|
|
|
|
|
int boxToX, boxToY;
|
|
|
|
|
|
bool pushedBox;
|
|
|
|
|
|
|
|
|
|
|
|
MoveRecord() = default;
|
|
|
|
|
|
MoveRecord(int fx, int fy, int tx, int ty, bool pushed = false)
|
|
|
|
|
|
: fromX(fx), fromY(fy), toX(tx), toY(ty)
|
|
|
|
|
|
, boxFromX(-1), boxFromY(-1), boxToX(-1), boxToY(-1)
|
|
|
|
|
|
, pushedBox(pushed) {}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 使用对象池创建移动记录
|
|
|
|
|
|
class GameScene : public Scene {
|
|
|
|
|
|
private:
|
|
|
|
|
|
std::stack<extra2d::Ptr<MoveRecord>> moveHistory_;
|
|
|
|
|
|
|
|
|
|
|
|
void move(int dx, int dy) {
|
|
|
|
|
|
// 使用对象池创建记录(自动管理内存)
|
|
|
|
|
|
auto record = E2D_MAKE_POOLED(MoveRecord, playerX, playerY,
|
|
|
|
|
|
playerX + dx, playerY + dy);
|
|
|
|
|
|
|
|
|
|
|
|
// 记录推箱子信息
|
|
|
|
|
|
if (pushedBox) {
|
|
|
|
|
|
record->pushedBox = true;
|
|
|
|
|
|
record->boxFromX = boxX;
|
|
|
|
|
|
record->boxFromY = boxY;
|
|
|
|
|
|
record->boxToX = newBoxX;
|
|
|
|
|
|
record->boxToY = newBoxY;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存到历史栈
|
|
|
|
|
|
moveHistory_.push(record);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void undoMove() {
|
|
|
|
|
|
if (moveHistory_.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto record = moveHistory_.top();
|
|
|
|
|
|
moveHistory_.pop();
|
|
|
|
|
|
|
|
|
|
|
|
// 恢复游戏状态
|
|
|
|
|
|
playerX = record->fromX;
|
|
|
|
|
|
playerY = record->fromY;
|
|
|
|
|
|
|
|
|
|
|
|
if (record->pushedBox) {
|
|
|
|
|
|
// 恢复箱子位置
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// record 离开作用域后自动回收到对象池
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 内存统计
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 获取对象池内存使用情况
|
|
|
|
|
|
auto pool = extra2d::ObjectPoolManager::getInstance().getPool<MyObject>();
|
|
|
|
|
|
size_t allocated = pool->allocatedCount(); // 已分配对象数
|
|
|
|
|
|
size_t capacity = pool->capacity(); // 总容量
|
|
|
|
|
|
size_t memory = pool->memoryUsage(); // 内存使用量(字节)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 配置参数
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 对象池配置(在 PoolConfig 中定义)
|
|
|
|
|
|
struct PoolConfig {
|
|
|
|
|
|
static constexpr size_t DEFAULT_BLOCK_SIZE = 64; // 每块对象数
|
|
|
|
|
|
static constexpr size_t THREAD_CACHE_SIZE = 16; // 线程缓存大小
|
|
|
|
|
|
static constexpr size_t SHRINK_THRESHOLD_MS = 30000; // 收缩检查间隔
|
|
|
|
|
|
static constexpr double SHRINK_RATIO = 0.5; // 收缩阈值
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 性能优势
|
|
|
|
|
|
|
|
|
|
|
|
| 场景 | 传统分配 | 对象池 |
|
|
|
|
|
|
|------|---------|--------|
|
|
|
|
|
|
| 频繁分配/释放 | 大量内存碎片 | 零碎片 |
|
|
|
|
|
|
| 多线程竞争 | 锁竞争严重 | 线程本地缓存 |
|
|
|
|
|
|
| 内存对齐 | 手动处理 | 自动对齐 |
|
|
|
|
|
|
| 首次分配延迟 | 可能卡顿 | 自动预热 |
|
|
|
|
|
|
|
2026-02-11 22:30:57 +08:00
|
|
|
|
### 内存池(内部自动管理)
|
|
|
|
|
|
|
|
|
|
|
|
Extra2D 使用内存池优化小对象分配,无需用户干预:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 内存池自动管理以下对象:
|
|
|
|
|
|
// - 场景节点
|
|
|
|
|
|
// - 渲染命令
|
|
|
|
|
|
// - 碰撞形状
|
|
|
|
|
|
// - 事件对象
|
|
|
|
|
|
|
|
|
|
|
|
// 用户代码无需特殊处理,正常使用即可
|
|
|
|
|
|
auto node = Node::create(); // 自动使用内存池
|
|
|
|
|
|
auto sprite = Sprite::create(texture); // 自动使用内存池
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 批量更新(内部自动进行)
|
|
|
|
|
|
|
|
|
|
|
|
Extra2D 自动批量更新节点变换,优化性能:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 以下操作会自动批处理:
|
|
|
|
|
|
// - 节点变换更新
|
|
|
|
|
|
// - 渲染命令提交
|
|
|
|
|
|
// - 纹理绑定
|
|
|
|
|
|
|
|
|
|
|
|
// 用户代码无需特殊处理
|
|
|
|
|
|
for (int i = 0; i < 1000; ++i) {
|
|
|
|
|
|
auto sprite = Sprite::create(texture);
|
|
|
|
|
|
sprite->setPosition(i * 10, 100);
|
|
|
|
|
|
addChild(sprite); // 变换更新会自动批处理
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 渲染批处理
|
|
|
|
|
|
|
|
|
|
|
|
### 自动批处理
|
|
|
|
|
|
|
|
|
|
|
|
Extra2D 自动将渲染命令批处理以优化性能:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 以下情况会自动批处理:
|
|
|
|
|
|
// 1. 相同纹理的精灵
|
|
|
|
|
|
// 2. 相同图层的节点
|
|
|
|
|
|
// 3. 相同混合模式
|
|
|
|
|
|
|
|
|
|
|
|
// 示例:1000 个相同纹理的精灵会自动批处理为少量 draw call
|
|
|
|
|
|
for (int i = 0; i < 1000; ++i) {
|
|
|
|
|
|
auto sprite = Sprite::create(texture);
|
|
|
|
|
|
addChild(sprite);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 手动控制渲染顺序
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 设置节点的渲染层级(z-order)
|
|
|
|
|
|
sprite->setZOrder(10); // 值越大,渲染越靠前
|
|
|
|
|
|
|
|
|
|
|
|
// 同层级的节点会自动批处理
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-02-11 19:40:26 +08:00
|
|
|
|
## 完整示例
|
|
|
|
|
|
|
|
|
|
|
|
参考 `examples/push_box/StartScene.cpp`:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
void StartScene::onEnter() {
|
|
|
|
|
|
Scene::onEnter();
|
|
|
|
|
|
|
|
|
|
|
|
auto& app = Application::instance();
|
|
|
|
|
|
auto& resources = app.resources();
|
|
|
|
|
|
|
2026-02-11 22:30:57 +08:00
|
|
|
|
// 加载背景纹理(异步 + 压缩)
|
|
|
|
|
|
auto bgTex = resources.loadTexture("assets/images/start.jpg", true, TextureFormat::DXT1);
|
2026-02-11 19:40:26 +08:00
|
|
|
|
if (bgTex) {
|
|
|
|
|
|
auto background = Sprite::create(bgTex);
|
|
|
|
|
|
background->setAnchor(0.0f, 0.0f);
|
|
|
|
|
|
addChild(background);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 22:30:57 +08:00
|
|
|
|
// 加载音效图标纹理(异步 + DXT5 压缩支持透明)
|
|
|
|
|
|
auto soundOn = resources.loadTexture("assets/images/soundon.png", true, TextureFormat::DXT5);
|
|
|
|
|
|
auto soundOff = resources.loadTexture("assets/images/soundoff.png", true, TextureFormat::DXT5);
|
2026-02-11 19:40:26 +08:00
|
|
|
|
if (soundOn && soundOff) {
|
|
|
|
|
|
soundIcon_ = Sprite::create(g_SoundOpen ? soundOn : soundOff);
|
|
|
|
|
|
addChild(soundIcon_);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 加载字体
|
|
|
|
|
|
font_ = resources.loadFont("assets/font.ttf", 28, true);
|
|
|
|
|
|
|
|
|
|
|
|
// 创建按钮...
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-02-11 22:30:57 +08:00
|
|
|
|
## 性能优化建议
|
|
|
|
|
|
|
|
|
|
|
|
### 纹理优化
|
|
|
|
|
|
|
|
|
|
|
|
1. **使用纹理压缩** - 对大型纹理使用 DXT/ASTC 压缩减少显存占用
|
|
|
|
|
|
2. **使用纹理图集** - 将多个小纹理打包到图集,减少 draw call
|
|
|
|
|
|
3. **异步加载大纹理** - 避免在主线程加载大型资源造成卡顿
|
|
|
|
|
|
4. **合理设置纹理尺寸** - 避免使用过大的纹理(建议最大 2048x2048)
|
2026-02-12 12:31:14 +08:00
|
|
|
|
5. **配置合适的缓存大小** - 根据平台内存设置合理的 LRU 缓存参数
|
|
|
|
|
|
6. **监控缓存命中率** - 使用 `printTextureCacheStats()` 检查缓存效率
|
2026-02-11 22:30:57 +08:00
|
|
|
|
|
|
|
|
|
|
### 资源加载策略
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 场景预加载
|
|
|
|
|
|
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) {
|
|
|
|
|
|
// 加载完成后创建背景
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-02-11 19:40:26 +08:00
|
|
|
|
## 最佳实践
|
|
|
|
|
|
|
|
|
|
|
|
1. **预加载资源** - 在场景 `onEnter()` 中加载所需资源
|
|
|
|
|
|
2. **检查资源有效性** - 始终检查加载结果是否为 nullptr
|
|
|
|
|
|
3. **复用资源** - 多次使用同一资源时保存指针,避免重复加载
|
|
|
|
|
|
4. **合理设置字号** - 字体加载时会生成对应字号的图集
|
2026-02-11 22:30:57 +08:00
|
|
|
|
5. **使用异步加载** - 对大型资源使用异步加载避免卡顿
|
|
|
|
|
|
6. **选择合适的压缩格式** - 根据纹理用途选择最佳压缩格式
|
|
|
|
|
|
7. **利用自动批处理** - 相同纹理的精灵会自动批处理,无需手动优化
|
2026-02-12 12:31:14 +08:00
|
|
|
|
8. **配置 LRU 缓存** - 根据平台内存配置合适的缓存大小
|
|
|
|
|
|
9. **定期监控缓存** - 在开发阶段定期检查缓存命中率和内存使用
|
|
|
|
|
|
10. **在主循环更新资源管理器** - 确保调用 `resources.update(dt)` 以触发自动清理
|
2026-02-11 19:40:26 +08:00
|
|
|
|
|
|
|
|
|
|
## 下一步
|
|
|
|
|
|
|
|
|
|
|
|
- [05. 输入处理](./05_Input_Handling.md) - 学习输入处理
|
|
|
|
|
|
- [06. 碰撞检测](./06_Collision_Detection.md) - 学习碰撞检测系统
|