news 2026/2/7 2:46:32

教程 33 - 资源系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
教程 33 - 资源系统

上一篇:几何体系统 | 下一篇:多渲染通道 | 返回目录


📚 快速导航


目录
  • 简介
  • 学习目标
  • 资源系统架构
    • 为什么需要资源系统
    • 统一加载接口
    • 可插拔加载器
  • 资源类型定义
  • 资源加载器模式
    • 加载器结构
    • 加载器注册
  • 内置加载器实现
    • 文本加载器
    • 二进制加载器
    • 图像加载器
    • 材质加载器
  • 资源系统集成
  • 自定义加载器
  • 常见问题
  • 练习

📖 简介

在之前的教程中,我们实现了多个独立的资源管理系统:纹理系统、材质系统、几何体系统。每个系统都有自己的加载逻辑,这导致代码重复和维护困难。

本教程将介绍资源系统(Resource System),它提供了一个统一的、可扩展的资源加载框架。通过加载器模式(Loader Pattern),资源系统将资源加载逻辑模块化,让不同类型的资源可以通过注册加载器来实现加载。

Existing Systems 现有系统
File System 文件系统
Resource System 资源系统
Loaders 加载器
Texture System
纹理系统
Material System
材质系统
File System
文件系统
Resource System
资源系统核心
Text Loader
文本加载器
Binary Loader
二进制加载器
Image Loader
图像加载器
Material Loader
材质加载器
Custom Loader
自定义加载器

🎯 学习目标

目标描述
理解资源系统的必要性了解为什么需要统一的资源加载框架
掌握加载器模式学习可插拔加载器的设计和实现
实现内置加载器实现文本、二进制、图像、材质加载器
资源类型管理理解资源类型枚举和扩展机制
系统集成将资源系统与现有系统集成

🏗️ 资源系统架构

为什么需要资源系统

在没有资源系统之前,每个系统都有自己的加载逻辑:

❌ 问题: ┌─────────────────┐ │ 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 有什么区别?

区别:

  1. type (resource_type): 资源的类型,是枚举值

    • RESOURCE_TYPE_TEXT- 文本
    • RESOURCE_TYPE_IMAGE- 图像
    • RESOURCE_TYPE_MATERIAL- 材质
    • 用于查找加载器(根据类型找到对应加载器)
  2. 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可以在不修改引擎的情况下扩展新类型。

4. 为什么图像加载器返回的是 image_resource_data,而不是直接返回 texture?

分层设计:

资源系统负责文件到内存的转换,而不涉及 GPU 资源:

文件系统层: 磁盘上的文件 (test.png) │ ▼ 资源系统层: 内存中的数据 (image_resource_data) - width, height, channel_count - pixels (CPU 内存) │ ▼ 渲染系统层: GPU 资源 (texture) - VkImage - VkImageView - VkSampler

职责分离:

  1. 资源系统: 只负责加载数据

    • 读取文件
    • 解码格式 (PNG → 像素数组)
    • 返回 CPU 内存中的数据
  2. 纹理系统: 负责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)
5. 资源系统会缓存资源吗?

当前实现不缓存:

资源系统只负责加载和卸载,不负责缓存和引用计数:

// 每次调用都会重新加载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-- → 卸载

为什么不在资源系统缓存?

  1. 职责单一: 资源系统只做加载/卸载
  2. 灵活性: 不同资源类型有不同的缓存策略
    • 纹理: 长期缓存 (GPU 内存贵)
    • 配置文件: 不缓存 (支持热重载)
    • 音频: 短期缓存 (磁盘 I/O 慢)
  3. 引用计数: 由专门的系统管理更合适
架构: ┌──────────────────┐ │ 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 静态网格加载


关注公众号「上手实验室」,获取更多游戏引擎开发教程!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/4 20:44:14

[光学原理与应用-491]:水冷机、零气模块CDA、功率计等影响266皮秒紫外激光器的种子源1064nm功率稳定性结果的主要因素有哪些?

影响 266 nm 皮秒紫外激光器种子源&#xff08;1064 nm&#xff09;功率稳定性 的关键因素涉及多个子系统&#xff0c;其中你提到的 水冷机、零气模块&#xff08;CDA&#xff09;、功率计 都是重要的外部支撑或监测设备。下面我们从系统工程角度&#xff0c;系统性地分析这些设…

作者头像 李华
网站建设 2026/2/7 2:11:47

昆仑通态MCGS与欧姆龙E5CC温控器通讯实战:PID模式及输出启停控制

昆仑通态MCGS与欧姆龙E5CC温控器通讯PID模式输出启停(KUNL-1) 功能&#xff1a;通过昆仑通态对欧姆龙E5CC温控器 设定温度&#xff0c;读取温控&#xff0c;控制输出启停&#xff0c;切换PID/ON-OFF控制&#xff0c;PID自整定调整。 反应灵敏&#xff0c;通讯稳定可靠。 器件&a…

作者头像 李华
网站建设 2026/2/6 21:38:06

通达信〖逆势突破强牛〗指标公式 逆市环境中率先突破前期重要压力位 较强内在上涨动力

通达信〖逆势突破强牛〗指标公式 逆市环境中率先突破前期重要压力位 较强内在上涨动力 今天介绍的这款工具正是为了识别那些在逆市环境中依然能够强势突破的个股信号。 这套分析方法通过捕捉价格运行的特殊状态来定位潜在机会。 它首先会标记出近期的一个关键高位位置&#…

作者头像 李华
网站建设 2026/2/6 3:18:55

AEB联合仿真算法设计:Carsim2019.0+Matlab/Simulink2021a实现...

AEB联合仿真算法设计 软件使用&#xff1a;Carsim2019.0Matlab/Simulink2021a 适用场景&#xff1a;采用模块化建模方法&#xff0c;搭建AEB仿真算法&#xff0c;适用于直线驾驶工况场景。 包含模块&#xff1a;Carsim模块&#xff0c;function函数逻辑模块&#xff0c;每个模块…

作者头像 李华
网站建设 2026/2/3 22:37:50

Java毕设选题推荐:基于springboot个人博客系统的设计与实现基于SpringBoot+Vue个人博客系统的设计与实现【附源码、mysql、文档、调试+代码讲解+全bao等】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华