bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
36.11k stars 3.56k forks source link

Hot-reloading is watching only one file from folder #3719

Closed SmallNibbler closed 2 years ago

SmallNibbler commented 2 years ago

Bevy version

[package] name = "experimental" version = "0.1.0" edition = "2021"

[dependencies] anyhow = "1.0.52"

[dependencies.bevy] version = "0.6.0" default-features = false features = [ "filesystem_watcher", "bevy_dynamic_plugin", "render", "bevy_winit", "png", "x11", ]

Operating system & version

Operating System: Manjaro Linux KDE Plasma Version: 5.23.4 KDE Frameworks Version: 5.89.0 Qt Version: 5.15.2 Kernel Version: 5.15.12-1-MANJARO (64-bit) Graphics Platform: X11 Processors: 4 × Intel® Pentium® CPU 3825U @ 1.90GHz Memory: 7.7 Gb of RAM Graphics Processor: Mesa Intel® HD Graphics

What you did

code_loader.rs

use bevy::prelude::*;
use bevy::asset::{AssetLoader, LoadContext, LoadedAsset};
use bevy::utils::BoxedFuture;
use bevy::reflect::TypeUuid;

#[derive(Debug, TypeUuid, Clone)]
#[uuid = "39cadc56-aa9c-4543-8640-a018b74b5052"]
pub (crate) struct Code {
    pub (crate) content: String
}

#[derive(Default)]
struct CodeAssetLoader;

impl AssetLoader for CodeAssetLoader {
    fn extensions(&self) -> &[&str] {
        &["rs"]
    }

    fn load<'a>(&'a self, bytes: &'a [u8], load_context: &'a mut LoadContext) -> BoxedFuture<'a, Result<(), anyhow::Error>> {
        Box::pin(async move {
            use std::str;

            let loaded_content = str::from_utf8(bytes)?;
            load_context.set_default_asset(LoadedAsset::new(Code{ content: loaded_content.to_string() }));
            Ok(())
        })
    }
}

pub struct CodeAssetPlugin;

impl Plugin for CodeAssetPlugin {
    fn build(&self, app: &mut App) {
        app.add_asset::<Code>()
            .init_asset_loader::<CodeAssetLoader>();
    }
}

main.rs

use bevy::prelude::*;

mod code_loader;
use crate::code_loader::*;

fn main() {
    App::new()
    .add_plugins(DefaultPlugins)
    .add_plugin(CodeAssetPlugin)
    .init_resource::<CodeHandles>()
    .add_startup_system(assets_load)
    .add_system(assets_print)
    .run();
}

#[derive(Default, Debug)]
pub (crate) struct CodeHandles {
    pub (crate) storage: Vec<Handle<Code>>
}

// Systems
fn assets_load (
    asset_server:       Res<AssetServer>,
    mut handles_storage:ResMut<CodeHandles>
) {
    handles_storage.storage = asset_server
        .load_folder("slides").unwrap()
        .into_iter().map(|h| h.typed::<Code>())
        .collect();
    asset_server.watch_for_changes().unwrap();

    println!("Handles {:#?}", handles_storage.storage);
}

fn assets_print (
    code_assets:            Res<Assets<Code>>,
    mut asset_events:       EventReader<AssetEvent<Code>>,
) {
    for event in asset_events.iter() {
        match event {
            AssetEvent::Created { handle } => {
                println!("Created {:#?}", handle);
                println!("Content {:#?}", &code_assets.get(handle).unwrap().content);
            }
            AssetEvent::Modified { handle } => {
                println!("Modified {:#?}", handle);
                println!("Content {:#?}", &code_assets.get(handle).unwrap().content);
            }
            AssetEvent::Removed { handle } => {println!("Removed {:#?}", handle);}
        }
    }
}

What does this code mean: There is an asset defenition in code_loader.rs. Asset with a plain text from .rs file. main.rs algorithm:

  1. Loading folder of .rs files.
  2. Convert all HandleUntyped types to Handle<Code>.
  3. Save it to Resource with Vec<Handle<Code>>.
  4. Start watch for changes.
  5. If a file has been loaded or modified, prints its content.

What you expected to happen

In most situations one file of the three is tracked, rarely two of three, sometimes no one. In the next example i'm loading two files from folder, one is tracked or no one.

Handles [
    StrongHandle<Code>(AssetPathId(AssetPathId(SourcePathId(13027041954229367283), LabelId(6298619649789039366)))),
    StrongHandle<Code>(AssetPathId(AssetPathId(SourcePathId(9312919904264361102), LabelId(6298619649789039366)))),
]
Created WeakHandle<Code>(AssetPathId(AssetPathId(SourcePathId(13027041954229367283), LabelId(6298619649789039366))))
Content "// Example\nfn main() {\n    println!(\"Hello World, from short string!\");\n}\n"
Created WeakHandle<Code>(AssetPathId(AssetPathId(SourcePathId(9312919904264361102), LabelId(6298619649789039366))))
Content "// Example\nfn main() {\n    println!(\"Hello World, from this very long string!\");\n}\n"
Modified WeakHandle<Code>(AssetPathId(AssetPathId(SourcePathId(9312919904264361102), LabelId(6298619649789039366))))
Content "// Example 1\nfn main() {\n    println!(\"Hello World, from this very long string!\");\n}\n"
Modified WeakHandle<Code>(AssetPathId(AssetPathId(SourcePathId(13027041954229367283), LabelId(6298619649789039366))))
Content "// Example 1\nfn main() {\n    println!(\"Hello World, from short string!\");\n}\n"

What actually happened

Handles [
    StrongHandle<Code>(AssetPathId(AssetPathId(SourcePathId(13027041954229367283), LabelId(6298619649789039366)))),
    StrongHandle<Code>(AssetPathId(AssetPathId(SourcePathId(9312919904264361102), LabelId(6298619649789039366)))),
]
Created WeakHandle<Code>(AssetPathId(AssetPathId(SourcePathId(13027041954229367283), LabelId(6298619649789039366))))
Content "// Example\nfn main() {\n    println!(\"Hello World, from short string!\");\n}\n"
Created WeakHandle<Code>(AssetPathId(AssetPathId(SourcePathId(9312919904264361102), LabelId(6298619649789039366))))
Content "// Example\nfn main() {\n    println!(\"Hello World, from this very long string!\");\n}\n"
Modified WeakHandle<Code>(AssetPathId(AssetPathId(SourcePathId(9312919904264361102), LabelId(6298619649789039366))))
Content "// Example 1\nfn main() {\n    println!(\"Hello World, from this very long string!\");\n}\n"
superdump commented 2 years ago

Calling AssetServer::watch_for_changes() initialises the filesystem watcher synchronously. Calling AssetServer::load_folder() eventually spawns a call to the internal AssetServer::load_async() as a task on a TaskPool. That is, it is called asynchronously and after load_folder() returns, the timing of those tasks executing can and will vary. At the end of load_async(), the path to the asset is added to the patches to be watched, but only if the filesystem watcher has been initialised and exists.

Hopefully from this description of how the code works, it is clear that this code is racey. I discovered this while investigating inconsistency in how hot-reloading was working and fixing it here: https://github.com/bevyengine/bevy/pull/3643

You could also try enabling the watcher first, and then calling load_folder.

SmallNibbler commented 2 years ago

This helped, thanks.

You could also try enabling the watcher first, and then calling load_folder.