Skip to main content

asset_processing/
asset_processing.rs

1//! This example illustrates how to define custom `AssetLoader`s, `AssetTransformer`s, and `AssetSaver`s, how to configure them, and how to register asset processors.
2
3use bevy::{
4    asset::{
5        embedded_asset,
6        io::{Reader, Writer},
7        processor::LoadTransformAndSave,
8        saver::{AssetSaver, SavedAsset},
9        transformer::{AssetTransformer, TransformedAsset},
10        AssetLoader, AssetPath, AsyncWriteExt, LoadContext,
11    },
12    prelude::*,
13    reflect::TypePath,
14};
15use serde::{Deserialize, Serialize};
16use std::convert::Infallible;
17use thiserror::Error;
18
19fn main() {
20    App::new()
21        // Using the "processed" mode will configure the AssetPlugin to use asset processing.
22        // If you also enable the `asset_processor` cargo feature, this will run the AssetProcessor
23        // in the background, run them through configured asset processors, and write the results to
24        // the `imported_assets` folder. If you also enable the `file_watcher` cargo feature, changes to the
25        // source assets will be detected and they will be reprocessed.
26        //
27        // The AssetProcessor will create `.meta` files automatically for assets in the `assets` folder,
28        // which can then be used to configure how the asset will be processed.
29        .add_plugins((
30            DefaultPlugins.set(AssetPlugin {
31                mode: AssetMode::Processed,
32                // This is just overriding the default paths to scope this to the correct example folder
33                // You can generally skip this in your own projects
34                file_path: "examples/asset/processing/assets".to_string(),
35                processed_file_path: "examples/asset/processing/imported_assets/Default"
36                    .to_string(),
37                ..default()
38            }),
39            TextPlugin,
40        ))
41        .add_systems(Startup, setup)
42        .add_systems(Update, print_text)
43        .run();
44}
45
46/// This [`TextPlugin`] defines two assets types:
47/// * [`CoolText`]: a custom RON text format that supports dependencies and embedded dependencies
48/// * [`Text`]: a "normal" plain text file
49///
50/// It also defines an asset processor that will load [`CoolText`], resolve embedded dependencies, and write the resulting
51/// output to a "normal" plain text file. When the processed asset is loaded, it is loaded as a Text (plaintext) asset.
52/// This illustrates that when you process an asset, you can change its type! However you don't _need_ to change the type.
53struct TextPlugin;
54
55impl Plugin for TextPlugin {
56    fn build(&self, app: &mut App) {
57        embedded_asset!(app, "examples/asset/processing/", "e.txt");
58        app.init_asset::<CoolText>()
59            .init_asset::<Text>()
60            .register_asset_loader(CoolTextLoader)
61            .register_asset_loader(TextLoader)
62            .register_asset_processor::<LoadTransformAndSave<CoolTextLoader, CoolTextTransformer, CoolTextSaver>>(
63                LoadTransformAndSave::new(CoolTextTransformer, CoolTextSaver),
64            )
65            .set_default_asset_processor::<LoadTransformAndSave<CoolTextLoader, CoolTextTransformer, CoolTextSaver>>("cool.ron");
66    }
67}
68
69#[derive(Asset, TypePath, Debug)]
70struct Text(String);
71
72#[derive(Default, TypePath)]
73struct TextLoader;
74
75#[derive(Clone, Default, Serialize, Deserialize)]
76struct TextSettings {
77    text_override: Option<String>,
78}
79
80impl AssetLoader for TextLoader {
81    type Asset = Text;
82    type Settings = TextSettings;
83    type Error = std::io::Error;
84    async fn load(
85        &self,
86        reader: &mut dyn Reader,
87        settings: &TextSettings,
88        _load_context: &mut LoadContext<'_>,
89    ) -> Result<Text, Self::Error> {
90        let mut bytes = Vec::new();
91        reader.read_to_end(&mut bytes).await?;
92        let value = if let Some(ref text) = settings.text_override {
93            text.clone()
94        } else {
95            String::from_utf8(bytes).unwrap()
96        };
97        Ok(Text(value))
98    }
99
100    fn extensions(&self) -> &[&str] {
101        &["txt"]
102    }
103}
104
105#[derive(Serialize, Deserialize)]
106struct CoolTextRon {
107    text: String,
108    dependencies: Vec<String>,
109    embedded_dependencies: Vec<String>,
110    dependencies_with_settings: Vec<(String, TextSettings)>,
111}
112
113#[derive(Asset, TypePath, Debug)]
114struct CoolText {
115    text: String,
116    #[expect(
117        dead_code,
118        reason = "Used to show that our assets can hold handles to other assets"
119    )]
120    dependencies: Vec<Handle<Text>>,
121}
122
123#[derive(Default, TypePath)]
124struct CoolTextLoader;
125
126#[derive(Debug, Error)]
127enum CoolTextLoaderError {
128    #[error(transparent)]
129    Io(#[from] std::io::Error),
130    #[error(transparent)]
131    RonSpannedError(#[from] ron::error::SpannedError),
132    #[error(transparent)]
133    LoadDirectError(#[from] bevy::asset::LoadDirectError),
134}
135
136impl AssetLoader for CoolTextLoader {
137    type Asset = CoolText;
138    type Settings = ();
139    type Error = CoolTextLoaderError;
140
141    async fn load(
142        &self,
143        reader: &mut dyn Reader,
144        _settings: &Self::Settings,
145        load_context: &mut LoadContext<'_>,
146    ) -> Result<CoolText, Self::Error> {
147        let mut bytes = Vec::new();
148        reader.read_to_end(&mut bytes).await?;
149        let ron: CoolTextRon = ron::de::from_bytes(&bytes)?;
150        let mut base_text = ron.text;
151        for embedded in ron.embedded_dependencies {
152            let loaded = load_context
153                .load_builder()
154                .load_value::<Text>(&embedded)
155                .await?;
156            base_text.push_str(&loaded.get().0);
157        }
158        for (path, settings_override) in ron.dependencies_with_settings {
159            let loaded = load_context
160                .load_builder()
161                .with_settings(move |settings| {
162                    *settings = settings_override.clone();
163                })
164                .load_value::<Text>(&path)
165                .await?;
166            base_text.push_str(&loaded.get().0);
167        }
168        Ok(CoolText {
169            text: base_text,
170            dependencies: ron
171                .dependencies
172                .iter()
173                .map(|p| load_context.load(p))
174                .collect(),
175        })
176    }
177
178    fn extensions(&self) -> &[&str] {
179        &["cool.ron"]
180    }
181}
182
183#[derive(Default, TypePath)]
184struct CoolTextTransformer;
185
186#[derive(Default, Serialize, Deserialize)]
187struct CoolTextTransformerSettings {
188    appended: String,
189}
190
191impl AssetTransformer for CoolTextTransformer {
192    type AssetInput = CoolText;
193    type AssetOutput = CoolText;
194    type Settings = CoolTextTransformerSettings;
195    type Error = Infallible;
196
197    async fn transform<'a>(
198        &'a self,
199        mut asset: TransformedAsset<Self::AssetInput>,
200        settings: &'a Self::Settings,
201    ) -> Result<TransformedAsset<Self::AssetOutput>, Self::Error> {
202        asset.text = format!("{}{}", asset.text, settings.appended);
203        Ok(asset)
204    }
205}
206
207#[derive(TypePath)]
208struct CoolTextSaver;
209
210impl AssetSaver for CoolTextSaver {
211    type Asset = CoolText;
212    type Settings = ();
213    type OutputLoader = TextLoader;
214    type Error = std::io::Error;
215
216    async fn save(
217        &self,
218        writer: &mut Writer,
219        asset: SavedAsset<'_, '_, Self::Asset>,
220        _settings: &Self::Settings,
221        _asset_path: AssetPath<'_>,
222    ) -> Result<TextSettings, Self::Error> {
223        writer.write_all(asset.text.as_bytes()).await?;
224        Ok(TextSettings::default())
225    }
226}
227
228#[derive(Resource)]
229struct TextAssets {
230    a: Handle<Text>,
231    b: Handle<Text>,
232    c: Handle<Text>,
233    d: Handle<Text>,
234    e: Handle<Text>,
235}
236
237fn setup(mut commands: Commands, assets: Res<AssetServer>) {
238    // This the final processed versions of `assets/a.cool.ron` and `assets/foo.c.cool.ron`
239    // Check out their counterparts in `imported_assets` to see what the outputs look like.
240    commands.insert_resource(TextAssets {
241        a: assets.load("a.cool.ron"),
242        b: assets.load("foo/b.cool.ron"),
243        c: assets.load("foo/c.cool.ron"),
244        d: assets.load("d.cool.ron"),
245        e: assets.load("embedded://asset_processing/e.txt"),
246    });
247}
248
249fn print_text(
250    handles: Res<TextAssets>,
251    texts: Res<Assets<Text>>,
252    mut asset_events: MessageReader<AssetEvent<Text>>,
253) {
254    if !asset_events.is_empty() {
255        // This prints the current values of the assets
256        // Hot-reloading is supported, so try modifying the source assets (and their meta files)!
257        println!("Current Values:");
258        println!("  a: {:?}", texts.get(&handles.a));
259        println!("  b: {:?}", texts.get(&handles.b));
260        println!("  c: {:?}", texts.get(&handles.c));
261        println!("  d: {:?}", texts.get(&handles.d));
262        println!("  e: {:?}", texts.get(&handles.e));
263        println!("(You can modify source assets and their .meta files to hot-reload changes!)");
264        println!();
265        asset_events.clear();
266    }
267}