#observing (custom) asset load events

4 messages · Page 1 of 1 (latest)

green urchin
#

Hey, so I have a custom asset GobtonData that simply loads a ron file. I'd like to observe when it's done loading (or fails to do so). For this I have a system in Startup that:

  • adds an observer on Trigger<AssetLoadFailedEvent<GobtonData>>, logging and sending AppExit::from_code(2)
  • adds an observer on Trigger<AssetEvent<GobtonData>>, logging and sending AppExit that exits the app with a failure if the asset is still missing from Assets<GobtonData>, or success if I get the asset.
  • loads the asset

The asset seems to load to, or to correctly fail when the file is missing or badly formatted (which confirm my custom asset loader is being used), but none of the observer run.
I noticed that when the asset fails to load, the error (from the asset loader) sometimes gets logged before, my info!("loading asset {}: {}, ...);, which makes me think there may be a race condition ? (I have bevy's default features).

I don't know where to go from there, are these events not supposed to be emitted in this situation ? Should I do like in bevy's custom asset examples and check if the event is loaded each frame ? I'd really like to react to events rather than actively poll.

#

The code:

use bevy::asset::{AssetLoadFailedEvent, Handle};
use bevy::prelude::*;
use gobton_world::loader::GobtonData;

pub fn main() -> AppExit {
    let mut app = App::new();
    app.add_plugins(DefaultPlugins)
        .init_asset_loader::<gobton_world::loader::Loader<GobtonData>>()
        .init_asset::<GobtonData>()
        .add_systems(Startup, load_world);
    return app.run();
}

fn load_world(asset_server: Res<AssetServer>, mut commands: Commands) {
    commands.add_observer(
        move |trigger: Trigger<AssetLoadFailedEvent<GobtonData>>,
              mut exit_writer: EventWriter<AppExit>| {
            let event = trigger.event();
            error!("failed to load asset {}: {}", event.id, event.path);
            let mut source: Option<&dyn core::error::Error> = Some(&event.error);
            while let Some(error) = source {
                error!("{}", error);
                source = error.source();
            }
            exit_writer.write(AppExit::from_code(2));
        },
    );

    commands.add_observer(
        |trigger: Trigger<AssetEvent<GobtonData>>,
         assets: Res<Assets<GobtonData>>,
         mut exit_writer: EventWriter<AppExit>| {
            info!("asset event {:?}", trigger.event());
            let &AssetEvent::LoadedWithDependencies { id } = trigger.event() else {
                return;
            };
            let Some(data) = assets.get(id) else {
                error!("asset {id} is missing");
                exit_writer.write(AppExit::from_code(1));
                return;
            };
            info!("got data:\n{data:#?}");
            exit_writer.write(AppExit::from_code(0));
            return;
        },
    );

    let handle: Handle<GobtonData> = asset_server.load("world/data.ron");
    info!("loading asset {}: {}", handle.id(), handle.path().unwrap());
    // An attempt at making sure the asset server believes the asset is always used
    core::mem::forget(handle);
}
#

The logs with a valid asset:

2025-06-02T09:58:28.190462Z  INFO bevy_diagnostic::system_information_diagnostics_plugin::internal: SystemInfo { os: "Linux (NixOS 25.05)", kernel: "6.12.23", cpu: "Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz", core_count: "6", memory: "15.4 GiB" }
2025-06-02T09:58:28.368090Z  INFO bevy_render::renderer: AdapterInfo { name: "NVIDIA GeForce GTX 1050", vendor: 4318, device: 7313, device_type: DiscreteGpu, driver: "NVIDIA", driver_info: "570.133.07", backend: Vulkan }
2025-06-02T09:58:28.602371Z  INFO bevy_render::batching::gpu_preprocessing: GPU preprocessing is fully supported on this device.
2025-06-02T09:58:28.730296Z  INFO bevy_winit::system: Creating new window App (0v1)
2025-06-02T09:58:28.734035Z  INFO chunk_loading: loading asset AssetId<gobton_world::loader::GobtonData>{ index: 0, generation: 0}: world/data.ron

And with an invalid error (the message comes from my custom asset loader):

2025-06-02T10:02:10.189300Z  INFO bevy_diagnostic::system_information_diagnostics_plugin::internal: SystemInfo { os: "Linux (NixOS 25.05)", kernel: "6.12.23", cpu: "Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz", core_count: "6", memory: "15.4 GiB" }
2025-06-02T10:02:10.383648Z  INFO bevy_render::renderer: AdapterInfo { name: "NVIDIA GeForce GTX 1050", vendor: 4318, device: 7313, device_type: DiscreteGpu, driver: "NVIDIA", driver_info: "570.133.07", backend: Vulkan }
2025-06-02T10:02:10.602556Z  INFO bevy_render::batching::gpu_preprocessing: GPU preprocessing is fully supported on this device.
2025-06-02T10:02:10.689388Z  INFO bevy_winit::system: Creating new window App (0v1)
2025-06-02T10:02:10.693482Z  INFO chunk_loading: loading asset AssetId<gobton_world::loader::GobtonData>{ index: 0, generation: 0}: world/data.ron
2025-06-02T10:02:10.694054Z ERROR bevy_asset::server: Failed to load asset 'world/data.ron' with asset loader 'gobton_world::loader::Loader<gobton_world::loader::GobtonData, 0>': invalid gobton asset format
green urchin
#

Oh, I got it, I missed this note in the observer docs:

Note that “buffered” events sent using EventReader and EventWriter are not automatically triggered. They must be triggered at a specific point in the schedule.
So I just added:

pub fn main() -> AppExit {
    /* ... */
        .add_systems(
            PreUpdate,
            trigger_asset_events.after(TrackAssets).run_if(
                on_event::<AssetEvent<GobtonData>>.or(on_event::<AssetLoadFailedEvent<GobtonData>>),
            ),
        );
    /* ... */
}

fn trigger_asset_events(
    mut asset_events: EventReader<AssetEvent<GobtonData>>,
    mut asset_load_failed_events: EventReader<AssetLoadFailedEvent<GobtonData>>,
    mut commands: Commands,
) {
    for &event in asset_events.read() {
        commands.trigger(event);
    }
    for event in asset_load_failed_events.read().cloned() {
        commands.trigger(event);
    }
}