教程 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

// 加载资源
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 有什么区别?

区别:

  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(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 可以在不修改引擎的情况下扩展新类型。

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.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 静态网格加载


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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值