上一篇:几何体系统 | 下一篇:多渲染通道 | 返回目录
📚 快速导航
目录
- 简介
- 学习目标
- 资源系统架构
- 为什么需要资源系统
- 统一加载接口
- 可插拔加载器
- 资源类型定义
- 资源加载器模式
- 加载器结构
- 加载器注册
- 内置加载器实现
- 文本加载器
- 二进制加载器
- 图像加载器
- 材质加载器
- 资源系统集成
- 自定义加载器
- 常见问题
- 练习
📖 简介
在之前的教程中,我们实现了多个独立的资源管理系统:纹理系统、材质系统、几何体系统。每个系统都有自己的加载逻辑,这导致代码重复和维护困难。
本教程将介绍资源系统(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// 加载资源b8resource_system_load(constchar*name,resource_type type,resource*out_resource);// 卸载资源voidresource_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);}可插拔加载器
资源系统通过注册加载器来支持不同的资源类型:
// 注册加载器b8resource_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 资源类型枚举 */typedefenumresource_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 资源结构 */typedefstructresource{u32 loader_id;// 加载器 IDconstchar*name;// 资源名称char*full_path;// 完整路径u64 data_size;// 数据大小void*data;// 资源数据}resource;/** * @brief 图像资源数据 */typedefstructimage_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 资源加载器结构 */typedefstructresource_loader{u32 id;// 加载器 IDresource_type type;// 资源类型constchar*custom_type;// 自定义类型名称constchar*type_path;// 资源路径 (如 "textures/")/** * @brief 加载资源 * @param self 加载器自身 * @param name 资源名称 * @param out_resource 输出资源 * @return 成功返回 true */b8(*load)(structresource_loader*self,constchar*name,resource*out_resource);/** * @brief 卸载资源 * @param self 加载器自身 * @param resource 要卸载的资源 */void(*unload)(structresource_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#defineMAX_LOADER_COUNT32typedefstructresource_system_state{resource_system_config config;resource_loader registered_loaders[MAX_LOADER_COUNT];}resource_system_state;staticresource_system_state*state_ptr=0;b8resource_system_register_loader(resource_loader*loader){if(state_ptr==0){returnfalse;}// 分配加载器 IDu32 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);returntrue;}}KERROR("resource_system_register_loader - No available loader slots");returnfalse;}🔧 内置加载器实现
文本加载器
加载纯文本文件:
// engine/src/resources/loaders/text_loader.ctypedefstructtext_loader{// 基础路径 (如 "assets/text/")charbase_path[512];}text_loader;staticb8text_loader_load(resource_loader*self,constchar*name,resource*out_resource){if(self==0||name==0||out_resource==0){returnfalse;}text_loader*loader=(text_loader*)self;// 构建完整路径charfull_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);returnfalse;}// 获取文件大小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);returnfalse;}// 分配内存 (+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);returnfalse;}filesystem_close(&f);// 添加 null terminatorresource_data[file_size]='\0';// 填充 resourceout_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;returntrue;}staticvoidtext_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.cstaticb8binary_loader_load(resource_loader*self,constchar*name,resource*out_resource){if(self==0||name==0||out_resource==0){returnfalse;}// 构建完整路径 (不添加扩展名,使用原始名称)charfull_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);returnfalse;}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);returnfalse;}// 分配内存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);returnfalse;}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;returntrue;}staticvoidbinary_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#defineSTB_IMAGE_IMPLEMENTATION#include"vendor/stb_image.h"staticb8image_loader_load(resource_loader*self,constchar*name,resource*out_resource){if(self==0||name==0||out_resource==0){returnfalse;}// 构建完整路径charfull_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);returnfalse;}resource_data->width=width;resource_data->height=height;resource_data->channel_count=channel_count;// 填充 resourceout_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);returntrue;}staticvoidimage_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.ctypedefstructmaterial_config{charname[MATERIAL_NAME_MAX_LENGTH];b8 auto_release;vec4 diffuse_colour;chardiffuse_map_name[TEXTURE_NAME_MAX_LENGTH];}material_config;staticb8material_loader_load(resource_loader*self,constchar*name,resource*out_resource){if(self==0||name==0||out_resource==0){returnfalse;}// 构建完整路径charfull_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);returnfalse;}// 读取配置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);// 逐行解析charline_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=valuei32 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 和 valuecharkey[64]="";string_mid(key,trimmed,0,equal_index);char*key_trimmed=string_trim(key);charvalue[512]="";string_mid(value,trimmed,equal_index+1,-1);char*value_trimmed=string_trim(value);// 解析具体配置if(strings_equali(key_trimmed,"version")){// 版本号}elseif(strings_equali(key_trimmed,"name")){string_ncopy(resource_data->name,value_trimmed,MATERIAL_NAME_MAX_LENGTH);}elseif(strings_equali(key_trimmed,"diffuse_map_name")){string_ncopy(resource_data->diffuse_map_name,value_trimmed,TEXTURE_NAME_MAX_LENGTH);}elseif(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);// 填充 resourceout_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;returntrue;}staticvoidmaterial_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.cb8resource_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");returnfalse;}*memory_requirement=sizeof(resource_system_state);if(state==0){returntrue;}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);returntrue;}加载资源
统一的加载接口:
b8resource_system_load(constchar*name,resource_type type,resource*out_resource){if(state_ptr==0||name==0||out_resource==0){returnfalse;}// 查找对应类型的加载器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);returnfalse;}// 调用加载器KINFO("Loading resource '%s' with loader id %d",name,loader->id);returnloader->load(loader,name,out_resource);}voidresource_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 (修改后)b8texture_system_load(constchar*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);returnfalse;}// 提取图像数据image_resource_data*data=(image_resource_data*)image_resource.data;// 创建纹理t->width=data->width;t->height=data->height;t->channel_count=data->channel_count;// 上传到 GPUrenderer_create_texture(data->pixels,t);// 卸载资源 (像素数据已上传到 GPU)resource_system_unload(&image_resource);returntrue;}材质系统同样可以使用资源系统:
// engine/src/systems/material_system.c (修改后)material*material_system_acquire(constchar*material_name){// 使用资源系统加载材质配置resource material_resource;if(!resource_system_load(material_name,RESOURCE_TYPE_MATERIAL,&material_resource)){KERROR("Failed to load material '%s'",material_name);return0;}material_config*config=(material_config*)material_resource.data;// 创建材质material*m=create_material(config);// 卸载配置资源resource_system_unload(&material_resource);returnm;}⚙️ 自定义加载器
开发者可以注册自定义加载器以支持新的资源类型:
// 示例:音频加载器typedefstructaudio_resource_data{u32 sample_rate;u32 channel_count;u32 sample_count;f32*samples;}audio_resource_data;staticb8audio_loader_load(resource_loader*self,constchar*name,resource*out_resource){// 构建路径charfull_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);returnfalse;}// 解析 WAV 文件头// ... (解析 RIFF, fmt, data chunks)audio_resource_data*audio_data=kallocate(sizeof(audio_resource_data),MEMORY_TAG_AUDIO);// 读取音频数据// ...filesystem_close(&f);// 填充 resourceout_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;returntrue;}staticvoidaudio_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));}}// 注册音频加载器voidregister_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(constchar*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)){returnloader;}}return0;}为什么不直接添加新的 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.ctypedefstructjson_object{// 使用 cJSON 或自定义 JSON 解析器void*root;// cJSON* root}json_object;staticb8json_loader_load(resource_loader*self,constchar*name,resource*out_resource){// 1. 构建路径: assets/config/{name}.json// 2. 读取文件内容// 3. 使用 cJSON_Parse() 解析// 4. 填充 out_resource// TODO: 实现returntrue;}staticvoidjson_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;constchar*title=cJSON_GetObjectItem(root,"title")->valuestring;intwidth=cJSON_GetObjectItem(root,"width")->valueint;resource_system_unload(&json_res);}练习 2: 实现资源缓存任务:为资源系统添加缓存功能,避免重复加载。
// resource_system.ctypedefstructcached_resource{charname[256];resource_type type;resource resource;u32 ref_count;}cached_resource;typedefstructresource_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;b8resource_system_load(constchar*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);returntrue;}}// 2. 未命中,加载资源resource_loader*loader=find_loader_for_type(type);if(!loader->load(loader,name,out_resource)){returnfalse;}// 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);returntrue;}voidresource_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.htypedefvoid(*resource_loaded_callback)(resource*resource,void*user_data);/** * @brief 异步加载资源 * @param name 资源名称 * @param type 资源类型 * @param callback 加载完成回调 * @param user_data 用户数据 * @return 异步任务 ID */u32resource_system_load_async(constchar*name,resource_type type,resource_loaded_callback callback,void*user_data);/** * @brief 取消异步加载 * @param task_id 任务 ID */voidresource_system_cancel_async(u32 task_id);// resource_system.ctypedefstructasync_load_task{u32 id;charname[256];resource_type type;resource_loaded_callback callback;void*user_data;b8 completed;resource result;}async_load_task;// 工作线程函数voidresource_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)}}// 主线程更新函数voidresource_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;}}}// 使用示例voidon_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 静态网格加载
关注公众号「上手实验室」,获取更多游戏引擎开发教程!