/bevy-scene-hook

Ad-hoc component insertion for bevy scenes

Primary LanguageRustApache License 2.0Apache-2.0

Bevy Scene hook

Bevy tracking Latest version Apache 2.0 Documentation

A proof of concept for adding components ad-hoc within code to entities spawned through scenes (such as gltf files) in the bevy game engine.

If you don't mind adding such a small dependency to your code rather than copy/pasting the code as a module, you can get it from crates.io.

Usage

  1. Add the crate to your dependencies
[dependencies]
bevy-scene-hook = "8.0.0"
  1. Add the plugin
.add_plugin(HookPlugin)

Example

use bevy_scene_hook::{SceneHook, HookedSceneBundle};

enum PileType { Drawing }

#[derive(Component)]
struct Pile(PileType);

#[derive(Component)]
struct Card;

fn load_scene(mut cmds: Commands, asset_server: Res<AssetServer>) {
   cmds.spawn_bundle(HookedSceneBundle {
       scene: SceneBundle { scene: asset_server.load("scene.glb#Scene0"), ..default() },
       hook: SceneHook::new(|entity, cmds| {
           match entity.get::<Name>().map(|t|t.as_str()) {
               Some("Pile") => cmds.insert(Pile(PileType::Drawing)),
               Some("Card") => cmds.insert(Card),
               _ => cmds,
           };
       }),
   });
}

It loads the scene.glb file when the game starts. When the scene is fully loaded, the closure passed to SceneHook::new is ran for each entity present in the scene. We add a Pile component to entities with a Name component of value "Pile".

It is possible to name object in glb scenes in blender using the Outliner dock (the tree view at the top right) and double-clicking object names.

Implementation

bevy-scene-hook is a tinny crate, here is copy/pastable code you can directly vendor in your project:

use bevy::{
    prelude::*,
    scene::SceneInstance,
    ecs::{world::EntityRef, system::EntityCommands},
};
#[derive(Component, Debug)]
pub struct SceneHooked;

#[derive(Component)]
pub struct SceneHook {
    hook: Box<dyn Fn(&EntityRef, &mut EntityCommands) + Send + Sync + 'static>,
}
impl SceneHook {
    pub fn new<F: Fn(&EntityRef, &mut EntityCommands) + Send + Sync + 'static>(hook: F) -> Self {
        Self { hook: Box::new(hook) }
    }
}

pub fn run_hooks(
    unloaded_instances: Query<(Entity, &SceneInstance, &SceneHook), Without<SceneHooked>>,
    scene_manager: Res<SceneSpawner>,
    world: &World,
    mut cmds: Commands,
) {
    for (entity, instance, hooked) in unloaded_instances.iter() {
        if scene_manager.instance_is_ready(**instance) {
            cmds.entity(entity).insert(SceneHooked);
        }
        let entities = scene_manager
            .iter_instance_entities(**instance)
            .chain(std::iter::once(entity));
        for entity_ref in entities.filter_map(|e| world.get_entity(e)) {
            let mut cmd = cmds.entity(entity_ref.id());
            (hooked.hook)(&entity_ref, &mut cmd);
        }
    }
}

pub struct HookPlugin;
impl Plugin for HookPlugin {
    fn build(&self, app: &mut App) { app.add_systems(Update, run_hooks); }
}

Note that bevy-scene-hook also has a few items defined for user convinience:

  • HookedSceneBundle
  • HookedSceneState
  • is_scene_hooked

Those extra items are all defined in lib.rs.

Change log

  • 1.1.0: Add is_loaded method to SceneInstance
  • 1.2.0: Add the world module containing a SceneHook trait that has exclusive world access. Useful if you want access to assets for example.
  • 2.0.0: Breaking: bump bevy version to 0.7 (you should be able to upgrade from 1.2.0 without changing your code)
  • 3.0.0: Breaking: completely rework the crate.
    • Remove the world module, as the base hook method has become much more powerful.
    • Rename SceneHook to Hook, now Hook has a unique method to implement.
    • You don't have to add any system yourself, now you have to add the HookPlugin plugin to your app.
    • Move the API exclusively to a new SystemParam: HookingSceneSpawner. Please use that parameter to add and remove scenes that contain hooks. (please tell if you you accidentally spell it HonkingSceneSpawner more than once 🦆)
    • Moved the when_spawned run criteria to the is_scene_hooked function exposed at the root of the crate, the HookedSceneState system parameter or the SceneLoaded component. Please use any of those three instead of when_spawned.
    • Now supports passing closures as hooks, instead of having to define a trait each time.
    • Now supports adding multiple of the same scene! Doesn't handle hot-reloading, but that's alright since bevy's scene hot-reloading is currently broken anyway :D
  • 3.1.0: make run_hooks system public so that it's possible to add it to any stage you want in relation to any other system you want.
  • 4.0.0: Breaking: bump bevy version to 0.8
    • Uses the new scene bundle system
    • Rename SceneLoaded to SceneHooked.
    • Removed the Hook trait, now SceneHook::new accepts a closure.
    • Instead of using HookingSceneSpawner, uses HookedSceneBundle and spawn it into an entity.
  • 4.1.0: Add HookedDynamicSceneBundle to use with DynamicScenes. Thanks Shatur (nicopap#3)
  • 5.0.0: Breaking: bump bevy version to 0.9
  • 5.1.1: My bad, I accidentally published to version 5.1.0 instead of 5.0.0
  • 5.1.2: Fix scenes never triggering hooks due to a missing check. Thanks sdfgeoff (#5) If you depend on bevy-scene-hook as a cargo dependency, you must run cargo update to get this fix.
  • 5.2.0: Add the reload module, defining reload::Hook, a variant of SceneHook that handles gracefully reloads and unloads.
  • 6.0.0: Breaking: bump bevy version to 0.10.
  • 7.0.0: Breaking: bump bevy version to 0.11.
  • 8.0.0: Add the root entity of the scene to the list of traversed scene entities.
    • May be breaking if you were blanket-adding components to entities & were relying on the root not being part of them.
    • Also if somehow the root has a property you were testing when adding components.
    • Thanks ickk (nicopap#7)

Version matrix

bevy latest supporting version
0.11 8.0.0
0.10 6.0.0
0.9 5.2.0
0.8 4.1.0
0.7 3.1.0
0.6 1.2.0

License

This library is licensed under Apache 2.0.