Skip to content

Commit 822f5a5

Browse files
committed
Add scene hooks
1 parent 412afd9 commit 822f5a5

File tree

6 files changed

+137
-111
lines changed

6 files changed

+137
-111
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description = "A Bevy engine plugin for loading Magica Voxel world files and ren
44
keywords = ["Bevy", "voxel", "Magica-Voxel"]
55
categories = ["game-development", "graphics", "rendering", "rendering::data-formats"]
66
license = "MIT"
7-
version = "0.10.4"
7+
version = "0.11.0"
88
repository = "https://github.com/Utsira/bevy_vox_scene"
99
authors = ["Oliver Dew <[email protected]>"]
1010
edition = "2021"

README.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22
<code>bevy_vox_scene</code>
33
</h1>
44

5-
<a href="https://crates.io/crates/bevy_vox_scene">
6-
<img height="24" src="https://img.shields.io/crates/v/bevy_vox_scene?style=for-the-badge"/>
7-
</a>
8-
5+
[![Latest version](https://img.shields.io/crates/v/bevy_vox_scene.svg)](https://crates.io/crates/bevy_vox_scene)
96
[![CI](https://github.com/Utsira/bevy_vox_scene/actions/workflows/ci.yml/badge.svg)](https://github.com/Utsira/bevy_vox_scene/actions/workflows/ci.yml)
107

118
A plugin for [the Bevy Engine](https://bevyengine.org) which allows loading [Magica Voxel](https://ephtracy.github.io) `.vox` files directly into a Bevy scene graph.

assets/study.vox

0 Bytes
Binary file not shown.

examples/modify-scene.rs

Lines changed: 62 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use std::f32::consts::PI;
22
use rand::Rng;
33

4-
use bevy::{prelude::*, core_pipeline::{bloom::BloomSettings, tonemapping::Tonemapping, core_3d::ScreenSpaceTransmissionQuality, experimental::taa::{TemporalAntiAliasPlugin, TemporalAntiAliasBundle}}, input::keyboard::KeyboardInput};
5-
use bevy_vox_scene::{VoxScenePlugin, VoxelSceneBundle, VoxelEntityReady};
4+
use bevy::{prelude::*, core_pipeline::{bloom::BloomSettings, tonemapping::Tonemapping, core_3d::ScreenSpaceTransmissionQuality, experimental::taa::{TemporalAntiAliasPlugin, TemporalAntiAliasBundle}}, input::keyboard::KeyboardInput, ecs::system::EntityCommands};
5+
use bevy_vox_scene::{VoxScenePlugin, VoxelSceneHook, VoxelSceneHookBundle};
66
use bevy_panorbit_camera::{PanOrbitCameraPlugin, PanOrbitCamera};
77

8-
/// Uses the [`bevy_vox_scene::VoxelEntityReady`] event to add extra components into the scene graph.
8+
/// Uses the [`bevy_vox_scene::VoxelSceneHook`] component to add extra components into the scene graph.
99
/// Press any key to toggle the fish tank black-light on and off
1010
fn main() {
1111
let mut app = App::new();
@@ -15,10 +15,8 @@ fn main() {
1515
PanOrbitCameraPlugin,
1616
VoxScenePlugin,
1717
))
18-
.init_resource::<TankSize>()
1918
.add_systems(Startup, setup)
2019
.add_systems(Update, (
21-
add_components_to_voxel_entities.run_if(on_event::<VoxelEntityReady>()),
2220
toggle_black_light.run_if(on_event::<KeyboardInput>()),
2321
swim_fish,
2422
));
@@ -33,6 +31,8 @@ fn main() {
3331
app.run();
3432
}
3533

34+
// Systems
35+
3636
fn setup(
3737
mut commands: Commands,
3838
assets: Res<AssetServer>,
@@ -64,77 +64,36 @@ fn setup(
6464
specular_map: assets.load("pisa_specular.ktx2"),
6565
},
6666
));
67-
68-
commands.spawn(VoxelSceneBundle {
69-
// "tank" is the name of the group containing the glass walls, the body of water, the scenery in the tank and the fish
70-
scene: assets.load("study.vox#tank"),
71-
transform: Transform::from_scale(Vec3::splat(0.05)),
72-
..default()
73-
});
74-
}
75-
76-
#[derive(Component)]
77-
struct EmissiveToggle {
78-
is_on: bool,
79-
on_material: Handle<StandardMaterial>,
80-
off_material: Handle<StandardMaterial>,
81-
}
82-
83-
impl EmissiveToggle {
84-
fn toggle(&mut self) {
85-
self.is_on = !self.is_on;
86-
}
87-
88-
fn material(&self) -> &Handle<StandardMaterial> {
89-
match self.is_on {
90-
true => &self.on_material,
91-
false => &self.off_material,
92-
}
93-
}
94-
}
95-
96-
#[derive(Component)]
97-
struct Fish(f32);
98-
99-
#[derive(Resource, Default)]
100-
struct TankSize(Vec3);
67+
let asset_server = assets.clone();
68+
commands.spawn((
69+
VoxelSceneHookBundle {
70+
// "tank" is the name of the group containing the glass walls, the body of water, the scenery in the tank and the fish
71+
scene: assets.load("study.vox#tank"),
10172

102-
fn add_components_to_voxel_entities(
103-
mut commands: Commands,
104-
mut event_reader: EventReader<VoxelEntityReady>,
105-
mesh_query: Query<&Handle<Mesh>>,
106-
meshes: Res<Assets<Mesh>>,
107-
mut tank_size: ResMut<TankSize>,
108-
assets: Res<AssetServer>,
109-
) {
110-
let mut rng = rand::thread_rng();
111-
for event in event_reader.read() {
112-
// If we are spawning multiple scenes we could match on the scene that the entity was spawned from. Here we just check it is the scene we're expecting.
113-
if event.scene_name != "study.vox#tank" { return };
114-
match event.name.as_str() {
115-
// Node names give the path to the asset, with components separated by /. Here, "black-light" belongs to the "tank" group
116-
"tank/black-light" => {
117-
commands.entity(event.entity).insert(EmissiveToggle {
118-
is_on: true,
119-
on_material: assets.load("study.vox#material"), // emissive texture
120-
off_material: assets.load("study.vox#material-no-emission"), // non-emissive texture
121-
});
122-
},
123-
"tank/goldfish" | "tank/tetra" => {
124-
// Make fish go brrrrr
125-
commands.entity(event.entity).insert(Fish(rng.gen_range(5.0..10.0)));
126-
}
127-
"tank/water" => {
128-
// measure size of tank
129-
let Ok(mesh_handle) = mesh_query.get_component::<Handle<Mesh>>(event.entity) else { return };
130-
let Some(mesh) = meshes.get(mesh_handle) else { return };
131-
let Some(aabb) = mesh.compute_aabb() else { return };
132-
let half_extent: Vec3 = aabb.half_extents.into();
133-
tank_size.0 = half_extent - Vec3::splat(6.0); // add a margin
134-
}
135-
_ => {},
136-
}
137-
}
73+
// This closure will be run against every child Entity that gets spawned in the scene
74+
hook: VoxelSceneHook::new(move |entity, commands| {
75+
let Some(name) = entity.get::<Name>() else { return };
76+
match name.as_str() {
77+
// Node names give the path to the asset, with components separated by /. Here, "black-light" belongs to the "tank" group
78+
"tank/black-light" => {
79+
commands.insert(EmissiveToggle {
80+
is_on: true,
81+
on_material: asset_server.load("study.vox#material"), // emissive texture
82+
off_material: asset_server.load("study.vox#material-no-emission"), // non-emissive texture
83+
});
84+
},
85+
"tank/goldfish" | "tank/tetra" => {
86+
// Make fish go brrrrr
87+
let mut rng = rand::thread_rng(); // random speed
88+
commands.insert(Fish(rng.gen_range(5.0..10.0)));
89+
}
90+
_ => {},
91+
}
92+
}),
93+
transform: Transform::from_scale(Vec3::splat(0.05)),
94+
..default()
95+
},
96+
));
13897
}
13998

14099
fn toggle_black_light(
@@ -151,19 +110,44 @@ fn toggle_black_light(
151110
fn swim_fish(
152111
mut query: Query<(&mut Transform, &Fish)>,
153112
time: Res<Time>,
154-
tank_size: Res<TankSize>,
155113
) {
114+
let tank_half_extents = Vec3::new(29.0, 20.0, 25.0);
156115
for (mut transform, fish) in query.iter_mut() {
157116
let x_direction = transform.forward().dot(Vec3::X);
158-
if (x_direction < -0.5 && transform.translation.x < -tank_size.0.x) || (x_direction > 0.5 && transform.translation.x > tank_size.0.x) {
117+
if (x_direction < -0.5 && transform.translation.x < -tank_half_extents.x) || (x_direction > 0.5 && transform.translation.x > tank_half_extents.x) {
159118
// change direction at tank edges
160119
transform.rotate_axis(Vec3::Y, PI);
161120
}
162121
// slow down when near the edge
163-
let slow_down = 1.0 - ((transform.translation.x.abs() - (tank_size.0.x - 4.2)) / 5.0).clamp(0.0, 1.0);
122+
let slow_down = 1.0 - ((transform.translation.x.abs() - (tank_half_extents.x - 4.2)) / 5.0).clamp(0.0, 1.0);
164123
let forward = transform.forward();
165124
transform.translation += forward * (time.delta_seconds() * fish.0 * slow_down);
166125
// make them weave up and down
167126
transform.translation.y = (transform.translation.x * 0.1).sin() * 6.0;
168127
}
169128
}
129+
130+
// Components
131+
132+
#[derive(Component)]
133+
struct EmissiveToggle {
134+
is_on: bool,
135+
on_material: Handle<StandardMaterial>,
136+
off_material: Handle<StandardMaterial>,
137+
}
138+
139+
impl EmissiveToggle {
140+
fn toggle(&mut self) {
141+
self.is_on = !self.is_on;
142+
}
143+
144+
fn material(&self) -> &Handle<StandardMaterial> {
145+
match self.is_on {
146+
true => &self.on_material,
147+
false => &self.off_material,
148+
}
149+
}
150+
}
151+
152+
#[derive(Component)]
153+
struct Fish(f32);

src/lib.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,12 @@
3434
//!```
3535
use bevy::{
3636
app::{App, Plugin, SpawnScene},
37-
asset::AssetApp,
37+
asset::AssetApp, ecs::schedule::IntoSystemConfigs,
3838
};
3939

4040
mod loader;
4141
mod voxel_scene;
42-
pub use voxel_scene::VoxelEntityReady;
43-
pub use voxel_scene::{VoxelSceneBundle, VoxelScene, VoxelLayer};
42+
pub use voxel_scene::{VoxelSceneBundle, VoxelSceneHookBundle, VoxelScene, VoxelLayer, VoxelSceneHook};
4443
pub use loader::VoxLoaderSettings;
4544
#[doc(inline)]
4645
use loader::VoxSceneLoader;
@@ -57,7 +56,9 @@ impl Plugin for VoxScenePlugin {
5756
app
5857
.init_asset::<voxel_scene::VoxelScene>()
5958
.register_asset_loader(VoxSceneLoader)
60-
.add_event::<VoxelEntityReady>()
61-
.add_systems(SpawnScene, voxel_scene::spawn_vox_scenes);
59+
.add_systems(SpawnScene, (
60+
voxel_scene::spawn_vox_scenes,
61+
voxel_scene::run_hooks
62+
).chain());
6263
}
6364
}

src/voxel_scene.rs

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use bevy::{ecs::{bundle::Bundle, component::Component, system::{Commands, Query, Res}, entity::Entity, event::{Event, EventWriter}}, asset::{Handle, Asset, Assets}, transform::components::Transform, reflect::TypePath, math::{Mat4, Vec3, Mat3, Quat}, render::{mesh::Mesh, view::Visibility, prelude::SpatialBundle}, pbr::{StandardMaterial, PbrBundle}, core::Name, hierarchy::BuildChildren, log::warn};
1+
use bevy::{ecs::{bundle::Bundle, component::Component, system::{Commands, EntityCommands, Query, Res}, entity::Entity, world::{EntityRef, World}, query::Without}, asset::{Handle, Asset, Assets}, transform::components::Transform, reflect::TypePath, math::{Mat4, Vec3, Mat3, Quat}, render::{mesh::Mesh, view::Visibility, prelude::SpatialBundle}, pbr::{StandardMaterial, PbrBundle}, core::Name, hierarchy::{BuildChildren, Children}, log::warn};
22
use dot_vox::{SceneNode, Frame};
33

44
#[derive(Bundle, Default)]
@@ -8,6 +8,14 @@ pub struct VoxelSceneBundle {
88
pub visibility: Visibility,
99
}
1010

11+
#[derive(Bundle, Default)]
12+
pub struct VoxelSceneHookBundle {
13+
pub scene: Handle<VoxelScene>,
14+
pub hook: VoxelSceneHook,
15+
pub transform: Transform,
16+
pub visibility: Visibility,
17+
}
18+
1119
#[derive(Asset, TypePath, Debug)]
1220
pub struct VoxelScene {
1321
pub name: String,
@@ -44,23 +52,35 @@ pub struct VoxelLayer {
4452
pub name: Option<String>,
4553
}
4654

47-
#[derive(Event)]
48-
pub struct VoxelEntityReady {
49-
pub scene_name: String,
50-
pub entity: Entity,
51-
pub name: String,
52-
pub layer_id: u32,
55+
#[derive(Component)]
56+
pub struct VoxelSceneHook {
57+
hook: Box<dyn Fn(&EntityRef, &mut EntityCommands) + Send + Sync + 'static>,
58+
}
59+
60+
impl VoxelSceneHook {
61+
pub fn new<F: Fn(&EntityRef, &mut EntityCommands) + Send + Sync + 'static>(hook: F) -> Self {
62+
Self {
63+
hook: Box::new(hook),
64+
}
65+
}
66+
}
67+
68+
impl Default for VoxelSceneHook {
69+
fn default() -> Self {
70+
Self::new(|_, _| {
71+
warn!("Default VoxelSceneHook does nothing")
72+
})
73+
}
5374
}
5475

5576
pub(crate) fn spawn_vox_scenes(
5677
mut commands: Commands,
5778
query: Query<(Entity, &Transform, &Visibility, &Handle<VoxelScene>)>,
5879
vox_scenes: Res<Assets<VoxelScene>>,
59-
mut event_writer: EventWriter<VoxelEntityReady>,
6080
) {
6181
for (root, transform, visibility, scene_handle) in query.iter() {
6282
if let Some(scene) = vox_scenes.get(scene_handle) {
63-
spawn_voxel_node_recursive(&mut commands, &scene.root, root, scene, &mut event_writer);
83+
spawn_voxel_node_recursive(&mut commands, &scene.root, root, scene);
6484
commands.entity(root)
6585
.remove::<Handle<VoxelScene>>()
6686
.insert((*transform, *visibility));
@@ -73,9 +93,11 @@ fn spawn_voxel_node_recursive(
7393
voxel_node: &VoxelNode,
7494
entity: Entity,
7595
scene: &VoxelScene,
76-
event_writer: &mut EventWriter<VoxelEntityReady>,
7796
) {
7897
let mut entity_commands = commands.entity(entity);
98+
if let Some(name) = &voxel_node.name {
99+
entity_commands.insert(Name::new(name.clone()));
100+
}
79101
if let Some(model) = voxel_node.model_id.and_then(|id| {
80102
if let Some(model) = scene.models.get(id) {
81103
Some(model)
@@ -109,17 +131,34 @@ fn spawn_voxel_node_recursive(
109131
for child in &voxel_node.children {
110132
let mut child_entity = builder.spawn_empty();
111133
let id = child_entity.id();
112-
spawn_voxel_node_recursive(child_entity.commands(), child, id, scene, event_writer);
134+
spawn_voxel_node_recursive(child_entity.commands(), child, id, scene);
113135
}
114136
});
115-
if let Some(name) = &voxel_node.name {
116-
entity_commands.insert(Name::new(name.clone()));
117-
event_writer.send(VoxelEntityReady {
118-
scene_name: scene.name.clone(),
119-
entity,
120-
name: name.to_string(),
121-
layer_id: voxel_node.layer_id
122-
});
137+
}
138+
139+
pub(super) fn run_hooks(
140+
mut commands: Commands,
141+
world: &World,
142+
query: Query<(Entity, &VoxelSceneHook), Without<Handle<VoxelScene>>>,
143+
) {
144+
for (entity, scene_hook) in query.iter() {
145+
run_hook_recursive(&mut commands, world, entity, scene_hook);
146+
commands.entity(entity).remove::<VoxelSceneHook>();
147+
}
148+
}
149+
150+
fn run_hook_recursive(
151+
commands: &mut Commands,
152+
world: &World,
153+
entity: Entity,
154+
scene_hook: &VoxelSceneHook,
155+
) {
156+
let entity_ref = world.entity(entity);
157+
let mut entity_commands = commands.entity(entity);
158+
(scene_hook.hook)(&entity_ref, &mut entity_commands);
159+
let Some(children) = entity_ref.get::<Children>() else { return };
160+
for child in children.as_ref() {
161+
run_hook_recursive(commands, world, *child, scene_hook);
123162
}
124163
}
125164

@@ -221,7 +260,7 @@ fn transform_from_frame(frame: &Frame) -> Mat4 {
221260

222261
#[cfg(test)]
223262
mod tests {
224-
use bevy::{app::App, asset::{AssetPlugin, AssetServer, LoadState, AssetApp}, MinimalPlugins, render::texture::ImagePlugin, hierarchy::Children, reflect::Enum};
263+
use bevy::{app::App, asset::{AssetPlugin, AssetServer, LoadState, AssetApp}, MinimalPlugins, render::texture::ImagePlugin, hierarchy::Children};
225264
use crate::VoxScenePlugin;
226265
use super::*;
227266

@@ -293,21 +332,26 @@ mod tests {
293332
let mut app = App::new();
294333
let handle = setup_and_load_voxel_scene(&mut app, "test.vox#outer-group/inner-group").await;
295334
app.update();
296-
335+
297336
assert_eq!(app.world.resource::<AssetServer>().load_state(handle.clone()), LoadState::Loaded);
298-
let entity = app.world.spawn(VoxelSceneBundle {
337+
let entity = app.world.spawn(VoxelSceneHookBundle {
299338
scene: handle,
339+
hook: VoxelSceneHook::new(move |entity, _| {
340+
let Some(name) = entity.get::<Name>() else { return };
341+
let expected_names: [&'static str; 3] = ["outer-group/inner-group", "outer-group/inner-group/dice", "outer-group/inner-group/walls"];
342+
assert!(expected_names.contains(&name.as_str()));
343+
}),
300344
..Default::default()
301345
}).id();
302346
app.update();
303-
304347
assert!(app.world.get::<Handle<VoxelScene>>(entity).is_none());
305348
assert_eq!(app.world.query::<&VoxelLayer>().iter(&app.world).len(), 5, "5 voxel nodes spawned in this scene slice");
306349
assert_eq!(app.world.query::<&Name>().iter(&app.world).len(), 3, "But only 3 of the voxel nodes are named");
307350
assert_eq!(app.world.get::<Name>(entity).expect("Name component").as_str(), "outer-group/inner-group");
308351
let children = app.world.get::<Children>(entity).expect("children of inner-group").as_ref();
309352
assert_eq!(children.len(), 4, "inner-group has 4 children");
310353
assert_eq!(app.world.get::<Name>(*children.last().expect("last child")).expect("Name component").as_str(), "outer-group/inner-group/dice");
354+
app.update(); // fire the hooks
311355
}
312356

313357
/// `await` the response from this and then call `app.update()`

0 commit comments

Comments
 (0)