Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement hot reloading for prototypes #22

Merged
merged 2 commits into from
Oct 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ typetag = "0.2"
serde_yaml = "0.9"
dyn-clone = "1.0"
indexmap = "1.9"
crossbeam-channel = { version = "0.5", optional = true }
notify = { version = "=5.0.0-pre.15", optional = true }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this specifically need to be =5.0.0-pre.15? Can it work with just 5.0.0 (which is what Bevy's using)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used =5.0.0-pre.15 because that's what Bevy 0.8(.1) uses.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense!


[dev-dependencies]
bevy = "0.8"
Expand All @@ -28,6 +30,8 @@ default = ["analysis"]
analysis = []
# If enabled, panics when a dependency cycle is found, otherwise logs a warning
no_cycles = ["analysis"]
# If enabled, allows for hot reloading
hot_reloading = ["dep:crossbeam-channel", "dep:notify"]

[[example]]
name = "basic"
Expand Down
2 changes: 1 addition & 1 deletion src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub struct ProtoData {
HashMap<HandleId, UuidHandleMap>,
>,
>,
prototypes: HashMap<String, Box<dyn Prototypical>>,
pub(crate) prototypes: HashMap<String, Box<dyn Prototypical>>,
}

impl ProtoData {
Expand Down
81 changes: 81 additions & 0 deletions src/hot_reload.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use bevy::prelude::{App, Plugin, Res, ResMut};
use crossbeam_channel::Receiver;
use notify::{Event, RecommendedWatcher, RecursiveMode, Result, Watcher};

use crate::prelude::{ProtoData, ProtoDataOptions};

// Copied from bevy_asset's implementation
// https://github.com/bevyengine/bevy/blob/main/crates/bevy_asset/src/filesystem_watcher.rs
struct FilesystemWatcher {
watcher: RecommendedWatcher,
receiver: Receiver<Result<Event>>,
}

impl FilesystemWatcher {
/// Watch for changes recursively at the provided path.
fn watch<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<()> {
self.watcher.watch(path.as_ref(), RecursiveMode::Recursive)
}
}

impl Default for FilesystemWatcher {
fn default() -> Self {
let (sender, receiver) = crossbeam_channel::unbounded();
let watcher: RecommendedWatcher = RecommendedWatcher::new(move |res| {
sender.send(res).expect("Watch event send failure.");
})
.expect("Failed to create filesystem watcher.");
FilesystemWatcher { watcher, receiver }
}
}

// Copied from bevy_asset's filesystem watching implementation:
// https://github.com/bevyengine/bevy/blob/main/crates/bevy_asset/src/io/file_asset_io.rs#L167-L199
fn watch_for_changes(
watcher: Res<FilesystemWatcher>,
mut proto_data: ResMut<ProtoData>,
options: Res<ProtoDataOptions>,
) {
let mut changed = bevy::utils::HashSet::default();
loop {
let event = match watcher.receiver.try_recv() {
Ok(result) => result.unwrap(),
Err(crossbeam_channel::TryRecvError::Empty) => break,
Err(crossbeam_channel::TryRecvError::Disconnected) => {
panic!("FilesystemWatcher disconnected.")
}
};
if let notify::event::Event {
kind: notify::event::EventKind::Modify(_),
paths,
..
} = event
{
for path in &paths {
if !changed.contains(path) {
if let Ok(data) = std::fs::read_to_string(path) {
if let Some(proto) = options.deserializer.deserialize(&data) {
proto_data
.prototypes
.insert(proto.name().to_string(), proto);
}
}
}
}
changed.extend(paths);
}
}
}

pub(crate) struct HotReloadPlugin {
pub(crate) path: String,
}

impl Plugin for HotReloadPlugin {
fn build(&self, app: &mut App) {
let mut watcher = FilesystemWatcher::default();
watcher.watch(self.path.clone()).unwrap();

app.insert_resource(watcher).add_system(watch_for_changes);
}
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ pub use plugin::ProtoPlugin;
mod prototype;
pub use prototype::{deserialize_templates_list, Prototype, Prototypical};

#[cfg(feature = "hot_reloading")]
mod hot_reload;

pub mod data;
#[macro_use]
mod utils;
Expand Down
19 changes: 11 additions & 8 deletions src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,19 +121,22 @@ impl ProtoPlugin {

impl Plugin for ProtoPlugin {
fn build(&self, app: &mut App) {
if let Some(opts) = &self.options {
// Insert custom prototype options
app.insert_resource(opts.clone());
} else {
// Insert default options
app.insert_resource(ProtoDataOptions {
let opts = self.options.clone();
let opts = opts.unwrap_or(
ProtoDataOptions {
directories: vec![String::from("assets/prototypes")],
recursive_loading: false,
deserializer: Box::new(DefaultProtoDeserializer),
extensions: Some(vec!["yaml", "json"]),
});
}
}
);

#[cfg(feature = "hot_reloading")]
app.add_plugin(crate::hot_reload::HotReloadPlugin {
path: opts.directories[0].clone()
});

app.insert_resource(opts);
// Initialize prototypes
app.init_resource::<ProtoData>();
}
Expand Down