Skip to content

Commit

Permalink
Quick and dirty hot-reloading implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
CatThingy committed Aug 9, 2022
1 parent 42c8fa2 commit 6305ce6
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 1 deletion.
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 }

[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
46 changes: 45 additions & 1 deletion src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ use bevy::ecs::system::EntityCommands;
use bevy::prelude::{FromWorld, Handle};
use bevy::reflect::Uuid;
use bevy::utils::HashMap;
#[cfg(feature = "hot_reloading")]
use crossbeam_channel::Receiver;
use dyn_clone::DynClone;
use indexmap::IndexSet;
#[cfg(feature = "hot_reloading")]
use notify::{Event, RecommendedWatcher, RecursiveMode, Result, Watcher};
use serde::{Deserialize, Serialize};

use crate::plugin::DefaultProtoDeserializer;
Expand All @@ -36,6 +40,34 @@ impl From<&HandlePath> for HandleId {

type UuidHandleMap = HashMap<Uuid, HandleUntyped>;

// Copied from bevy_asset's implementation
// https://github.com/bevyengine/bevy/blob/main/crates/bevy_asset/src/filesystem_watcher.rs
#[cfg(feature = "hot_reloading")]
pub(crate) struct FilesystemWatcher {
pub(crate) watcher: RecommendedWatcher,
pub(crate) receiver: Receiver<Result<Event>>,
}

#[cfg(feature = "hot_reloading")]
impl FilesystemWatcher {
/// Watch for changes recursively at the provided path.
pub fn watch<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<()> {
self.watcher.watch(path.as_ref(), RecursiveMode::Recursive)
}
}

#[cfg(feature = "hot_reloading")]
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 }
}
}

/// A resource containing data for all prototypes that need data stored
pub struct ProtoData {
/// Maps Prototype Name -> Component Type -> HandleId -> Asset Type -> HandleUntyped
Expand All @@ -46,7 +78,10 @@ pub struct ProtoData {
HashMap<HandleId, UuidHandleMap>,
>,
>,
prototypes: HashMap<String, Box<dyn Prototypical>>,
pub(crate) prototypes: HashMap<String, Box<dyn Prototypical>>,

#[cfg(feature = "hot_reloading")]
pub(crate) watcher: FilesystemWatcher,
}

impl ProtoData {
Expand All @@ -55,6 +90,8 @@ impl ProtoData {
Self {
handles: HashMap::default(),
prototypes: HashMap::default(),
#[cfg(feature = "hot_reloading")]
watcher: FilesystemWatcher::default(),
}
}

Expand Down Expand Up @@ -224,6 +261,8 @@ impl FromWorld for ProtoData {
let mut myself = Self {
handles: Default::default(),
prototypes: HashMap::default(),
#[cfg(feature = "hot_reloading")]
watcher: FilesystemWatcher::default(),
};

let options = world
Expand Down Expand Up @@ -497,6 +536,9 @@ pub struct ProtoDataOptions {
/// };
/// ```
pub extensions: Option<Vec<&'static str>>,
/// Whether to enable hot-reloading or not
#[cfg(feature = "hot_reloading")]
pub hot_reload: bool,
}

impl Default for ProtoDataOptions {
Expand All @@ -506,6 +548,8 @@ impl Default for ProtoDataOptions {
recursive_loading: Default::default(),
deserializer: Box::new(DefaultProtoDeserializer),
extensions: Default::default(),
#[cfg(feature = "hot_reloading")]
hot_reload: false,
}
}
}
59 changes: 59 additions & 0 deletions src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ impl ProtoPlugin {
recursive_loading: false,
deserializer: Box::new(DefaultProtoDeserializer),
extensions: Some(vec!["yaml", "json"]),
#[cfg(feature = "hot_reloading")]
hot_reload: false,
}),
}
}
Expand All @@ -60,6 +62,8 @@ impl ProtoPlugin {
recursive_loading: true,
deserializer: Box::new(DefaultProtoDeserializer),
extensions: Some(vec!["yaml", "json"]),
#[cfg(feature = "hot_reloading")]
hot_reload: false,
}),
}
}
Expand Down Expand Up @@ -87,6 +91,8 @@ impl ProtoPlugin {
recursive_loading: false,
deserializer: Box::new(DefaultProtoDeserializer),
extensions: Some(vec!["yaml", "json"]),
#[cfg(feature = "hot_reloading")]
hot_reload: false,
}),
}
}
Expand Down Expand Up @@ -114,6 +120,8 @@ impl ProtoPlugin {
recursive_loading: true,
deserializer: Box::new(DefaultProtoDeserializer),
extensions: Some(vec!["yaml", "json"]),
#[cfg(feature = "hot_reloading")]
hot_reload: false,
}),
}
}
Expand All @@ -124,13 +132,20 @@ impl Plugin for ProtoPlugin {
if let Some(opts) = &self.options {
// Insert custom prototype options
app.insert_resource(opts.clone());
#[cfg(feature = "hot_reloading")]
if opts.hot_reload {
app.add_startup_system(begin_watch);
app.add_system(watch_for_changes);
}
} else {
// Insert default options
app.insert_resource(ProtoDataOptions {
directories: vec![String::from("assets/prototypes")],
recursive_loading: false,
deserializer: Box::new(DefaultProtoDeserializer),
extensions: Some(vec!["yaml", "json"]),
#[cfg(feature = "hot_reloading")]
hot_reload: false,
});
}

Expand All @@ -139,6 +154,50 @@ impl Plugin for ProtoPlugin {
}
}

fn begin_watch(
mut data: bevy::prelude::ResMut<ProtoData>,
opts: bevy::prelude::Res<ProtoDataOptions>,
) {
data.watcher.watch(opts.directories[0].clone()).unwrap();
}

// 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(
mut proto_data: bevy::prelude::ResMut<ProtoData>,
options: bevy::prelude::Res<ProtoDataOptions>,
) {
let mut changed = bevy::utils::HashSet::default();
loop {
let event = match proto_data.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);
}
}
}

#[derive(Clone)]
pub(crate) struct DefaultProtoDeserializer;

Expand Down

0 comments on commit 6305ce6

Please sign in to comment.