📚 快速导航
目录
📖 简介
在之前的教程中,我们实现了多个独立的资源管理系统:纹理系统、材质系统、几何体系统。每个系统都有自己的加载逻辑,这导致代码重复和维护困难。
本教程将介绍资源系统 (Resource System),它提供了一个统一的、可扩展的资源加载框架。通过加载器模式 (Loader Pattern),资源系统将资源加载逻辑模块化,让不同类型的资源可以通过注册加载器来实现加载。
🎯 学习目标
| 目标 | 描述 |
|---|---|
| 理解资源系统的必要性 | 了解为什么需要统一的资源加载框架 |
| 掌握加载器模式 | 学习可插拔加载器的设计和实现 |
| 实现内置加载器 | 实现文本、二进制、图像、材质加载器 |
| 资源类型管理 | 理解资源类型枚举和扩展机制 |
| 系统集成 | 将资源系统与现有系统集成 |
🏗️ 资源系统架构
为什么需要资源系统
在没有资源系统之前,每个系统都有自己的加载逻辑:
❌ 问题:
┌─────────────────┐
│ Texture System │ → 加载 PNG/JPG
│ - load_texture()│
└─────────────────┘
┌─────────────────┐
│ Material System │ → 加载 .kmt 文件
│ - load_material│
└─────────────────┘
┌─────────────────┐
│ Geometry System │ → 加载顶点数据
│ - load_geometry│
└─────────────────┘
问题:
1. 代码重复 (文件读取、路径处理)
2. 难以扩展 (添加新类型需要修改多处)
3. 不一致的错误处理
4. 难以统一管理资源生命周期
资源系统通过统一接口解决这些问题:
✅ 解决方案:
┌──────────────────┐
│ Resource System │
│ │
│ register_loader │
│ load_resource │
│ unload_resource │
└────────┬─────────┘
│
┌───────────────┼───────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ Text │ │ Image │ │Material │
│ Loader │ │ Loader │ │ Loader │
└─────────┘ └─────────┘ └─────────┘
优势:
1. 统一的加载接口
2. 可插拔的加载器
3. 一致的错误处理
4. 易于扩展新类型
统一加载接口
资源系统提供了简单的加载接口:
// engine/src/systems/resource_system.h
// 加载资源
b8 resource_system_load(const char* name, resource_type type, resource* out_resource);
// 卸载资源
void resource_system_unload(resource* resource);
// 使用示例
resource my_resource;
if (resource_system_load("test.png", RESOURCE_TYPE_IMAGE, &my_resource)) {
// 使用资源
image_resource_data* image_data = (image_resource_data*)my_resource.data;
KINFO("Loaded image: %dx%d", image_data->width, image_data->height);
// 卸载资源
resource_system_unload(&my_resource);
}
可插拔加载器
资源系统通过注册加载器来支持不同的资源类型:
// 注册加载器
b8 resource_system_register_loader(resource_loader* loader);
// 示例:注册自定义加载器
resource_loader my_loader;
my_loader.type = RESOURCE_TYPE_CUSTOM;
my_loader.custom_type = "my_format";
my_loader.load = my_custom_load_function;
my_loader.unload = my_custom_unload_function;
resource_system_register_loader(&my_loader);
📋 资源类型定义
资源系统支持多种资源类型:
// engine/src/resources/resource_types.h
/**
* @brief 资源类型枚举
*/
typedef enum resource_type {
RESOURCE_TYPE_TEXT, // 文本文件 (.txt, .json, .xml)
RESOURCE_TYPE_BINARY, // 二进制文件 (通用)
RESOURCE_TYPE_IMAGE, // 图像文件 (.png, .jpg)
RESOURCE_TYPE_MATERIAL, // 材质配置 (.kmt)
RESOURCE_TYPE_STATIC_MESH, // 静态网格 (.obj, .fbx)
RESOURCE_TYPE_CUSTOM // 自定义类型
} resource_type;
/**
* @brief 资源结构
*/
typedef struct resource {
u32 loader_id; // 加载器 ID
const char* name; // 资源名称
char* full_path; // 完整路径
u64 data_size; // 数据大小
void* data; // 资源数据
} resource;
/**
* @brief 图像资源数据
*/
typedef struct image_resource_data {
u8 channel_count; // 通道数
u32 width; // 宽度
u32 height; // 高度
u8* pixels; // 像素数据
} image_resource_data;
资源类型的组织:
资源类型层次:
┌────────────────────────────────┐
│ Resource (通用资源) │
│ - loader_id │
│ - name │
│ - full_path │
│ - data_size │
│ - data (void*) │
└────────┬───────────────────────┘
│
├─► TEXT → data = char*
│
├─► BINARY → data = u8*
│
├─► IMAGE → data = image_resource_data*
│ ├─ width
│ ├─ height
│ ├─ channel_count
│ └─ pixels
│
├─► MATERIAL → data = material_config*
│
└─► CUSTOM → data = 自定义结构
🔌 资源加载器模式
加载器结构
每个加载器实现统一的接口:
// engine/src/systems/resource_system.h
/**
* @brief 资源加载器结构
*/
typedef struct resource_loader {
u32 id; // 加载器 ID
resource_type type; // 资源类型
const char* custom_type; // 自定义类型名称
const char* type_path; // 资源路径 (如 "textures/")
/**
* @brief 加载资源
* @param self 加载器自身
* @param name 资源名称
* @param out_resource 输出资源
* @return 成功返回 true
*/
b8 (*load)(struct resource_loader* self, const char* name, resource* out_resource);
/**
* @brief 卸载资源
* @param self 加载器自身
* @param resource 要卸载的资源
*/
void (*unload)(struct resource_loader* self, resource* resource);
} resource_loader;
加载器接口设计:
加载器生命周期:
┌──────────────┐
│ Register │ → resource_system_register_loader()
│ 注册加载器 │
└──────┬───────┘
│
▼
┌──────────────┐
│ Load │ → loader->load()
│ 加载资源 │ ├─ 构建文件路径
│ │ ├─ 读取文件内容
└──────┬───────┘ ├─ 解析数据
│ └─ 返回 resource
│
▼
┌──────────────┐
│ Use │ → 使用 resource->data
│ 使用资源 │
└──────┬───────┘
│
▼
┌──────────────┐
│ Unload │ → loader->unload()
│ 卸载资源 │ ├─ 释放 resource->data
│ │ └─ 释放 resource->full_path
└──────────────┘
加载器注册
资源系统维护加载器数组:
// engine/src/systems/resource_system.c
#define MAX_LOADER_COUNT 32
typedef struct resource_system_state {
resource_system_config config;
resource_loader registered_loaders[MAX_LOADER_COUNT];
} resource_system_state;
static resource_system_state* state_ptr = 0;
b8 resource_system_register_loader(resource_loader* loader) {
if (state_ptr == 0) {
return false;
}
// 分配加载器 ID
u32 count = state_ptr->config.max_loader_count;
for (u32 i = 0; i < count; ++i) {
if (state_ptr->registered_loaders[i].id == INVALID_ID) {
state_ptr->registered_loaders[i] = *loader;
state_ptr->registered_loaders[i].id = i;
KINFO("Registered loader for type %d at index %d", loader->type, i);
return true;
}
}
KERROR("resource_system_register_loader - No available loader slots");
return false;
}
🔧 内置加载器实现
文本加载器
加载纯文本文件:
// engine/src/resources/loaders/text_loader.c
typedef struct text_loader {
// 基础路径 (如 "assets/text/")
char base_path[512];
} text_loader;
static b8 text_loader_load(resource_loader* self, const char* name, resource* out_resource) {
if (self == 0 || name == 0 || out_resource == 0) {
return false;
}
text_loader* loader = (text_loader*)self;
// 构建完整路径
char full_path[512];
string_format(full_path, "%s%s%s", loader->base_path, name, ".txt");
// 读取文件
file_handle f;
if (!filesystem_open(full_path, FILE_MODE_READ, false, &f)) {
KERROR("text_loader_load - Failed to open file: %s", full_path);
return false;
}
// 获取文件大小
u64 file_size = 0;
if (!filesystem_size(&f, &file_size)) {
KERROR("text_loader_load - Failed to get file size: %s", full_path);
filesystem_close(&f);
return false;
}
// 分配内存 (+1 for null terminator)
char* resource_data = kallocate(file_size + 1, MEMORY_TAG_STRING);
// 读取内容
u64 read_size = 0;
if (!filesystem_read_all_bytes(&f, (u8*)resource_data, &read_size)) {
KERROR("text_loader_load - Failed to read file: %s", full_path);
kfree(resource_data, file_size + 1, MEMORY_TAG_STRING);
filesystem_close(&f);
return false;
}
filesystem_close(&f);
// 添加 null terminator
resource_data[file_size] = '\0';
// 填充 resource
out_resource->full_path = string_duplicate(full_path);
out_resource->name = name;
out_resource->data = resource_data;
out_resource->data_size = file_size;
out_resource->loader_id = self->id;
return true;
}
static void text_loader_unload(resource_loader* self, resource* resource) {
if (resource) {
if (resource->data) {
kfree(resource->data, resource->data_size + 1, MEMORY_TAG_STRING);
}
if (resource->full_path) {
u32 length = string_length(resource->full_path);
kfree(resource->full_path, length + 1, MEMORY_TAG_STRING);
}
kzero_memory(resource, sizeof(resource));
}
}
二进制加载器
加载二进制文件:
// engine/src/resources/loaders/binary_loader.c
static b8 binary_loader_load(resource_loader* self, const char* name, resource* out_resource) {
if (self == 0 || name == 0 || out_resource == 0) {
return false;
}
// 构建完整路径 (不添加扩展名,使用原始名称)
char full_path[512];
string_format(full_path, "%s%s", self->type_path, name);
// 读取文件
file_handle f;
if (!filesystem_open(full_path, FILE_MODE_READ, true, &f)) {
KERROR("binary_loader_load - Failed to open file: %s", full_path);
return false;
}
u64 file_size = 0;
if (!filesystem_size(&f, &file_size)) {
KERROR("binary_loader_load - Failed to get file size: %s", full_path);
filesystem_close(&f);
return false;
}
// 分配内存
u8* resource_data = kallocate(file_size, MEMORY_TAG_ARRAY);
// 读取内容
u64 read_size = 0;
if (!filesystem_read_all_bytes(&f, resource_data, &read_size)) {
KERROR("binary_loader_load - Failed to read file: %s", full_path);
kfree(resource_data, file_size, MEMORY_TAG_ARRAY);
filesystem_close(&f);
return false;
}
filesystem_close(&f);
out_resource->full_path = string_duplicate(full_path);
out_resource->name = name;
out_resource->data = resource_data;
out_resource->data_size = file_size;
out_resource->loader_id = self->id;
return true;
}
static void binary_loader_unload(resource_loader* self, resource* resource) {
if (resource) {
if (resource->data) {
kfree(resource->data, resource->data_size, MEMORY_TAG_ARRAY);
}
if (resource->full_path) {
u32 length = string_length(resource->full_path);
kfree(resource->full_path, length + 1, MEMORY_TAG_STRING);
}
kzero_memory(resource, sizeof(resource));
}
}
图像加载器
加载图像文件 (PNG/JPG):
// engine/src/resources/loaders/image_loader.c
#define STB_IMAGE_IMPLEMENTATION
#include "vendor/stb_image.h"
static b8 image_loader_load(resource_loader* self, const char* name, resource* out_resource) {
if (self == 0 || name == 0 || out_resource == 0) {
return false;
}
// 构建完整路径
char full_path[512];
string_format(full_path, "%s%s", self->type_path, name);
// 使用 stb_image 加载
image_resource_data* resource_data = kallocate(sizeof(image_resource_data), MEMORY_TAG_TEXTURE);
i32 width, height, channel_count;
stbi_set_flip_vertically_on_load(true);
resource_data->pixels = stbi_load(full_path, &width, &height, &channel_count, 0);
if (!resource_data->pixels) {
KERROR("image_loader_load - Failed to load image: %s", full_path);
kfree(resource_data, sizeof(image_resource_data), MEMORY_TAG_TEXTURE);
return false;
}
resource_data->width = width;
resource_data->height = height;
resource_data->channel_count = channel_count;
// 填充 resource
out_resource->full_path = string_duplicate(full_path);
out_resource->name = name;
out_resource->data = resource_data;
out_resource->data_size = width * height * channel_count;
out_resource->loader_id = self->id;
KINFO("Loaded image: %s (%dx%d, %d channels)", name, width, height, channel_count);
return true;
}
static void image_loader_unload(resource_loader* self, resource* resource) {
if (resource) {
if (resource->data) {
image_resource_data* data = (image_resource_data*)resource->data;
if (data->pixels) {
stbi_image_free(data->pixels);
}
kfree(data, sizeof(image_resource_data), MEMORY_TAG_TEXTURE);
}
if (resource->full_path) {
u32 length = string_length(resource->full_path);
kfree(resource->full_path, length + 1, MEMORY_TAG_STRING);
}
kzero_memory(resource, sizeof(resource));
}
}
材质加载器
加载材质配置文件:
// engine/src/resources/loaders/material_loader.c
typedef struct material_config {
char name[MATERIAL_NAME_MAX_LENGTH];
b8 auto_release;
vec4 diffuse_colour;
char diffuse_map_name[TEXTURE_NAME_MAX_LENGTH];
} material_config;
static b8 material_loader_load(resource_loader* self, const char* name, resource* out_resource) {
if (self == 0 || name == 0 || out_resource == 0) {
return false;
}
// 构建完整路径
char full_path[512];
string_format(full_path, "%s%s%s", self->type_path, name, ".kmt");
// 读取文件
file_handle f;
if (!filesystem_open(full_path, FILE_MODE_READ, false, &f)) {
KERROR("material_loader_load - Failed to open file: %s", full_path);
return false;
}
// 读取配置
material_config* resource_data = kallocate(sizeof(material_config), MEMORY_TAG_MATERIAL);
// 设置默认值
resource_data->auto_release = false;
string_ncopy(resource_data->name, name, MATERIAL_NAME_MAX_LENGTH);
resource_data->diffuse_colour = vec4_one(); // 默认白色
kzero_memory(resource_data->diffuse_map_name, TEXTURE_NAME_MAX_LENGTH);
// 逐行解析
char line_buf[512] = "";
char* p = &line_buf[0];
u64 line_length = 0;
while (filesystem_read_line(&f, 511, &p, &line_length)) {
// 去除前后空格
char* trimmed = string_trim(line_buf);
line_length = string_length(trimmed);
// 跳过注释和空行
if (line_length < 1 || trimmed[0] == '#') {
continue;
}
// 解析 key=value
i32 equal_index = string_index_of(trimmed, '=');
if (equal_index == -1) {
KWARN("Potential formatting issue found in file '%s': '=' token not found", full_path);
continue;
}
// 提取 key 和 value
char key[64] = "";
string_mid(key, trimmed, 0, equal_index);
char* key_trimmed = string_trim(key);
char value[512] = "";
string_mid(value, trimmed, equal_index + 1, -1);
char* value_trimmed = string_trim(value);
// 解析具体配置
if (strings_equali(key_trimmed, "version")) {
// 版本号
} else if (strings_equali(key_trimmed, "name")) {
string_ncopy(resource_data->name, value_trimmed, MATERIAL_NAME_MAX_LENGTH);
} else if (strings_equali(key_trimmed, "diffuse_map_name")) {
string_ncopy(resource_data->diffuse_map_name, value_trimmed, TEXTURE_NAME_MAX_LENGTH);
} else if (strings_equali(key_trimmed, "diffuse_colour")) {
// 解析颜色 (r g b a)
if (!string_to_vec4(value_trimmed, &resource_data->diffuse_colour)) {
KWARN("Failed to parse diffuse_colour in file '%s'", full_path);
}
}
}
filesystem_close(&f);
// 填充 resource
out_resource->full_path = string_duplicate(full_path);
out_resource->name = name;
out_resource->data = resource_data;
out_resource->data_size = sizeof(material_config);
out_resource->loader_id = self->id;
return true;
}
static void material_loader_unload(resource_loader* self, resource* resource) {
if (resource) {
if (resource->data) {
kfree(resource->data, sizeof(material_config), MEMORY_TAG_MATERIAL);
}
if (resource->full_path) {
u32 length = string_length(resource->full_path);
kfree(resource->full_path, length + 1, MEMORY_TAG_STRING);
}
kzero_memory(resource, sizeof(resource));
}
}
🔗 资源系统集成
系统初始化
在资源系统初始化时注册所有内置加载器:
// engine/src/systems/resource_system.c
b8 resource_system_initialize(u64* memory_requirement, void* state, resource_system_config config) {
if (config.max_loader_count == 0) {
KFATAL("resource_system_initialize - config.max_loader_count must be > 0");
return false;
}
*memory_requirement = sizeof(resource_system_state);
if (state == 0) {
return true;
}
state_ptr = state;
state_ptr->config = config;
// 初始化加载器数组
for (u32 i = 0; i < config.max_loader_count; ++i) {
state_ptr->registered_loaders[i].id = INVALID_ID;
}
KINFO("Resource system initialized with %d loader slots", config.max_loader_count);
// 注册内置加载器
resource_loader text_loader;
text_loader.type = RESOURCE_TYPE_TEXT;
text_loader.custom_type = 0;
text_loader.type_path = "assets/text/";
text_loader.load = text_loader_load;
text_loader.unload = text_loader_unload;
resource_system_register_loader(&text_loader);
resource_loader binary_loader;
binary_loader.type = RESOURCE_TYPE_BINARY;
binary_loader.custom_type = 0;
binary_loader.type_path = "assets/";
binary_loader.load = binary_loader_load;
binary_loader.unload = binary_loader_unload;
resource_system_register_loader(&binary_loader);
resource_loader image_loader;
image_loader.type = RESOURCE_TYPE_IMAGE;
image_loader.custom_type = 0;
image_loader.type_path = "assets/textures/";
image_loader.load = image_loader_load;
image_loader.unload = image_loader_unload;
resource_system_register_loader(&image_loader);
resource_loader material_loader;
material_loader.type = RESOURCE_TYPE_MATERIAL;
material_loader.custom_type = 0;
material_loader.type_path = "assets/materials/";
material_loader.load = material_loader_load;
material_loader.unload = material_loader_unload;
resource_system_register_loader(&material_loader);
return true;
}
加载资源
统一的加载接口:
b8 resource_system_load(const char* name, resource_type type, resource* out_resource) {
if (state_ptr == 0 || name == 0 || out_resource == 0) {
return false;
}
// 查找对应类型的加载器
resource_loader* loader = 0;
for (u32 i = 0; i < state_ptr->config.max_loader_count; ++i) {
if (state_ptr->registered_loaders[i].id != INVALID_ID &&
state_ptr->registered_loaders[i].type == type) {
loader = &state_ptr->registered_loaders[i];
break;
}
}
if (loader == 0) {
KERROR("resource_system_load - No loader for type %d", type);
return false;
}
// 调用加载器
KINFO("Loading resource '%s' with loader id %d", name, loader->id);
return loader->load(loader, name, out_resource);
}
void resource_system_unload(resource* resource) {
if (state_ptr == 0 || resource == 0) {
return;
}
// 查找加载器
if (resource->loader_id >= state_ptr->config.max_loader_count) {
KERROR("resource_system_unload - Invalid loader_id %d", resource->loader_id);
return;
}
resource_loader* loader = &state_ptr->registered_loaders[resource->loader_id];
if (loader->id == INVALID_ID) {
KERROR("resource_system_unload - Loader %d is not registered", resource->loader_id);
return;
}
KINFO("Unloading resource '%s' with loader id %d", resource->name, loader->id);
loader->unload(loader, resource);
}
与现有系统集成
纹理系统可以使用资源系统加载图像:
// engine/src/systems/texture_system.c (修改后)
b8 texture_system_load(const char* texture_name, texture* t) {
// 使用资源系统加载图像
resource image_resource;
if (!resource_system_load(texture_name, RESOURCE_TYPE_IMAGE, &image_resource)) {
KERROR("Failed to load texture '%s'", texture_name);
return false;
}
// 提取图像数据
image_resource_data* data = (image_resource_data*)image_resource.data;
// 创建纹理
t->width = data->width;
t->height = data->height;
t->channel_count = data->channel_count;
// 上传到 GPU
renderer_create_texture(data->pixels, t);
// 卸载资源 (像素数据已上传到 GPU)
resource_system_unload(&image_resource);
return true;
}
材质系统同样可以使用资源系统:
// engine/src/systems/material_system.c (修改后)
material* material_system_acquire(const char* material_name) {
// 使用资源系统加载材质配置
resource material_resource;
if (!resource_system_load(material_name, RESOURCE_TYPE_MATERIAL, &material_resource)) {
KERROR("Failed to load material '%s'", material_name);
return 0;
}
material_config* config = (material_config*)material_resource.data;
// 创建材质
material* m = create_material(config);
// 卸载配置资源
resource_system_unload(&material_resource);
return m;
}
⚙️ 自定义加载器
开发者可以注册自定义加载器以支持新的资源类型:
// 示例:音频加载器
typedef struct audio_resource_data {
u32 sample_rate;
u32 channel_count;
u32 sample_count;
f32* samples;
} audio_resource_data;
static b8 audio_loader_load(resource_loader* self, const char* name, resource* out_resource) {
// 构建路径
char full_path[512];
string_format(full_path, "%s%s%s", self->type_path, name, ".wav");
// 打开文件
file_handle f;
if (!filesystem_open(full_path, FILE_MODE_READ, true, &f)) {
KERROR("audio_loader_load - Failed to open file: %s", full_path);
return false;
}
// 解析 WAV 文件头
// ... (解析 RIFF, fmt, data chunks)
audio_resource_data* audio_data = kallocate(sizeof(audio_resource_data), MEMORY_TAG_AUDIO);
// 读取音频数据
// ...
filesystem_close(&f);
// 填充 resource
out_resource->full_path = string_duplicate(full_path);
out_resource->name = name;
out_resource->data = audio_data;
out_resource->data_size = audio_data->sample_count * sizeof(f32) * audio_data->channel_count;
out_resource->loader_id = self->id;
return true;
}
static void audio_loader_unload(resource_loader* self, resource* resource) {
if (resource) {
if (resource->data) {
audio_resource_data* data = (audio_resource_data*)resource->data;
if (data->samples) {
kfree(data->samples, resource->data_size, MEMORY_TAG_AUDIO);
}
kfree(data, sizeof(audio_resource_data), MEMORY_TAG_AUDIO);
}
if (resource->full_path) {
u32 length = string_length(resource->full_path);
kfree(resource->full_path, length + 1, MEMORY_TAG_STRING);
}
kzero_memory(resource, sizeof(resource));
}
}
// 注册音频加载器
void register_audio_loader() {
resource_loader audio_loader;
audio_loader.type = RESOURCE_TYPE_CUSTOM;
audio_loader.custom_type = "audio";
audio_loader.type_path = "assets/audio/";
audio_loader.load = audio_loader_load;
audio_loader.unload = audio_loader_unload;
if (resource_system_register_loader(&audio_loader)) {
KINFO("Audio loader registered successfully");
}
}
// 使用音频加载器
resource audio_resource;
if (resource_system_load("background_music", RESOURCE_TYPE_CUSTOM, &audio_resource)) {
audio_resource_data* audio = (audio_resource_data*)audio_resource.data;
KINFO("Loaded audio: %d Hz, %d channels, %d samples",
audio->sample_rate, audio->channel_count, audio->sample_count);
// 播放音频
audio_system_play(audio);
// 卸载
resource_system_unload(&audio_resource);
}
❓ 常见问题
1. 为什么需要资源系统?直接在各个系统里加载文件不行吗?问题:
- 代码重复:每个系统都要实现文件读取、路径处理
- 难以维护:修改文件格式需要改多处
- 不一致:错误处理、日志输出不统一
- 难以扩展:添加新类型需要修改多个系统
资源系统的优势:
- 统一接口:所有资源通过
resource_system_load()加载 - 可插拔:新增类型只需注册新加载器
- 易于测试:可以 mock 加载器进行单元测试
- 统一管理:所有资源生命周期在一处管理
没有资源系统:
┌────────────┐ ┌────────────┐ ┌────────────┐
│ System A │ │ System B │ │ System C │
│ load_A() │ │ load_B() │ │ load_C() │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ │ │
└───────────────┴───────────────┘
重复的文件操作代码
有资源系统:
┌────────────┐ ┌────────────┐ ┌────────────┐
│ System A │ │ System B │ │ System C │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ │ │
└───────────────┴───────────────┘
│
┌──────────▼──────────┐
│ Resource System │
│ 统一的加载接口 │
└─────────────────────┘
2. resource_loader 的 id 和 type 有什么区别?
区别:
-
type (resource_type): 资源的类型,是枚举值
RESOURCE_TYPE_TEXT- 文本RESOURCE_TYPE_IMAGE- 图像RESOURCE_TYPE_MATERIAL- 材质- 用于查找加载器 (根据类型找到对应加载器)
-
id (u32): 加载器的实例 ID,是唯一标识
- 在注册时自动分配 (数组索引)
- 用于反向查找 (卸载时根据 ID 找到加载器)
类型 vs ID:
┌──────────────────────────────────┐
│ registered_loaders 数组 │
├──────────────────────────────────┤
│ [0] id=0, type=RESOURCE_TYPE_TEXT │ ← ID 是数组索引
│ [1] id=1, type=RESOURCE_TYPE_BINARY │ Type 是资源类型
│ [2] id=2, type=RESOURCE_TYPE_IMAGE │
│ [3] id=3, type=RESOURCE_TYPE_MATERIAL │
│ [4] id=INVALID_ID (未使用) │
└──────────────────────────────────┘
加载流程:
1. 用户调用: resource_system_load("test", RESOURCE_TYPE_IMAGE, ...)
2. 遍历数组,找到 type == RESOURCE_TYPE_IMAGE 的加载器
3. 调用 loader->load()
4. 保存 loader_id 到 resource->loader_id
卸载流程:
1. 用户调用: resource_system_unload(&resource)
2. 根据 resource->loader_id 直接访问数组 [loader_id]
3. 调用 loader->unload()
3. 自定义加载器如何设置 custom_type?
使用 custom_type 的场景:
当你需要加载引擎预定义类型之外的资源时,使用 RESOURCE_TYPE_CUSTOM + custom_type:
// 音频加载器
resource_loader audio_loader;
audio_loader.type = RESOURCE_TYPE_CUSTOM; // 自定义类型
audio_loader.custom_type = "audio"; // 自定义类型名称
audio_loader.type_path = "assets/audio/";
// 场景加载器
resource_loader scene_loader;
scene_loader.type = RESOURCE_TYPE_CUSTOM;
scene_loader.custom_type = "scene"; // 不同的自定义类型
scene_loader.type_path = "assets/scenes/";
// 字体加载器
resource_loader font_loader;
font_loader.type = RESOURCE_TYPE_CUSTOM;
font_loader.custom_type = "font";
font_loader.type_path = "assets/fonts/";
查找自定义加载器:
resource_loader* find_custom_loader(const char* custom_type) {
for (u32 i = 0; i < state_ptr->config.max_loader_count; ++i) {
resource_loader* loader = &state_ptr->registered_loaders[i];
if (loader->id != INVALID_ID &&
loader->type == RESOURCE_TYPE_CUSTOM &&
strings_equal(loader->custom_type, custom_type)) {
return loader;
}
}
return 0;
}
为什么不直接添加新的 resource_type 枚举值?
因为 resource_type 是引擎核心枚举,添加新值需要修改引擎代码。使用 custom_type 可以在不修改引擎的情况下扩展新类型。
分层设计:
资源系统负责文件到内存的转换,而不涉及 GPU 资源:
文件系统层:
磁盘上的文件 (test.png)
│
▼
资源系统层:
内存中的数据 (image_resource_data)
- width, height, channel_count
- pixels (CPU 内存)
│
▼
渲染系统层:
GPU 资源 (texture)
- VkImage
- VkImageView
- VkSampler
职责分离:
-
资源系统: 只负责加载数据
- 读取文件
- 解码格式 (PNG → 像素数组)
- 返回 CPU 内存中的数据
-
纹理系统: 负责GPU 资源管理
- 上传像素数据到 GPU
- 创建 VkImage、VkImageView
- 管理纹理生命周期
// 正确的使用方式:
// 1. 资源系统加载图像数据
resource image_resource;
resource_system_load("test.png", RESOURCE_TYPE_IMAGE, &image_resource);
image_resource_data* data = (image_resource_data*)image_resource.data;
// 2. 纹理系统使用数据创建 GPU 纹理
texture* t = texture_system_acquire("test");
renderer_create_texture(data->pixels, t); // 上传到 GPU
// 3. 卸载 CPU 数据 (GPU 已有副本)
resource_system_unload(&image_resource);
// 4. 后续使用纹理 (从 GPU 读取)
renderer_bind_texture(t);
优势:
- 资源系统不依赖渲染 API (可以换 OpenGL/DirectX)
- 可以加载图像用于非渲染目的 (如 CPU 图像处理)
- 测试更容易 (不需要初始化 GPU)
当前实现不缓存:
资源系统只负责加载和卸载,不负责缓存和引用计数:
// 每次调用都会重新加载
resource res1, res2;
resource_system_load("test.png", RESOURCE_TYPE_IMAGE, &res1); // 从磁盘加载
resource_system_load("test.png", RESOURCE_TYPE_IMAGE, &res2); // 再次从磁盘加载
// 两次独立的内存
res1.data != res2.data
// 需要分别卸载
resource_system_unload(&res1);
resource_system_unload(&res2);
缓存由上层系统负责:
// 纹理系统缓存纹理
texture* t1 = texture_system_acquire("test"); // 加载
texture* t2 = texture_system_acquire("test"); // 返回缓存
t1 == t2 // 同一个指针
// 引用计数
texture_system_release("test"); // ref_count--
texture_system_release("test"); // ref_count-- → 卸载
为什么不在资源系统缓存?
- 职责单一: 资源系统只做加载/卸载
- 灵活性: 不同资源类型有不同的缓存策略
- 纹理: 长期缓存 (GPU 内存贵)
- 配置文件: 不缓存 (支持热重载)
- 音频: 短期缓存 (磁盘 I/O 慢)
- 引用计数: 由专门的系统管理更合适
架构:
┌──────────────────┐
│ Texture System │ ← 缓存、引用计数、GPU 资源
├──────────────────┤
│ Material System │ ← 缓存、引用计数
├──────────────────┤
│ Resource System │ ← 只负责加载/卸载
└──────────────────┘
📝 练习
练习 1: 实现 JSON 加载器任务: 实现一个 JSON 文件加载器,返回解析后的 JSON 对象。
// json_loader.c
typedef struct json_object {
// 使用 cJSON 或自定义 JSON 解析器
void* root; // cJSON* root
} json_object;
static b8 json_loader_load(resource_loader* self, const char* name, resource* out_resource) {
// 1. 构建路径: assets/config/{name}.json
// 2. 读取文件内容
// 3. 使用 cJSON_Parse() 解析
// 4. 填充 out_resource
// TODO: 实现
return true;
}
static void json_loader_unload(resource_loader* self, resource* resource) {
// 1. 调用 cJSON_Delete() 释放 JSON 对象
// 2. 释放路径字符串
// TODO: 实现
}
使用:
resource json_res;
if (resource_system_load("config", RESOURCE_TYPE_CUSTOM, &json_res)) {
json_object* json = (json_object*)json_res.data;
// 读取配置
cJSON* root = (cJSON*)json->root;
const char* title = cJSON_GetObjectItem(root, "title")->valuestring;
int width = cJSON_GetObjectItem(root, "width")->valueint;
resource_system_unload(&json_res);
}
练习 2: 实现资源缓存
任务: 为资源系统添加缓存功能,避免重复加载。
// resource_system.c
typedef struct cached_resource {
char name[256];
resource_type type;
resource resource;
u32 ref_count;
} cached_resource;
typedef struct resource_system_state {
resource_system_config config;
resource_loader registered_loaders[MAX_LOADER_COUNT];
// 缓存
cached_resource* cache;
u32 cache_count;
u32 cache_capacity;
} resource_system_state;
b8 resource_system_load(const char* name, resource_type type, resource* out_resource) {
// 1. 检查缓存
for (u32 i = 0; i < state_ptr->cache_count; ++i) {
cached_resource* cached = &state_ptr->cache[i];
if (cached->type == type && strings_equal(cached->name, name)) {
// 命中缓存
cached->ref_count++;
*out_resource = cached->resource;
KINFO("Cache hit for '%s' (ref_count=%d)", name, cached->ref_count);
return true;
}
}
// 2. 未命中,加载资源
resource_loader* loader = find_loader_for_type(type);
if (!loader->load(loader, name, out_resource)) {
return false;
}
// 3. 添加到缓存
if (state_ptr->cache_count >= state_ptr->cache_capacity) {
// 扩展缓存数组
// ...
}
cached_resource* cached = &state_ptr->cache[state_ptr->cache_count++];
string_ncopy(cached->name, name, 256);
cached->type = type;
cached->resource = *out_resource;
cached->ref_count = 1;
KINFO("Cached resource '%s'", name);
return true;
}
void resource_system_unload(resource* resource) {
// 1. 查找缓存条目
for (u32 i = 0; i < state_ptr->cache_count; ++i) {
cached_resource* cached = &state_ptr->cache[i];
if (cached->resource.data == resource->data) {
// 2. 减少引用计数
cached->ref_count--;
KINFO("Released '%s' (ref_count=%d)", cached->name, cached->ref_count);
// 3. 引用计数为 0 时真正卸载
if (cached->ref_count == 0) {
resource_loader* loader = &state_ptr->registered_loaders[resource->loader_id];
loader->unload(loader, &cached->resource);
// 从缓存移除 (交换到末尾再 pop)
cached_resource temp = state_ptr->cache[state_ptr->cache_count - 1];
state_ptr->cache[i] = temp;
state_ptr->cache_count--;
KINFO("Unloaded resource '%s'", cached->name);
}
return;
}
}
}
练习 3: 实现异步加载
任务: 实现异步资源加载,避免阻塞主线程。
// resource_system.h
typedef void (*resource_loaded_callback)(resource* resource, void* user_data);
/**
* @brief 异步加载资源
* @param name 资源名称
* @param type 资源类型
* @param callback 加载完成回调
* @param user_data 用户数据
* @return 异步任务 ID
*/
u32 resource_system_load_async(const char* name, resource_type type,
resource_loaded_callback callback, void* user_data);
/**
* @brief 取消异步加载
* @param task_id 任务 ID
*/
void resource_system_cancel_async(u32 task_id);
// resource_system.c
typedef struct async_load_task {
u32 id;
char name[256];
resource_type type;
resource_loaded_callback callback;
void* user_data;
b8 completed;
resource result;
} async_load_task;
// 工作线程函数
void resource_loader_worker_thread(void* arg) {
while (state_ptr->running) {
// 1. 从队列获取任务
async_load_task* task = pop_task_from_queue();
if (!task) {
// 等待新任务
thread_sleep(10);
continue;
}
// 2. 加载资源
resource_loader* loader = find_loader_for_type(task->type);
b8 success = loader->load(loader, task->name, &task->result);
// 3. 标记完成
task->completed = true;
// 4. 回调将在主线程执行 (通过 resource_system_update)
}
}
// 主线程更新函数
void resource_system_update() {
// 处理完成的异步任务
for (u32 i = 0; i < state_ptr->async_task_count; ++i) {
async_load_task* task = &state_ptr->async_tasks[i];
if (task->completed) {
// 在主线程执行回调
task->callback(&task->result, task->user_data);
// 移除任务
remove_task(i);
--i;
}
}
}
// 使用示例
void on_texture_loaded(resource* resource, void* user_data) {
texture* t = (texture*)user_data;
image_resource_data* data = (image_resource_data*)resource->data;
renderer_create_texture(data->pixels, t);
resource_system_unload(resource);
KINFO("Texture loaded asynchronously");
}
// 异步加载纹理
texture my_texture;
resource_system_load_async("large_texture", RESOURCE_TYPE_IMAGE,
on_texture_loaded, &my_texture);
恭喜!你已经掌握了资源系统的实现! 🎉
下一教程:034 静态网格加载
关注公众号「上手实验室」,获取更多游戏引擎开发教程!

被折叠的 条评论
为什么被折叠?



