2.5 资源管理
在开发游戏或应用界面时,有效地管理字体、图片等资源是至关重要的。本节将详细介绍 RmlUi 的资源管理系统。
一、字体管理
1.1 字体加载方式
RmlUi 支持多种字体格式(TTF、OTF、Woff、Woff2),可以通过多种方式加载字体。
在 C++ 中加载字体
cpp
#include <RmlUi/Core.h>
// 方式 1:加载单个字体文件
bool success = Rml::LoadFontFace("fonts/arial.ttf");
// 方式 2:加载字体并指定fallback
Rml::LoadFontFace("fonts/arial.ttf", true); // true 表示设为fallback字体
// 方式 3:从内存加载字体
std::string font_data = LoadFileIntoMemory("fonts/arial.ttf");
Rml::FontFaceHandle font_handle = Rml::LoadFontFaceFromMemory(
reinterpret_cast<const byte*>(font_data.data()),
font_data.size(),
"Arial",
false,
false
);
// 方式 4:加载字体家族
Rml::LoadFontFace("fonts/roboto-regular.ttf");
Rml::LoadFontFace("fonts/roboto-bold.ttf");
Rml::LoadFontFace("fonts/roboto-italic.ttf");
// RmlUi 会自动识别并归类到同一个字体家族在 RCSS 中引用字体
css
/* 使用 @font-face 定义自定义字体 */
@font-face {
font-family: "MyCustomFont";
src: url("fonts/myfont.ttf");
}
@font-face {
font-family: "MyCustomFont";
src: url("fonts/myfont-bold.ttf");
font-weight: bold;
}
@font-face {
font-family: "MyCustomFont";
src: url("fonts/myfont-italic.ttf");
font-style: italic;
}
/* 使用字体 */
body {
font-family: "MyCustomFont", Arial, sans-serif;
}1.2 字体图标(Icon Font)
字体图标是游戏 UI 中常用的技术,可以将图标作为字体字符使用。
css
/* 加载图标字体 */
@font-face {
font-family: "GameIcons";
src: url("fonts/game-icons.ttf");
}
/* 定义图标类 */
.icon-health::before {
content: "\e001"; /* Unicode 编码 */
font-family: "GameIcons";
color: #e74c3c;
}
.icon-mana::before {
content: "\e002";
font-family: "GameIcons";
color: #3498db;
}
.icon-gold::before {
content: "\e003";
font-family: "GameIcons";
color: #f1c40f;
}xml
<!-- 使用图标 -->
<div class="stat-row">
<span class="icon-health"></span>
<span class="value">100/100</span>
</div>1.3 中文字体处理
中文字体文件通常很大(几 MB 到几十 MB),建议:
- 使用系统字体:让 RmlUi 回退到系统默认中文字体
- 字体子集化:只包含游戏中实际用到的汉字
- 使用在线字体服务:动态加载所需字符
cpp
// 设置 fallback 字体链
Rml::LoadFontFace("fonts/latin.ttf", false);
Rml::LoadFontFace("fonts/noto-sans-cjk.ttf", true); // 作为 fallbackcss
/* 字体栈:优先使用自定义字体,回退到系统字体 */
body {
font-family: "MyFont", "Noto Sans CJK SC", "Microsoft YaHei", sans-serif;
}二、图片资源管理
2.1 基本图片加载
RmlUi 支持常见的图片格式(PNG、JPG、TGA、BMP、DDS 等)。
xml
<!-- 基本图片 -->
<img src="images/logo.png"/>
<!-- 带尺寸的图片 -->
<img src="images/background.jpg" style="width: 800px; height: 600px;"/>
<!-- 使用 object-fit 控制缩放行为 -->
<img src="images/avatar.jpg" style="width: 64px; height: 64px; object-fit: cover;"/>2.2 图片作为背景
css
/* 背景图片 */
.panel {
background-image: url(images/panel-bg.png);
background-repeat: no-repeat;
background-size: cover;
}
/* 九宫格切图(用于可拉伸的边框) */
.window {
background-image: url(images/window-frame.9.png);
background-slice: 10px 10px 10px 10px; /* 上右下左 */
}
/* 平铺背景 */
.tiled-bg {
background-image: url(images/pattern.png);
background-repeat: repeat;
}2.3 图片预加载
为了避免运行时首次加载的延迟,可以预加载图片。
cpp
class ResourceManager
{
public:
// 预加载图片
void PreloadImages()
{
const std::vector<std::string> images = {
"images/logo.png",
"images/background.jpg",
"images/button-hover.png",
"images/button-pressed.png"
};
for (const auto& path : images)
{
// 调用 GetTexture 会触发加载
Rml::TextureResource* resource = Rml::TextureResource::Get(path);
if (resource)
{
resource->EnableKeepMemoryAlive(); // 保持内存中的引用
}
}
}
// 预加载整个目录
void PreloadDirectory(const std::string& directory)
{
// 使用文件接口列出目录
Rml::FileInterface* file_interface = Rml::GetFileInterface();
// ... 实现目录遍历和批量加载
}
};三、精灵图(Sprite Sheet)
精灵图将多个小图标合并为一张大图,减少绘制调用并提高性能。
3.1 定义精灵图
首先,在 RCSS 中定义精灵图:
css
/* 定义精灵图容器 */
@sprite-sheet "ui-icons" {
src: url("images/icons-sprite.png");
sprite-size: 32px 32px; /* 每个精灵的尺寸 */
frames: 8 8; /* 8x8 网格 */
}
/* 或者手动定义每个精灵 */
@sprite "icon-play" {
src: url("images/icons-sprite.png");
rect: 0px 0px 32px 32px; /* left top right bottom */
}
@sprite "icon-pause" {
src: url("images/icons-sprite.png");
rect: 32px 0px 64px 32px;
}
@sprite "icon-stop" {
src: url("images/icons-sprite.png");
rect: 64px 0px 96px 32px;
}3.2 使用精灵图
xml
<!-- 使用精灵 -->
<div class="icon-play"></div>
<div class="icon-pause"></div>
<div class="icon-stop"></div>
<!-- 或者作为背景 -->
<button class="play-button">
<span class="icon-play"></span>
播放
</button>css
/* 精灵作为背景 */
.icon-play {
display: inline-block;
width: 32px;
height: 32px;
background-image: url("images/icons-sprite.png");
background-position: 0px 0px;
}
.icon-pause {
display: inline-block;
width: 32px;
height: 32px;
background-image: url("images/icons-sprite.png");
background-position: -32px 0px;
}3.3 使用工具生成精灵图
推荐使用工具自动生成精灵图和相关配置:
- TexturePacker: 商业软件,功能强大
- ShoeBox: 免费工具
- spritesheet.js: Node.js 命令行工具
生成后,可以编写脚本自动生成 RCSS 的 @sprite 定义。
四、资源打包与虚拟化
4.1 自定义文件接口
游戏通常使用打包文件(如 ZIP、自定义格式),需要实现自定义的 FileInterface。
cpp
#include <RmlUi/Core.h>
#include <physfs.h> // 使用 PhysicsFS 读取 ZIP/打包文件
class PhysFSFileInterface : public Rml::FileInterface
{
public:
// 打开文件
Rml::FileHandle Open(const Rml::String& path) override
{
PHYSFS_File* file = PHYSFS_openRead(path.c_str());
return reinterpret_cast<Rml::FileHandle>(file);
}
// 关闭文件
void Close(Rml::FileHandle file) override
{
PHYSFS_File* physfs_file = reinterpret_cast<PHYSFS_File*>(file);
PHYSFS_close(physfs_file);
}
// 读取文件
size_t Read(void* buffer, size_t size, Rml::FileHandle file) override
{
PHYSFS_File* physfs_file = reinterpret_cast<PHYSFS_File*>(file);
return PHYSFS_read(physfs_file, buffer, 1, size);
}
// 获取文件大小
size_t Length(Rml::FileHandle file) override
{
PHYSFS_File* physfs_file = reinterpret_cast<PHYSFS_File*>(file);
return PHYSFS_fileLength(physfs_file);
}
// 设置目录(用于相对路径)
bool SetDirectory(const Rml::String& path) override
{
return PHYSFS_mount(path.c_str(), nullptr, 0) != 0;
}
};
// 初始化
void InitRmlUi()
{
Rml::SetFileInterface(new PhysFSFileInterface());
// 初始化 PhysicsFS 并挂载打包文件
PHYSFS_init(nullptr);
PHYSFS_mount("ui.zip", nullptr, 0);
}4.2 虚拟文件系统
对于复杂的项目,可以实现虚拟路径系统:
cpp
class VirtualFileSystem
{
public:
void Mount(const std::string& virtual_path, const std::string& physical_path)
{
mounts_[virtual_path] = physical_path;
}
std::string ResolvePath(const std::string& path)
{
// 解析虚拟路径到物理路径
// 例如:"ui:images/logo.png" -> "assets/ui/images/logo.png"
for (const auto& [prefix, base_path] : mounts_)
{
if (path.find(prefix + ":") == 0)
{
return base_path + "/" + path.substr(prefix.length() + 1);
}
}
return path;
}
private:
std::unordered_map<std::string, std::string> mounts_;
};
// 使用
VirtualFileSystem vfs;
vfs.Mount("ui", "assets/ui");
vfs.Mount("fonts", "assets/fonts");
vfs.Mount("images", "assets/images");
// 在 RML 中可以使用:
// <img src="ui:images/logo.png"/>五、资源缓存与优化
5.1 纹理缓存
cpp
class TextureCache
{
public:
// 获取或加载纹理
Rml::TextureResource* GetTexture(const std::string& path)
{
auto it = cache_.find(path);
if (it != cache_.end())
{
return it->second.get();
}
// 加载新纹理
Rml::TextureResource* texture = Rml::TextureResource::Get(path);
if (texture)
{
cache_[path] = std::shared_ptr<Rml::TextureResource>(texture);
}
return texture;
}
// 清除未使用的纹理
void ClearUnused()
{
for (auto it = cache_.begin(); it != cache_.end();)
{
if (it->second.use_count() == 1)
{
it = cache_.erase(it);
}
else
{
++it;
}
}
}
private:
std::unordered_map<std::string, std::shared_ptr<Rml::TextureResource>> cache_;
};5.2 资源释放策略
cpp
class ResourceManager
{
public:
// 内存紧张时释放资源
void OnLowMemory()
{
// 1. 释放字体缓存
Rml::FlushFontCache();
// 2. 释放未使用的纹理
texture_cache_.ClearUnused();
// 3. 强制垃圾回收
Rml::GarbageCollect();
}
// 切换到新场景时清理
void OnSceneChange()
{
// 保留常用资源
KeepEssentialResources();
// 清除场景相关资源
ClearSceneResources();
}
private:
TextureCache texture_cache_;
void KeepEssentialResources()
{
// 保留基础 UI 资源
}
void ClearSceneResources()
{
// 清除当前场景的资源
}
};六、实战:完整资源管理系统
6.1 资源管理器设计
cpp
// UIManager.h
#pragma once
#include <RmlUi/Core.h>
#include <unordered_map>
#include <memory>
class UIManager
{
public:
static UIManager& Instance()
{
static UIManager instance;
return instance;
}
// 初始化
bool Initialize()
{
// 1. 设置文件接口
file_interface_ = std::make_unique<GameFileInterface>();
Rml::SetFileInterface(file_interface_.get());
// 2. 加载字体
if (!LoadFonts())
return false;
// 3. 预加载资源
PreloadResources();
return true;
}
// 加载字体
bool LoadFonts()
{
// 基础字体
if (!Rml::LoadFontFace("fonts/arial.ttf", true))
return false;
// 中文字体
Rml::LoadFontFace("fonts/noto-sans-cjk.ttf", true);
// 图标字体
Rml::LoadFontFace("fonts/game-icons.ttf");
return true;
}
// 预加载资源
void PreloadResources()
{
// UI 背景
Rml::TextureResource::Get("textures/ui/panel-bg.png");
Rml::TextureResource::Get("textures/ui/button-normal.png");
Rml::TextureResource::Get("textures/ui/button-hover.png");
Rml::TextureResource::Get("textures/ui/button-pressed.png");
// 图标
Rml::TextureResource::Get("textures/icons/health.png");
Rml::TextureResource::Get("textures/icons/mana.png");
Rml::TextureResource::Get("textures/icons/gold.png");
}
// 创建上下文
Rml::Context* CreateContext(const std::string& name, int width, int height)
{
return Rml::CreateContext(name.c_str(), Rml::Vector2i(width, height));
}
private:
UIManager() = default;
std::unique_ptr<GameFileInterface> file_interface_;
};七、实践练习
练习 1:创建字体加载模块
编写一个字体加载模块,支持:
- 从配置文件读取字体列表
- 自动识别字体家族(Regular、Bold、Italic)
- 设置 fallback 字体链
练习 2:实现精灵图工具
编写脚本自动生成精灵图的 RCSS 定义:
- 输入:目录中的多个小图标
- 输出:合并后的精灵图 + RCSS 定义文件
练习 3:资源打包
创建一个简单的打包工具:
- 将 RML、RCSS、图片文件打包到 ZIP
- 实现自定义 FileInterface 读取 ZIP
📝 检查清单
下一节:响应式设计 - 学习如何创建适配不同屏幕尺寸的界面。