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, 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)]
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)]
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                .loader()
154                .immediate()
155                .load::<Text>(&embedded)
156                .await?;
157            base_text.push_str(&loaded.get().0);
158        }
159        for (path, settings_override) in ron.dependencies_with_settings {
160            let loaded = load_context
161                .loader()
162                .with_settings(move |settings| {
163                    *settings = settings_override.clone();
164                })
165                .immediate()
166                .load::<Text>(&path)
167                .await?;
168            base_text.push_str(&loaded.get().0);
169        }
170        Ok(CoolText {
171            text: base_text,
172            dependencies: ron
173                .dependencies
174                .iter()
175                .map(|p| load_context.load(p))
176                .collect(),
177        })
178    }
179
180    fn extensions(&self) -> &[&str] {
181        &["cool.ron"]
182    }
183}
184
185#[derive(Default)]
186struct CoolTextTransformer;
187
188#[derive(Default, Serialize, Deserialize)]
189struct CoolTextTransformerSettings {
190    appended: String,
191}
192
193impl AssetTransformer for CoolTextTransformer {
194    type AssetInput = CoolText;
195    type AssetOutput = CoolText;
196    type Settings = CoolTextTransformerSettings;
197    type Error = Infallible;
198
199    async fn transform<'a>(
200        &'a self,
201        mut asset: TransformedAsset<Self::AssetInput>,
202        settings: &'a Self::Settings,
203    ) -> Result<TransformedAsset<Self::AssetOutput>, Self::Error> {
204        asset.text = format!("{}{}", asset.text, settings.appended);
205        Ok(asset)
206    }
207}
208
209struct CoolTextSaver;
210
211impl AssetSaver for CoolTextSaver {
212    type Asset = CoolText;
213    type Settings = ();
214    type OutputLoader = TextLoader;
215    type Error = std::io::Error;
216
217    async fn save(
218        &self,
219        writer: &mut Writer,
220        asset: SavedAsset<'_, Self::Asset>,
221        _settings: &Self::Settings,
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: EventReader<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}