feat(Blenvy:Bevy):

* lots of doc updates
 * removed obsolete code
 * some cleanups
 * started adding & upgrading pieces of save_load into blenvy
This commit is contained in:
kaosat.dev 2024-07-16 23:39:09 +02:00
parent 0938a1d10c
commit 30b052d4d2
8 changed files with 75 additions and 1035 deletions

View File

@ -3,8 +3,8 @@ name = "blenvy"
version = "0.1.0" version = "0.1.0"
authors = ["Mark 'kaosat-dev' Moissette"] authors = ["Mark 'kaosat-dev' Moissette"]
description = "Allows you to define Bevy components direclty inside gltf files and instanciate the components on the Bevy side." description = "Allows you to define Bevy components direclty inside gltf files and instanciate the components on the Bevy side."
homepage = "https://github.com/kaosat-dev/Blender_bevy_components_workflow" homepage = "https://github.com/kaosat-dev/Blenvy"
repository = "https://github.com/kaosat-dev/Blender_bevy_components_workflow" repository = "https://github.com/kaosat-dev/Blenvy"
keywords = ["gamedev", "bevy", "assets", "gltf", "components"] keywords = ["gamedev", "bevy", "assets", "gltf", "components"]
categories = ["game-development"] categories = ["game-development"]
edition = "2021" edition = "2021"

View File

