导读
Amethyst 是一款用 Rust 编写的数据导向(Data-oriented)游戏引擎,其资源系统(amethyst_assets)以"可扩展"、"异步并行"(内部基于 Rayon)和"多来源"为设计目标,支持把任意 Rust 数据类型注册成可加载、可异步处理的游戏资源。本指南以官方文档 如何定义自定义资源 为主体,结合仓库源码与真实示例(examples/asset_custom/main.rs、examples/asset_loading/main.rs),完整讲解定义一个新资源类型的四步流程:定义类型与句柄、定义可序列化数据形式、实现Assettrait、实现ProcessableAssettrait,并延伸讲解资源注册、加载与异步等待、以及自定义格式等进阶主题。读完本文,你将能够在自己的 Amethyst 应用中定义、加载并使用任意自定义资源类型。
为什么需要自定义资源:理解 Amethyst 资源系统的分层
在深入四步流程之前,先理解资源系统各层的职责,有助于写出正确的实现。在 amethyst_assets/src/asset.rs 中,资源系统围绕三个核心 trait 展开:
Asset:描述"资源本身"的 trait,一个类型只要实现它,就可以像Mesh、Texture、Terrain一样成为资源。它通过关联类型Data声明该资源可以"从哪种中间数据创建而来"——例如网格的顶点数据、音频的采样数据。Format<D>:负责"从字节到资源数据"的转换,对应文件扩展名(如Png、Obj、Wave),通常由资源文件格式决定。ProcessableAsset:负责"从资源数据到最终资源"的转换,也就是把反序列化得到的Data加工成真正可用的资源实例。
三者的关系可以概括为一条流水线:
磁盘文件 --(Format 导入)--> 资源数据 Data --(ProcessableAsset::process)--> 资源实例 A --(存入 AssetStorage)--> 通过 Handle 异步获取Assettrait 的源码签名如下(见 amethyst_assets/src/asset.rs):
pub trait Asset: Send + Sync + 'static { /// An identifier for this asset used for debugging. fn name() -> &'static str; /// The `Data` type the asset can be created from. type Data: Send + Sync + 'static; }可以看到,资源类型本身必须满足Send + Sync + 'static(因为它会被并行处理和跨线程共享),Data也有同样的约束。name()返回的字符串主要用于调试日志,例如 processor.rs 中AssetProcessorSystem的系统名就是format!("Asset Processor: {}", A::name())。
第一步:定义资源类型与句柄
定义资源类型只需要一个普通的 Rust 结构体。以官方文档中的"能量冲击波"(Energy Blast)为例:
/// Custom asset representing an energy blast. #[derive(Clone, Debug, Default)] pub struct EnergyBlast { pub hp_damage: u32, pub mp_damage: u32, }此时它只是一个普通数据容器,尚不能参与资源加载。真正的"资源化"从第二步开始。
第二步:定义可序列化的资源数据形式(Data)
Asset::Data是资源加载的中间形态:磁盘上的文件内容会被反序列化成Data,再被加工成最终资源。定义Data有两种选择,文档中给出了两种形式:
方案 A:直接用资源类型本身作为Data
如果资源的序列化形式与最终形式完全一致,直接让资源类型派生Serialize、Deserialize和TypeUuid:
use serde::{Deserialize, Serialize}; use type_uuid::TypeUuid; #[derive(Clone, Debug, Default, Serialize, Deserialize, TypeUuid)] #[uuid = "00000000-0000-0000-0000-000000000001"] // generate this uuid yourself pub struct EnergyBlast { pub hp_damage: u32, pub mp_damage: u32, }注意这里必须派生TypeUuid并提供一个全局唯一的#[uuid]。TypeUuid是整个资源系统的"类型身份证":加载器通过它把磁盘上的资源与 Rust 类型关联起来。在 loader.rs 中,Loader::load正是用A::UUID构造AssetTypeId来查找资源类型的:
fn load<A: TypeUuid>(&self, path: &str) -> Handle<A> { Handle::new( self.ref_sender.clone(), self.loader .add_ref_indirect(IndirectIdentifier::PathWithType( path.to_string(), AssetTypeId(A::UUID), )), ) }方案 B:用枚举表达多种版本/数据布局
当同一资源可能存在不同数据布局(例如配置文件格式的版本演进)时,用一个独立枚举作为Data更合适:
/// Separate serializable type to support different versions /// of energy blast configuration. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum EnergyBlastData { /// Early version only could damage HP. Version1 { hp_damage: u32 }, /// Add support for subtracting MP. Version2 { hp_damage: u32, mp_damage: u32 }, }这样旧版本的配置文件依然可以被读取,并在process阶段被升级为新版本资源——这正是 examples/asset_custom/main.rs 的energy_blast.ron之外,官方文档特意展示第二种方案的用意。仓库中的真实示例采用方案 A(见 examples/asset_custom/main.rs),即type Data = Self;。
第三步:实现Assettrait
Assettrait 需要两个成员:type Data和name():
impl Asset for EnergyBlast { // use `Self` if the type is directly serialized. type Data = EnergyBlastData; fn name() -> &'static str { "EnergyBlast" } }从源码角度看,Asset还有一层隐式的"便捷实现":当Data == Self(资源类型直接序列化)时,amethyst_assets会自动提供一个ProcessableAsset的 blanket 实现(见 amethyst_assets/src/asset.rs),直接返回ProcessingState::Loaded(data),此时你甚至可以跳过第四步的完整自定义。这解释了为什么 examples/asset_custom/main.rs 中EnergyBlast只实现了Asset而没有显式实现ProcessableAsset——它依赖的正是这个 blanket 实现。
第四步:实现ProcessableAssettrait,把数据加工成资源
process方法的签名与职责
ProcessableAsset::process接收反序列化后的Data,将其转换为ProcessingState:
use amethyst::assets::{AssetStorage, LoadHandle, ProcessableAsset, ProcessingState}; impl ProcessableAsset for EnergyBlast { fn process( energy_blast_data: Self::Data, _storage: &mut AssetStorage<Self>, _handle: &LoadHandle, ) -> amethyst::Result<ProcessingState<Self::Data, Self>> { match energy_blast_data { EnergyBlastData::Version1 { hp_damage } => Ok(ProcessingState::Loaded(Self { hp_damage, ..Default::default() })), EnergyBlastData::Version2 { hp_damage, mp_damage, } => Ok(ProcessingState::Loaded(Self { hp_damage, mp_damage, })), } } }返回值的两种语义:Loaded与Loading
ProcessingState定义在 amethyst_assets/src/processor.rs:
pub enum ProcessingState<D, A> { /// Asset is not fully loaded yet, need to wait longer Loading(D), /// Asset have finished loading, can now be inserted into storage and tracker notified Loaded(A), }- 返回
ProcessingState::Loaded(asset):资源加工完成,会被写入AssetStorage并通知进度跟踪器(Tracker),随后可通过句柄获取。 - 返回
ProcessingState::Loading(data):资源尚未就绪,数据会被放回处理队列(requeue),等待下一次处理循环继续(见 processor.rs)。
谁在调用process:AssetProcessorSystem<A>的运转机制
AssetProcessorSystem<A>是每个资源类型的"加工车间"。它的System实现(见 amethyst_assets/src/processor.rs)会在每一帧:
- 清空"变更队列"(
changed队列,用于热重载场景); - 调用
queue.process(storage, ProcessableAsset::process),从处理队列中取出数据并执行ProcessableAsset::process; - 调用
storage.process_custom_drop释放被替换或移除的旧资源。
其核心循环ProcessingQueue::process(见 amethyst_assets/src/processor.rs)对每一条排队数据执行:若process返回Loaded(x),则通知加载完成并把资源写入存储(storage.update_asset,必要时commit_asset);若返回Loading(x)则重新入队;若返回Err则上报错误。这解释了文档中的一句话:"TheAssetProcessorSystem<A>system uses this trait to convert the deserialized asset data into the asset."
注册资源类型:让引擎认识你的资源
定义完 trait 实现还不够,必须让引擎把"数据 UUID"、"资源 UUID"、存储和加工系统关联起来。文档与源码都推荐使用register_asset_type!宏(见 amethyst_assets/src/loader.rs):
amethyst::assets::register_asset_type!( EnergyBlastData => EnergyBlast; AssetProcessorSystem<EnergyBlast> );该宏通过inventory机制向全局清单提交一个AssetType注册项(由 create_asset_type 构造),它携带了三段关键信息:
data_uuid:Data类型的 UUID(AssetTypeId(Intermediate::UUID));asset_uuid:资源类型的 UUID(AssetTypeId(Asset::UUID));- 三个函数指针:
create_storage(在 World 中创建AssetStorage<A>与ProcessingQueue<Data>资源)、register_system(把AssetProcessorSystem<A>加入 Dispatcher)、with_storage(桥接 distill 加载器与 Amethyst 存储)。
DefaultLoader初始化时会遍历这些注册项(见 loader.rs),自动完成资源创建与系统注册——这正是文档中"确保AssetProcessorSystem<A>被注册到 Dispatcher,可使用register_asset_type!宏"的底层含义。
实战:加载自定义资源并异步等待
资源定义完成后,如果Data使用 RON、JSON 等既有支持格式存储,即可直接通过Loader加载。以仓库示例 examples/asset_custom/main.rs 为例(该示例还演示了LoaderBundle与RenderingBundle的装配):
use amethyst::assets::{DefaultLoader, Loader}; pub struct LoadingState { /// Handle to the energy blast. energy_blast_handle: Option<Handle<EnergyBlast>>, } impl SimpleState for LoadingState { fn on_start(&mut self, data: StateData<'_, GameData>) { let loader = data.resources.get::<DefaultLoader>().unwrap(); let energy_blast_handle = loader.load("energy_blast.ron"); self.energy_blast_handle = Some(energy_blast_handle); } fn update(&mut self, data: &mut StateData<'_, GameData>) -> SimpleTrans { let energy_blast_assets = data.resources.get::<AssetStorage<EnergyBlast>>().unwrap(); if let Some(energy_blast) = energy_blast_assets.get(self.energy_blast_handle.as_ref().unwrap()) { println!("Loaded energy blast: {:?}", energy_blast); Trans::Quit } else { Trans::None } } } fn main() -> amethyst::Result<()> { let app_root = application_root_dir()?; let assets_dir = app_root.join("assets"); let game_data = DispatcherBuilder::default(); let mut game = Application::new( assets_dir, LoadingState { energy_blast_handle: None, }, game_data, )?; // uncomment to run this example // game.run(); Ok(()) }这个示例浓缩了三个关键点:
- 加载是异步的:
loader.load("energy_blast.ron")立即返回一个Handle<EnergyBlast>,真正加载在后台进行(通过 distill 加载器与ProcessingQueue协作)。 - 必须等待:在资源加载完成前,
AssetStorage::get(handle)返回None。因此update中需要轮询,直到拿到资源才进入下一状态(这里用Trans::Quit退出)。 - 存储按类型隔离:每种资源有独立的
AssetStorage<A>,通过data.resources.get::<AssetStorage<EnergyBlast>>()获取。
仓库中该示例对应的资源文件 energy_blast.ron 展示了 RON 数据的实际格式:
{ "a016abff-623d-48cf-a6e4-e76e069fe843": (hp_damage: 10, mp_damage: 10) }注意键是资源类型的 UUID(EnergyBlast的#[uuid]),(hp_damage: 10, mp_damage: 10)是 RON 结构体字面量,对应EnergyBlast { hp_damage: 10, mp_damage: 10 }。若采用方案 B(枚举Data),键同样应为Data类型的 UUID,值则写成对应的枚举变体。
延伸:数据格式不被支持时,定义自定义Format
文档明确指出:如果资源数据存储在 Amethyst 不支持的格式中,可以实现自定义Format并提供给Loader。完整流程见 如何定义自定义格式,核心要点如下。
Formattrait 定义在 amethyst_assets/src/asset.rs,通常只需要实现两个方法:
pub trait Format<D: 'static>: DynClone + Send + Sync + 'static { fn name(&self) -> &'static str; fn import_simple(&self, _bytes: Vec<u8>) -> amethyst_core::Result<D> { unimplemented!("You must implement either `import_simple` or `import`.") } }定义格式类型(需要Default、Clone、Copy、Serialize、Deserialize、TypeUuid),再实现Format,最后用register_importer!宏把文件扩展名与格式绑定(见 simple_importer.rs):
#[derive(Default, Clone, Copy, Serialize, Deserialize, TypeUuid)] #[uuid = "00000000-0000-0000-0000-000000000002"] // replace with your own uuid pub struct MyLangFormat; use amethyst::assets::Format; use ron::de::Deserializer; // replace this with your formats deserializer // EnergyBlast could be EnergyBlastData here. impl Format<EnergyBlast> for MyLangFormat { fn name(&self) -> &'static str { "MyLangFormat" } fn import_simple(&self, bytes: Vec<u8>) -> amethyst::Result<EnergyBlast> { let mut deserializer = Deserializer::from_bytes(&bytes)?; let val = EnergyBlast::deserialize(&mut deserializer)?; deserializer.end()?; Ok(val) } } amethyst::assets::register_importer!(".mylang", MyLangFormat);之后即可loader.load("energy_blast.mylang")加载该格式的资源。仓库示例 examples/asset_loading/main.rs 中定义了一个.custom格式的Custom实现,把每行 6 个浮点数的文本解析为网格顶点(位置、法线)并构造MeshBuilder,是自定义格式解析二进制/文本数据的另一个参考实现。
register_importer!的底层由SimpleImporter承担(见 simple_importer.rs):它为每个源文件分配稳定的资源 UUID,读取文件字节后调用options.import_simple(bytes),把结果封装为ImportedAsset交给 distill 导入管线。
底层链路一图看懂:从文件到Handle再到资源
综合 loader.rs、processor.rs 与 storage.rs 的实现,一次自定义资源加载的完整链路如下:
Loader::load(path)通过IndirectIdentifier::PathWithType(path, AssetTypeId(A::UUID))向 distill 加载器请求间接加载(见 loader.rs)。- distill 依据文件扩展名选择对应的
Formatimporter,读取文件字节并调用import_simple,产出资源数据Data(二进制中间格式经 bincode 反序列化,见 loader.rs)。 - 数据被
enqueue进ProcessingQueue<Data>(见 processor.rs)。 - 每帧由
AssetProcessorSystem<A>调用ProcessableAsset::process把Data加工成资源实例。 - 资源写入
AssetStorage<A>并提交(commit),此时AssetStorage::get(&handle)才能取到资源(见 storage.rs)。 - 在应用代码中,通过
data.resources.get::<AssetStorage<A>>()获取存储,用句柄取出资源使用。
需要说明的是,步骤 4 中的AssetStorage是"未提交"与"已提交"双表结构(见 storage.rs),新资源先进入uncommitted,提交后转入assets主表,句柄才能取到;同时旧版本资源会被放入to_drop队列延迟释放,为热重载(版本化加载)留出了空间。
结语与进一步阅读
定义自定义资源在 Amethyst 中是一条清晰、可组合的流水线:定义类型 → 定义序列化数据 → 实现Asset→ 实现ProcessableAsset→register_asset_type!注册。需要读写新格式文件时,再叠加一个自定义Format与register_importer!。这套机制与引擎的异步并行加载、存储隔离、热重载版本化设计紧密结合,是扩展引擎能力、接入自有数据格式的标准入口。
想继续深入,可以依次阅读仓库内的这些资料:
- 自定义资源官方指南:book/src/assets/how_to_define_custom_assets.md
- 资源使用基础(含
Loader、AssetStorage与异步等待的完整示例):book/src/assets/how_to_use_assets.md - 自定义格式官方指南:book/src/assets/how_to_define_custom_formats.md
- 资源格式总览:book/src/assets/formats.md
- 核心 trait 源码:amethyst_assets/src/asset.rs、处理系统源码:amethyst_assets/src/processor.rs、加载器源码:amethyst_assets/src/loader.rs
- 可直接运行的自定义资源示例:examples/asset_custom/main.rs(含配套资源 energy_blast.ron)
- 自定义格式解析示例:examples/asset_loading/main.rs
【免费下载链接】amethyst
Data-oriented and>项目地址:https://gitcode.com/gh_mirrors/ame/amethyst
相关推荐
al-folio 自定义域名在每次部署后被清空怎么解决?
al folio 自定义域名在每次部署后被清空怎么解决? 用 al folio 部署到 GitHub Pages 并设置了自定义域名(例如 example.co
GyroFlow索尼镜头配置文件加载失败完整排查指南
GyroFlow索尼镜头配置文件加载失败完整排查指南 GyroFlow是一款利用陀螺仪数据消除手持抖动的开源视频稳定工具。如果你正在被索尼镜头配置文件卡住——列
视频处理桌面应用音视频MXNet Gluon 自定义层(Custom Layer)入门实战:从 Block 到 HybridBlock 的完整指南
MXNet Gluon 自定义层(Custom Layer)入门实战:从 Block 到 HybridBlock 的完整指南 Gluon API 为 Apach
深度学习机器学习人工智能