@ -1,12 +1,13 @@
[![Crates.io](https://img.shields.io/crates/v/blenvy)](https://crates.io/crates/blenvy) [![Crates.io](https://img.shields.io/crates/v/blenvy)](https://crates.io/crates/blenvy)
[![Docs](https://img.shields.io/docsrs/blenvy)](https://docs.rs/blenvy/latest/blenvy/) [![Docs](https://img.shields.io/docsrs/blenvy)](https://docs.rs/blenvy/latest/blenvy/)
[![License](https://img.shields.io/crates/l/blenvy)](https://github.com/kaosat-dev/Blender_bevy_components_workflow/blob/main/crates/blenvy/License.md) [![License](https://img.shields.io/crates/l/blenvy)](https://github.com/kaosat-dev/Blenvy/blob/main/crates/blenvy/License.md)
[![Bevy tracking](https://img.shields.io/badge/Bevy%20tracking-released%20version-lightblue)](https://github.com/bevyengine/bevy/blob/main/docs/plugins_guidelines.md#main-branch-tracking) [![Bevy tracking](https://img.shields.io/badge/Bevy%20tracking-released%20version-lightblue)](https://github.com/bevyengine/bevy/blob/main/docs/plugins_guidelines.md#main-branch-tracking)
# blenvy # blenvy
this crate adds the ability to define Blueprints/Prefabs for [Bevy](https://bevyengine.org/) inside gltf files and spawn them in Bevy. This crate allows you to
- define [Bevy](https://bevyengine.org/) components direclty inside gltf files and instanciate the components on the Bevy side.
- define Blueprints/Prefabs for [Bevy](https://bevyengine.org/) inside gltf files and spawn them in Bevy.
* Allows you to create lightweight levels, where all assets are different gltf files and loaded after the main level is loaded * Allows you to create lightweight levels, where all assets are different gltf files and loaded after the main level is loaded
* Allows you to spawn different entities from gtlf files at runtime in a clean manner, including simplified animation support ! * Allows you to spawn different entities from gtlf files at runtime in a clean manner, including simplified animation support !
@ -16,8 +17,23 @@ A blueprint is a set of **overrideable** components + a hierarchy: ie
* a component called BlueprintInfo * a component called BlueprintInfo
Particularly useful when using [Blender](https://www.blender.org/) as an editor for the [Bevy](https://bevyengine.org/) game engine, combined with the Blender add-on that do a lot of the work for you Particularly useful when using [Blender](https://www.blender.org/) as an editor for the [Bevy](https://bevyengine.org/) game engine, combined with the Blender add-on that do a lot of the work for you
- [blenvy](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/blenvy) - [blenvy](https://github.com/kaosat-dev/Blenvy/tree/main/tools/blenvy)
- allows you to create a Json export of all your components/ registered types.
Its main use case is as a backbone for the [```blenvy``` Blender add-on](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/blenvy), that allows you to add & edit components directly in Blender, using the actual type definitions from Bevy
(and any of your custom types & components that you register in Bevy).
- adds the ability to easilly **save** and **load** your game worlds for [Bevy](https://bevyengine.org/) .
* leverages blueprints & seperation between
* **dynamic** entities : entities that can change during the lifetime of your app/game
* **static** entities : entities that do NOT change (typically, a part of your levels/ environements)
* and allows allow for :
* a simple save/load workflow thanks to the above
* ability to specify **which entities** to save or to exclude
* ability to specify **which components** to save or to exclude
* ability to specify **which resources** to save or to exclude
* small(er) save files (only a portion of the entities is saved)
Particularly useful when using [Blender](https://www.blender.org/) as an editor for the [Bevy](https://bevyengine.org/) game engine, combined with the [Blender plugin](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/gltf_auto_export) that does a lot of the work for you (including spliting generating seperate gltf files for your static vs dynamic assets)
## Usage ## Usage
@ -40,12 +56,27 @@ fn main() {
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
.add_plugins(BlenvyPlugin) .add_plugins(BlenvyPlugin)
.add_systems(Startup, setup_game)
.add_systems(Update, spawn_blueprint_instance)
.run(); .run();
} }
// not shown here: any other setup that is not specific to blueprints
fn spawn_blueprint( // this is how you setup & spawn a level from a blueprint
fn setup_game(
mut commands: Commands,
) {
// here we spawn our game world/level, which is also a blueprint !
commands.spawn((
BlueprintInfo::from_path("levels/World.glb"), // all we need is a Blueprint info...
SpawnBlueprint, // and spawnblueprint to tell blenvy to spawn the blueprint now
HideUntilReady, // only reveal the level once it is ready
GameWorldTag,
));
}
fn spawn_blueprint_instance(
mut commands: Commands, mut commands: Commands,
keycode: Res<Input<KeyCode>>, keycode: Res<Input<KeyCode>>,
){ ){
@ -152,7 +183,7 @@ any component you specify when spawning the Blueprint that is also specified **w
for example for example
```rust no_run ```rust no_run
commands.spawn(( commands.spawn((
BlueprintInfo("Health_Pickup".to_string()), BlueprintInfo(path: "Health_Pickup.glb".into()),
SpawnBlueprint, SpawnBlueprint,
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)), TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
HealthPowerUp(20)// if this is component is also present inside the "Health_Pickup" blueprint, that one will be replaced with this component during spawning HealthPowerUp(20)// if this is component is also present inside the "Health_Pickup" blueprint, that one will be replaced with this component during spawning
@ -168,7 +199,7 @@ There is also a ```BluePrintBundle``` for convenience , which just has
## Additional information ## Additional information
- When a blueprint is spawned, all its children entities (and nested children etc) also have an ```InBlueprint``` component that gets insert - When a blueprint is spawned, an ```InBlueprint``` component is inserted into all its children entities (and nested children etc)
- this crate also provides a special optional ```GameWorldTag``` component: this is useful when you want to keep all your spawned entities inside a root entity - this crate also provides a special optional ```GameWorldTag``` component: this is useful when you want to keep all your spawned entities inside a root entity
You can use it in your queries to add your entities as children of this "world" You can use it in your queries to add your entities as children of this "world"
@ -178,15 +209,9 @@ This way all your levels, your dynamic entities etc, are kept seperated from UI
```rust no_run ```rust no_run
commands.spawn(( commands.spawn((
SceneBundle { BlueprintInfo::from_path("levels/World.glb"),
scene: models SpawnBlueprint,
.get(game_assets.world.id()) HideUntilReady,
.expect("main level should have been loaded")
.scenes[0]
.clone(),
..default()
},
bevy::prelude::Name::from("world"),
GameWorldTag, // here it is GameWorldTag, // here it is
)); ));
``` ```
@ -204,7 +229,7 @@ Typically , the order of systems should be
***bevy_gltf_components (GltfComponentsSet::Injection)*** => ***blenvy (GltfBlueprintsSet::Spawn, GltfBlueprintsSet::AfterSpawn)*** => ***replace_proxies*** ***bevy_gltf_components (GltfComponentsSet::Injection)*** => ***blenvy (GltfBlueprintsSet::Spawn, GltfBlueprintsSet::AfterSpawn)*** => ***replace_proxies***
see https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/blenvy/basic for how to set it up correctly see https://github.com/kaosat-dev/Blenvy/tree/main/examples/blenvy/basic for how to set it up correctly
@ -258,9 +283,9 @@ pub fn animation_change_on_proximity_foxes(
} }
``` ```
see https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/blenvy/animation for how to set it up correctly see https://github.com/kaosat-dev/Blenvy/tree/main/examples/blenvy/animation for how to set it up correctly
particularly from https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/blenvy/animation/game/in_game.rs particularly from https://github.com/kaosat-dev/Blenvy/tree/main/examples/blenvy/animation/game/in_game.rs
## Materials ## Materials
@ -280,22 +305,22 @@ material_library: true // defaults to false, enable this to enable automatic in
```blenvy``` currently does NOT take care of loading those at runtime ```blenvy``` currently does NOT take care of loading those at runtime
see https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/blenvy/materials for how to set it up correctly see https://github.com/kaosat-dev/Blenvy/tree/main/examples/blenvy/materials for how to set it up correctly
Generating optimised blueprints and material libraries can be automated using the latests version of the [Blender plugin](https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/tools/gltf_auto_export) Generating optimised blueprints and material libraries can be automated using the latests version of the [Blender plugin](https://github.com/kaosat-dev/Blenvy/tree/main/tools/gltf_auto_export)
## Examples ## Examples
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/blenvy/basic https://github.com/kaosat-dev/Blenvy/tree/main/examples/blenvy/basic
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/blenvy/basic_xpbd_physics https://github.com/kaosat-dev/Blenvy/tree/main/examples/blenvy/basic_xpbd_physics
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/blenvy/animation https://github.com/kaosat-dev/Blenvy/tree/main/examples/blenvy/animation
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/blenvy/materials https://github.com/kaosat-dev/Blenvy/tree/main/examples/blenvy/materials
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/blenvy/multiple_levels_multiple_blendfiles https://github.com/kaosat-dev/Blenvy/tree/main/examples/blenvy/multiple_levels_multiple_blendfiles
## Compatible Bevy versions ## Compatible Bevy versions

View File

@ -1,190 +0,0 @@
use std::path::Path;
use bevy::{
asset::{AssetServer, Assets, Handle},
ecs::{
component::Component,
entity::Entity,
query::{Added, With},
reflect::ReflectComponent,
system::{Commands, Query, Res, ResMut},
},
gltf::Gltf,
hierarchy::{Children, Parent},
log::debug,
pbr::StandardMaterial,
reflect::Reflect,
render::mesh::Mesh,
};
use crate::{AssetLoadTracker, BlueprintAssetsLoadState, BlenvyConfig, BlueprintInstanceReady};
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// struct containing the name & path of the material to apply
pub struct MaterialInfo {
pub name: String,
pub path: String,
}
/// flag component
#[derive(Component)]
pub(crate) struct BlueprintMaterialAssetsLoaded;
/// flag component
#[derive(Component)]
pub(crate) struct BlueprintMaterialAssetsNotLoaded;
/// system that injects / replaces materials from material library
pub(crate) fn materials_inject(
blenvy_config: ResMut<BlenvyConfig>,
material_infos: Query<(Entity, &MaterialInfo), Added<MaterialInfo>>,
asset_server: Res<AssetServer>,
mut commands: Commands,
) {
for (entity, material_info) in material_infos.iter() {
println!("Entity with material info {:?} {:?}", entity, material_info);
let material_full_path = format!("{}#{}", material_info.path, material_info.name);
if blenvy_config
.materials_cache
.contains_key(&material_full_path)
{
debug!("material is cached, retrieving");
blenvy_config
.materials_cache
.get(&material_full_path)
.expect("we should have the material available");
commands
.entity(entity)
.insert(BlueprintMaterialAssetsLoaded);
} else {
let material_file_handle = asset_server.load_untyped(&material_info.path.clone()); // : Handle<Gltf>
let material_file_id = material_file_handle.id();
let asset_infos: Vec<AssetLoadTracker> = vec![AssetLoadTracker {
name: material_info.name.clone(),
path: material_info.path.clone(),
id: material_file_id,
loaded: false,
handle: material_file_handle.clone(),
}];
commands
.entity(entity)
.insert(BlueprintAssetsLoadState {
all_loaded: false,
asset_infos,
..Default::default()
})
.insert(BlueprintMaterialAssetsNotLoaded);
}
}
}
// TODO, merge with blueprints_check_assets_loading, make generic ?
pub(crate) fn check_for_material_loaded(
mut blueprint_assets_to_load: Query<
(Entity, &mut BlueprintAssetsLoadState),
With<BlueprintMaterialAssetsNotLoaded>,
>,
asset_server: Res<AssetServer>,
mut commands: Commands,
) {
for (entity, mut assets_to_load) in blueprint_assets_to_load.iter_mut() {
let mut all_loaded = true;
let mut loaded_amount = 0;
let total = assets_to_load.asset_infos.len();
for tracker in assets_to_load.asset_infos.iter_mut() {
let asset_id = tracker.id;
let loaded = asset_server.is_loaded_with_dependencies(asset_id);
tracker.loaded = loaded;
if loaded {
loaded_amount += 1;
} else {
all_loaded = false;
}
}
let progress: f32 = loaded_amount as f32 / total as f32;
assets_to_load.progress = progress;
if all_loaded {
assets_to_load.all_loaded = true;
commands
.entity(entity)
.insert(BlueprintMaterialAssetsLoaded)
.remove::<BlueprintMaterialAssetsNotLoaded>();
}
}
}
/// system that injects / replaces materials from material library
pub(crate) fn materials_inject2(
mut blenvy_config: ResMut<BlenvyConfig>,
material_infos: Query<
(&MaterialInfo, &Children),
(
Added<BlueprintMaterialAssetsLoaded>,
With<BlueprintMaterialAssetsLoaded>,
),
>,
with_materials_and_meshes: Query<
(),
(
With<Parent>,
With<Handle<StandardMaterial>>,
With<Handle<Mesh>>,
),
>,
assets_gltf: Res<Assets<Gltf>>,
asset_server: Res<AssetServer>,
mut commands: Commands,
) {
for (material_info, children) in material_infos.iter() {
let material_full_path = format!("{}#{}", material_info.path, material_info.name);
let mut material_found: Option<&Handle<StandardMaterial>> = None;
if blenvy_config
.materials_cache
.contains_key(&material_full_path)
{
debug!("material is cached, retrieving");
let material = blenvy_config
.materials_cache
.get(&material_full_path)
.expect("we should have the material available");
material_found = Some(material);
} else {
let model_handle: Handle<Gltf> = asset_server.load(material_info.path.clone()); // FIXME: kinda weird now
let mat_gltf = assets_gltf
.get(model_handle.id())
.expect("material should have been preloaded");
if mat_gltf.named_materials.contains_key(&material_info.name as &str) {
let material = mat_gltf
.named_materials
.get(&material_info.name as &str)
.expect("this material should have been loaded");
blenvy_config
.materials_cache
.insert(material_full_path, material.clone());
material_found = Some(material);
}
}
if let Some(material) = material_found {
for child in children.iter() {
if with_materials_and_meshes.contains(*child) {
debug!(
"injecting material {}, path: {:?}",
material_info.name,
material_info.path.clone()
);
commands.entity(*child).insert(material.clone());
}
}
}
}
}

View File

@ -1,248 +0,0 @@
/// helper component, for tracking loaded assets's loading state, id , handle etc
#[derive(Default, Debug)]
pub(crate) struct AssetLoadTracker<T: bevy::prelude::Asset> {
#[allow(dead_code)]
pub name: String,
pub id: AssetId<T>,
pub loaded: bool,
#[allow(dead_code)]
pub handle: Handle<T>,
}
/// helper component, for tracking loaded assets
#[derive(Component, Debug)]
pub(crate) struct AssetsToLoad<T: bevy::prelude::Asset> {
pub all_loaded: bool,
pub asset_infos: Vec<AssetLoadTracker<T>>,
pub progress: f32,
}
impl<T: bevy::prelude::Asset> Default for AssetsToLoad<T> {
fn default() -> Self {
Self {
all_loaded: Default::default(),
asset_infos: Default::default(),
progress: Default::default(),
}
}
}
/// flag component, usually added when a blueprint is loaded
#[derive(Component)]
pub(crate) struct BlueprintAssetsLoaded;
/// flag component
#[derive(Component)]
pub(crate) struct BlueprintAssetsNotLoaded;
/// spawning prepare function,
/// * also takes into account the already exisiting "override" components, ie "override components" > components from blueprint
pub(crate) fn prepare_blueprints(
spawn_placeholders: Query<
(
Entity,
&BlueprintName,
Option<&Parent>,
Option<&Library>,
Option<&Name>,
Option<&BlueprintsList>,
),
(Added<BlueprintName>, Added<SpawnBlueprint>, Without<Spawned>),
>,
mut commands: Commands,
asset_server: Res<AssetServer>,
blenvy_config: Res<BluePrintsConfig>,
) {
for (entity, blupeprint_name, original_parent, library_override, name, blueprints_list) in
spawn_placeholders.iter()
{
debug!(
"requesting to spawn {:?} for entity {:?}, id: {:?}, parent:{:?}",
blupeprint_name.0, name, entity, original_parent
);
// println!("main model path {:?}", model_path);
if blueprints_list.is_some() {
let blueprints_list = blueprints_list.unwrap();
// println!("blueprints list {:?}", blueprints_list.0.keys());
let mut asset_infos: Vec<AssetLoadTracker<Gltf>> = vec![];
let library_path =
library_override.map_or_else(|| &blenvy_config.library_folder, |l| &l.0);
for (blueprint_name, _) in blueprints_list.0.iter() {
let model_file_name = format!("{}.{}", &blueprint_name, &blenvy_config.format);
let model_path = Path::new(&library_path).join(Path::new(model_file_name.as_str()));
let model_handle: Handle<Gltf> = asset_server.load(model_path.clone());
let model_id = model_handle.id();
let loaded = asset_server.is_loaded_with_dependencies(model_id);
if !loaded {
asset_infos.push(AssetLoadTracker {
name: model_path.to_string_lossy().into(),
id: model_id,
loaded: false,
handle: model_handle.clone(),
});
}
}
// if not all assets are already loaded, inject a component to signal that we need them to be loaded
if !asset_infos.is_empty() {
commands
.entity(entity)
.insert(AssetsToLoad {
all_loaded: false,
asset_infos,
..Default::default()
})
.insert(BlueprintAssetsNotLoaded);
} else {
commands.entity(entity).insert(BlueprintAssetsLoaded);
}
} else {
// in case there are no blueprintsList, we revert back to the old behaviour
commands.entity(entity).insert(BlueprintAssetsLoaded);
}
}
}
pub(crate) fn blueprints_check_assets_loading(
mut blueprint_assets_to_load: Query<
(Entity, &mut AssetsToLoad<Gltf>),
With<BlueprintAssetsNotLoaded>,
>,
asset_server: Res<AssetServer>,
mut commands: Commands,
) {
for (entity, mut assets_to_load) in blueprint_assets_to_load.iter_mut() {
let mut all_loaded = true;
let mut loaded_amount = 0;
let total = assets_to_load.asset_infos.len();
for tracker in assets_to_load.asset_infos.iter_mut() {
let asset_id = tracker.id;
let loaded = asset_server.is_loaded_with_dependencies(asset_id);
tracker.loaded = loaded;
if loaded {
loaded_amount += 1;
} else {
all_loaded = false;
}
}
let progress: f32 = loaded_amount as f32 / total as f32;
// println!("progress: {}",progress);
assets_to_load.progress = progress;
if all_loaded {
assets_to_load.all_loaded = true;
commands
.entity(entity)
.insert(BlueprintAssetsLoaded)
.remove::<BlueprintAssetsNotLoaded>();
}
}
}
pub(crate) fn blueprints_spawn(
spawn_placeholders: Query<
(
Entity,
&BlueprintName,
Option<&Transform>,
Option<&Parent>,
Option<&Library>,
Option<&AddToGameWorld>,
Option<&Name>,
),
(
With<BlueprintAssetsLoaded>,
Added<BlueprintAssetsLoaded>,
Without<BlueprintAssetsNotLoaded>,
),
>,
mut commands: Commands,
mut game_world: Query<Entity, With<GameWorldTag>>,
assets_gltf: Res<Assets<Gltf>>,
asset_server: Res<AssetServer>,
blenvy_config: Res<BluePrintsConfig>,
children: Query<&Children>,
) {
for (
entity,
blupeprint_name,
transform,
original_parent,
library_override,
add_to_world,
name,
) in spawn_placeholders.iter()
{
debug!(
"attempting to spawn {:?} for entity {:?}, id: {:?}, parent:{:?}",
blupeprint_name.0, name, entity, original_parent
);
let what = &blupeprint_name.0;
let model_file_name = format!("{}.{}", &what, &blenvy_config.format);
// library path is either defined at the plugin level or overriden by optional Library components
let library_path =
library_override.map_or_else(|| &blenvy_config.library_folder, |l| &l.0);
let model_path = Path::new(&library_path).join(Path::new(model_file_name.as_str()));
// info!("attempting to spawn {:?}", model_path);
let model_handle: Handle<Gltf> = asset_server.load(model_path.clone()); // FIXME: kinda weird now
let gltf = assets_gltf.get(&model_handle).unwrap_or_else(|| {
panic!(
"gltf file {:?} should have been loaded",
model_path.to_str()
)
});
// WARNING we work under the assumtion that there is ONLY ONE named scene, and that the first one is the right one
let main_scene_name = gltf
.named_scenes
.keys()
.next()
.expect("there should be at least one named scene in the gltf file to spawn");
let scene = &gltf.named_scenes[main_scene_name];
// transforms are optional, but still deal with them correctly
let mut transforms: Transform = Transform::default();
if transform.is_some() {
transforms = *transform.unwrap();
}
let mut original_children: Vec<Entity> = vec![];
if let Ok(c) = children.get(entity) {
for child in c.iter() {
original_children.push(*child);
}
}
commands.entity(entity).insert((
SceneBundle {
scene: scene.clone(),
transform: transforms,
..Default::default()
},
Spawned,
OriginalChildren(original_children),
BlueprintAnimations {
// these are animations specific to the inside of the blueprint
named_animations: gltf.named_animations.clone(),
},
));
if add_to_world.is_some() {
let world = game_world
.get_single_mut()
.expect("there should be a game world present");
commands.entity(world).add_child(entity);
}
}
}

View File

@ -1,396 +0,0 @@
use std::path::{Path, PathBuf};
use bevy::{gltf::Gltf, prelude::*, utils::hashbrown::HashMap};
use serde_json::Value;
use crate::{BlueprintAssets, AssetsToLoad, AssetLoadTracker, BlenvyConfig, BlueprintAnimations, BlueprintAssetsLoaded, BlueprintAssetsNotLoaded};
/// this is a flag component for our levels/game world
#[derive(Component)]
pub struct GameWorldTag;
/// Main component for the blueprints
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct BlueprintName(pub String);
/// path component for the blueprints
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct BlueprintPath(pub String);
/// flag component needed to signify the intent to spawn a Blueprint
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct SpawnBlueprint;
#[derive(Component)]
/// flag component for dynamically spawned scenes
pub struct Spawned;
#[derive(Component, Debug)]
/// flag component added when a Blueprint instance ist Ready : ie :
/// - its assets have loaded
/// - it has finished spawning
pub struct BlueprintInstanceReady;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// flag component marking any spwaned child of blueprints ..unless the original entity was marked with the `NoInBlueprint` marker component
pub struct InBlueprint;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// flag component preventing any spawned child of blueprints to be marked with the `InBlueprint` component
pub struct NoInBlueprint;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
// this allows overriding the default library path for a given entity/blueprint
pub struct Library(pub PathBuf);
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// flag component to force adding newly spawned entity as child of game world
pub struct AddToGameWorld;
#[derive(Component)]
/// helper component, just to transfer child data
pub(crate) struct OriginalChildren(pub Vec<Entity>);
#[derive(Event, Debug)]
pub enum BlueprintEvent {
/// event fired when a blueprint has finished loading its assets & before it attempts spawning
AssetsLoaded {
blueprint_name: String,
blueprint_path: String,
// TODO: add assets list ?
},
/// event fired when a blueprint is COMPLETELY done spawning ie
/// - all its assets have been loaded
/// - the spawning attempt has been sucessfull
Spawned {
blueprint_name: String,
blueprint_path: String,
},
///
Ready {
blueprint_path: String,
}
}
use gltf::Gltf as RawGltf;
pub(crate) fn blueprints_prepare_spawn(
spawn_placeholders: Query<
(
Entity,
&BlueprintPath,
),
(Added<BlueprintPath>, Without<Spawned>, Without<SpawnBlueprint>)>,
// before 0.14 we have to use a seperate query, after migrating we can query at the root level
entities_with_assets: Query<
(
Entity,
/*&BlueprintName,
&BlueprintPath,
Option<&Parent>,*/
Option<&Name>,
Option<&BlueprintAssets>,
),
(Added<BlueprintAssets>), // Added<BlueprintAssets>
>,
bla_bla : Query<
(
Entity,
&BlueprintName,
&BlueprintPath,
Option<&Parent>,
Option<&BlueprintAssets>,
),(Added<BlueprintPath>)
>,
mut commands: Commands,
asset_server: Res<AssetServer>,
) {
for (entity, blueprint_path) in spawn_placeholders.iter() {
println!("added blueprint_path {:?}", blueprint_path);
/*commands.entity(entity).insert(
SceneBundle {
scene: asset_server.load(format!("{}#Scene0", &blueprint_path.0)), // "levels/World.glb#Scene0"),
..default()
},
);*/
// let model_handle: Handle<Gltf> = asset_server.load(model_path.clone());
}
for (entity, blueprint_name, blueprint_path, parent, all_assets) in bla_bla.iter() {
println!("added blueprint to spawn {:?} {:?}", blueprint_name, blueprint_path);
println!("all assets {:?}", all_assets);
/* prefetch attempt */
let gltf = RawGltf::open(format!("assets/{}", blueprint_path.0)).unwrap();// RawGltf::open("examples/Box.gltf")?;
for scene in gltf.scenes() {
let foo_extras = scene.extras().clone().unwrap();
let lookup: HashMap<String, Value> = serde_json::from_str(&foo_extras.get()).unwrap();
for (key, value) in lookup.clone().into_iter() {
println!("{} / {}", key, value);
}
if lookup.contains_key("BlueprintAssets"){
let assets_raw = &lookup["BlueprintAssets"];
println!("ASSETS RAW {}", assets_raw);
let x: BlueprintAssets = ron::from_str(&assets_raw.as_str().unwrap()).unwrap();
println!("YAHA {:?}", x);
}
//println!("SCENE EXTRAS {:?}", foo_extras);
}
//////////////
let untyped_handle = asset_server.load_untyped(&blueprint_path.0);
let asset_id = untyped_handle.id();
let loaded = asset_server.is_loaded_with_dependencies(asset_id);
let mut asset_infos: Vec<AssetLoadTracker> = vec![];
if !loaded {
asset_infos.push(AssetLoadTracker {
name: blueprint_name.0.clone(),
id: asset_id,
loaded: false,
handle: untyped_handle.clone(),
});
}
// now insert load tracker
if !asset_infos.is_empty() {
commands
.entity(entity)
.insert(AssetsToLoad {
all_loaded: false,
asset_infos,
..Default::default()
})
.insert(BlueprintAssetsNotLoaded);
} else {
commands.entity(entity).insert(BlueprintAssetsLoaded);
}
}
for (child_entity, child_entity_name, all_assets) in entities_with_assets.iter(){
println!("added assets {:?} to {:?}", all_assets, child_entity_name);
if all_assets.is_some() {
let mut asset_infos: Vec<AssetLoadTracker> = vec![];
for asset in all_assets.unwrap().0.iter() {
let untyped_handle = asset_server.load_untyped(&asset.path);
//println!("untyped handle {:?}", untyped_handle);
//asset_server.load(asset.path);
let asset_id = untyped_handle.id();
//println!("ID {:?}", asset_id);
let loaded = asset_server.is_loaded_with_dependencies(asset_id);
//println!("Loaded ? {:?}", loaded);
if !loaded {
asset_infos.push(AssetLoadTracker {
name: asset.name.clone(),
id: asset_id,
loaded: false,
handle: untyped_handle.clone(),
});
}
}
// now insert load tracker
if !asset_infos.is_empty() {
commands
.entity(child_entity)
.insert(AssetsToLoad {
all_loaded: false,
asset_infos,
..Default::default()
})
.insert(BlueprintAssetsNotLoaded);
} else {
commands.entity(child_entity).insert(BlueprintAssetsLoaded);
}
}
}
}
pub(crate) fn blueprints_check_assets_loading(
mut blueprint_assets_to_load: Query<
(Entity, Option<&Name>, &BlueprintPath, &mut AssetsToLoad),
With<BlueprintAssetsNotLoaded>,
>,
asset_server: Res<AssetServer>,
mut commands: Commands,
mut blueprint_events: EventWriter<BlueprintEvent>,
) {
for (entity, entity_name, blueprint_path, mut assets_to_load) in blueprint_assets_to_load.iter_mut() {
let mut all_loaded = true;
let mut loaded_amount = 0;
let total = assets_to_load.asset_infos.len();
for tracker in assets_to_load.asset_infos.iter_mut() {
let asset_id = tracker.id;
let loaded = asset_server.is_loaded_with_dependencies(asset_id);
println!("loading {}: // load state: {:?}", tracker.name, asset_server.load_state(asset_id));
// FIXME: hack for now
let mut failed = false;// asset_server.load_state(asset_id) == bevy::asset::LoadState::Failed(_error);
match asset_server.load_state(asset_id) {
bevy::asset::LoadState::Failed(_) => {
failed = true
},
_ => {}
}
tracker.loaded = loaded || failed;
if loaded || failed {
loaded_amount += 1;
} else {
all_loaded = false;
}
}
let progress: f32 = loaded_amount as f32 / total as f32;
println!("progress: {}",progress);
assets_to_load.progress = progress;
if all_loaded {
assets_to_load.all_loaded = true;
println!("done with loading {:?}, inserting components", entity_name);
blueprint_events.send(BlueprintEvent::AssetsLoaded {blueprint_name:"".into(), blueprint_path: blueprint_path.0.clone() });
commands
.entity(entity)
.insert(BlueprintAssetsLoaded)
.remove::<BlueprintAssetsNotLoaded>()
.remove::<AssetsToLoad>()
;
}
}
}
pub(crate) fn blueprints_spawn(
spawn_placeholders: Query<
(
Entity,
&BlueprintName,
&BlueprintPath,
Option<&Transform>,
Option<&Parent>,
Option<&AddToGameWorld>,
Option<&Name>,
),
(
With<BlueprintAssetsLoaded>,
Added<BlueprintAssetsLoaded>,
Without<BlueprintAssetsNotLoaded>,
),
>,
mut commands: Commands,
mut game_world: Query<Entity, With<GameWorldTag>>,
assets_gltf: Res<Assets<Gltf>>,
asset_server: Res<AssetServer>,
children: Query<&Children>,
) {
for (
entity,
blupeprint_name,
blueprint_path,
transform,
original_parent,
add_to_world,
name,
) in spawn_placeholders.iter()
{
info!(
"attempting to spawn blueprint {:?} for entity {:?}, id: {:?}, parent:{:?}",
blupeprint_name.0, name, entity, original_parent
);
// info!("attempting to spawn {:?}", model_path);
let model_handle: Handle<Gltf> = asset_server.load(blueprint_path.0.clone()); // FIXME: kinda weird now
let gltf = assets_gltf.get(&model_handle).unwrap_or_else(|| {
panic!(
"gltf file {:?} should have been loaded",
&blueprint_path.0
)
});
// WARNING we work under the assumtion that there is ONLY ONE named scene, and that the first one is the right one
let main_scene_name = gltf
.named_scenes
.keys()
.next()
.expect("there should be at least one named scene in the gltf file to spawn");
let scene = &gltf.named_scenes[main_scene_name];
// transforms are optional, but still deal with them correctly
let mut transforms: Transform = Transform::default();
if transform.is_some() {
transforms = *transform.unwrap();
}
let mut original_children: Vec<Entity> = vec![];
if let Ok(c) = children.get(entity) {
for child in c.iter() {
original_children.push(*child);
}
}
let mut named_animations:HashMap<String, Handle<AnimationClip>> = HashMap::new() ;
for (key, value) in gltf.named_animations.iter() {
named_animations.insert(key.to_string(), value.clone());
}
commands.entity(entity).insert((
SceneBundle {
scene: scene.clone(),
transform: transforms,
..Default::default()
},
Spawned,
BlueprintInstanceReady, // FIXME: not sure if this is should be added here or in the post process
OriginalChildren(original_children),
BlueprintAnimations {
// these are animations specific to the inside of the blueprint
named_animations: named_animations//gltf.named_animations.clone(),
},
));
if add_to_world.is_some() {
let world = game_world
.get_single_mut()
.expect("there should be a game world present");
commands.entity(world).add_child(entity);
}
}
}

View File

@ -1,165 +0,0 @@
use std::any::TypeId;
use bevy::gltf::Gltf;
use bevy::prelude::*;
use bevy::scene::SceneInstance;
// use bevy::utils::hashbrown::HashSet;
use crate::{BlueprintAnimationPlayerLink, BlueprintAnimations, BlueprintInfo, BlueprintReadyForPostProcess, BlueprintInstanceReady, BlueprintSpawning, SubBlueprintSpawnRoot, SubBlueprintsSpawnTracker};
use crate::{SpawnBlueprint, Spawned};
use crate::{
BlueprintEvent, CopyComponents, InBlueprint, NoInBlueprint, OriginalChildren
};
/// this system is in charge of doing any necessary post processing after a blueprint scene has been spawned
/// - it removes one level of useless nesting
/// - it copies the blueprint's root components to the entity it was spawned on (original entity)
/// - it copies the children of the blueprint scene into the original entity
/// - it add `AnimationLink` components so that animations can be controlled from the original entity
/// - it cleans up/ removes a few , by then uneeded components
pub(crate) fn spawned_blueprint_post_process( // rename to '
unprocessed_entities: Query<
(
Entity,
&Children,
&OriginalChildren,
&BlueprintAnimations,
Option<&NoInBlueprint>,
Option<&Name>,
&BlueprintInfo,
// sub blueprint instances tracker
Option<&SubBlueprintSpawnRoot>
),
(With<SpawnBlueprint>, With<SceneInstance>, Added<BlueprintReadyForPostProcess>),
>,
added_animation_players: Query<(Entity, &Parent), Added<AnimationPlayer>>,
all_children: Query<&Children>,
mut trackers: Query<(Entity, &mut SubBlueprintsSpawnTracker, &BlueprintInfo)>,
mut commands: Commands,
mut blueprint_events: EventWriter<BlueprintEvent>,
) {
for (original, children, original_children, animations, no_inblueprint, name, blueprint_info, track_root) in
unprocessed_entities.iter()
{
info!("post processing blueprint for entity {:?}", name);
if children.len() == 0 {
warn!("timing issue ! no children found, please restart your bevy app (bug being investigated)");
continue;
}
// the root node is the first & normally only child inside a scene, it is the one that has all relevant components
let mut root_entity = Entity::PLACEHOLDER; //FIXME: and what about childless ones ?? => should not be possible normally
// let diff = HashSet::from_iter(original_children.0).difference(HashSet::from_iter(children));
// we find the first child that was not in the entity before (aka added during the scene spawning)
for c in children.iter() {
if !original_children.0.contains(c) {
root_entity = *c;
break;
}
}
// we flag all children of the blueprint instance with 'InBlueprint'
// can be usefull to filter out anything that came from blueprints vs normal children
if no_inblueprint.is_none() {
for child in all_children.iter_descendants(root_entity) {
commands.entity(child).insert(InBlueprint); // we do this here in order to avoid doing it to normal children
}
}
// copy components into from blueprint instance's root_entity to original entity
commands.add(CopyComponents {
source: root_entity,
destination: original,
exclude: vec![TypeId::of::<Parent>(), TypeId::of::<Children>()],
stringent: false,
});
// we move all of children of the blueprint instance one level to the original entity
if let Ok(root_entity_children) = all_children.get(root_entity) {
for child in root_entity_children.iter() {
// info!("copying child {:?} upward from {:?} to {:?}", names.get(*child), root_entity, original);
commands.entity(original).add_child(*child);
}
}
if animations.named_animations.keys().len() > 0 {
for (added, parent) in added_animation_players.iter() {
if parent.get() == root_entity {
// FIXME: stopgap solution: since we cannot use an AnimationPlayer at the root entity level
// and we cannot update animation clips so that the EntityPaths point to one level deeper,
// BUT we still want to have some marker/control at the root entity level, we add this
commands
.entity(original)
.insert(BlueprintAnimationPlayerLink(added));
}
}
}
commands.entity(original).remove::<SpawnBlueprint>();
commands.entity(original).remove::<Spawned>();
// commands.entity(original).remove::<Handle<Scene>>(); // FIXME: if we delete the handle to the scene, things get despawned ! not what we want
//commands.entity(original).remove::<BlueprintAssetsLoadState>(); // also clear the sub assets tracker to free up handles, perhaps just freeing up the handles and leave the rest would be better ?
//commands.entity(original).remove::<BlueprintAssetsLoaded>();
commands.entity(root_entity).despawn_recursive(); // Remove the root entity that comes from the spawned-in scene
commands.entity(original).insert( Visibility::Visible
);
commands.entity(original)
.insert(BlueprintInstanceReady)
.remove::<BlueprintSpawning>()
//
.remove::<BlueprintReadyForPostProcess>()
;
if let Some(track_root) = track_root {
//println!("got some root");
if let Ok((s_entity, mut tracker, bp_info)) = trackers.get_mut(track_root.0) {
// println!("found the tracker, setting loaded for {}", entity);
tracker.sub_blueprint_instances.entry(original).or_insert(true);
tracker.sub_blueprint_instances.insert(original, true);
// TODO: ugh, my limited rust knowledge, this is bad code
let mut all_spawned = true;
for key in tracker.sub_blueprint_instances.keys() {
let val = tracker.sub_blueprint_instances[key];
println!("Key: {key}, Spawned {}", val);
}
for val in tracker.sub_blueprint_instances.values() {
println!("spawned {}", val);
if !val {
all_spawned = false;
break;
}
}
if all_spawned { // TODO: move this to an other system, or "notify" the tracked root entity of the fact that all its sub blueprints have been loaded
println!("ALLLLL SPAAAAWNED for {}", track_root.0);
// commands.entity(track_root.0).insert(bundle)
blueprint_events.send(BlueprintEvent::Spawned {entity: track_root.0, blueprint_name: bp_info.name.clone(), blueprint_path: bp_info.path.clone()});
}
}
}
if trackers.get(original).is_err() {
// if it has no sub blueprint instances
blueprint_events.send(BlueprintEvent::Spawned {entity: original, blueprint_name: blueprint_info.name.clone(), blueprint_path: blueprint_info.path.clone()});
}
debug!("DONE WITH POST PROCESS");
info!("done instanciating blueprint for entity {:?}", name);
}
}

View File

@ -23,7 +23,7 @@ use bevy::{
/// # use bevy::gltf::*; /// # use bevy::gltf::*;
/// # use bevy_gltf_components::ComponentsFromGltfPlugin; /// # use bevy_gltf_components::ComponentsFromGltfPlugin;
/// ///
/// //too barebones of an example to be meaningfull, please see https://github.com/kaosat-dev/Blender_bevy_components_workflow/examples/basic for a real example /// //too barebones of an example to be meaningfull, please see https://github.com/kaosat-dev/Blenvy/examples/basic for a real example
/// fn main() { /// fn main() {
/// App::new() /// App::new()
/// .add_plugins(DefaultPlugins) /// .add_plugins(DefaultPlugins)

View File

@ -22,6 +22,10 @@ pub struct BlenvyConfig {
pub(crate) aabbs: bool, pub(crate) aabbs: bool,
pub(crate) aabb_cache: HashMap<String, Aabb>, // cache for aabbs pub(crate) aabb_cache: HashMap<String, Aabb>, // cache for aabbs
pub(crate) materials_cache: HashMap<String, Handle<StandardMaterial>>, // cache for materials pub(crate) materials_cache: HashMap<String, Handle<StandardMaterial>>, // cache for materials
// save & load
pub(crate) save_component_filter: SceneFilter,
pub(crate) save_resource_filter: SceneFilter,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -34,6 +38,10 @@ pub struct BlenvyPlugin {
/// Automatically generate aabbs for the blueprints root objects /// Automatically generate aabbs for the blueprints root objects
pub aabbs: bool, pub aabbs: bool,
// for save & load
pub save_component_filter: SceneFilter,
pub save_resource_filter: SceneFilter,
} }
impl Default for BlenvyPlugin { impl Default for BlenvyPlugin {
@ -43,6 +51,9 @@ impl Default for BlenvyPlugin {
registry_component_filter: SceneFilter::default(), registry_component_filter: SceneFilter::default(),
registry_resource_filter: SceneFilter::default(), registry_resource_filter: SceneFilter::default(),
aabbs: false, aabbs: false,
save_component_filter: SceneFilter::default(),
save_resource_filter: SceneFilter::default(),
} }
} }
} }
@ -63,6 +74,9 @@ impl Plugin for BlenvyPlugin {
aabb_cache: HashMap::new(), aabb_cache: HashMap::new(),
materials_cache: HashMap::new(), materials_cache: HashMap::new(),
save_component_filter: self.save_component_filter.clone(),
save_resource_filter: self.save_resource_filter.clone()
}); });
} }
} }