Compare commits
6 Commits
5e73d68718
...
d0dad53253
Author | SHA1 | Date | |
---|---|---|---|
|
d0dad53253 | ||
|
67756dd2cd | ||
|
b00ef0a9a8 | ||
|
d08c235122 | ||
|
171ec7490a | ||
|
ae9f07f549 |
@ -10,6 +10,9 @@ pub use registry::*;
|
|||||||
pub mod blueprints;
|
pub mod blueprints;
|
||||||
pub use blueprints::*;
|
pub use blueprints::*;
|
||||||
|
|
||||||
|
pub mod save_load;
|
||||||
|
pub use save_load::*;
|
||||||
|
|
||||||
#[derive(Clone, Resource)]
|
#[derive(Clone, Resource)]
|
||||||
pub struct BlenvyConfig {
|
pub struct BlenvyConfig {
|
||||||
// registry
|
// registry
|
||||||
@ -26,6 +29,8 @@ pub struct BlenvyConfig {
|
|||||||
// save & load
|
// save & load
|
||||||
pub(crate) save_component_filter: SceneFilter,
|
pub(crate) save_component_filter: SceneFilter,
|
||||||
pub(crate) save_resource_filter: SceneFilter,
|
pub(crate) save_resource_filter: SceneFilter,
|
||||||
|
//pub(crate) save_path: PathBuf,
|
||||||
|
// save_path: PathBuf::from("saves"),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -63,6 +68,7 @@ impl Plugin for BlenvyPlugin {
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
ExportRegistryPlugin::default(),
|
ExportRegistryPlugin::default(),
|
||||||
BlueprintsPlugin::default(),
|
BlueprintsPlugin::default(),
|
||||||
|
SaveLoadPlugin::default()
|
||||||
))
|
))
|
||||||
.insert_resource(BlenvyConfig {
|
.insert_resource(BlenvyConfig {
|
||||||
export_registry: self.export_registry,
|
export_registry: self.export_registry,
|
||||||
|
@ -1,108 +1,59 @@
|
|||||||
pub mod saveable;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
pub use saveable::*;
|
|
||||||
|
|
||||||
pub mod saving;
|
|
||||||
pub use saving::*;
|
|
||||||
|
|
||||||
pub mod loading;
|
|
||||||
pub use loading::*;
|
|
||||||
|
|
||||||
use bevy::core_pipeline::core_3d::{Camera3dDepthTextureUsage, ScreenSpaceTransmissionQuality};
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::prelude::{App, IntoSystemConfigs, Plugin};
|
|
||||||
use blenvy::GltfBlueprintsSet;
|
|
||||||
|
|
||||||
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
|
#[derive(Component, Reflect, Debug, Default)]
|
||||||
pub enum SavingSet {
|
#[reflect(Component)]
|
||||||
Save,
|
/// component used to mark any entity as Dynamic: aka add this to make sure your entity is going to be saved
|
||||||
}
|
pub struct Dynamic;
|
||||||
|
|
||||||
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
|
#[derive(Component, Reflect, Debug, Default)]
|
||||||
pub enum LoadingSet {
|
#[reflect(Component)]
|
||||||
Load,
|
/// marker component for entities that do not have parents, or whose parents should be ignored when serializing
|
||||||
}
|
pub(crate) struct RootEntity;
|
||||||
|
|
||||||
// Plugin configuration
|
#[derive(Component, Debug)]
|
||||||
|
/// internal helper component to store parents before resetting them
|
||||||
|
pub(crate) struct OriginalParent(pub(crate) Entity);
|
||||||
|
|
||||||
#[derive(Clone, Resource)]
|
|
||||||
pub struct SaveLoadConfig {
|
|
||||||
pub(crate) save_path: PathBuf,
|
|
||||||
pub(crate) component_filter: SceneFilter,
|
|
||||||
pub(crate) resource_filter: SceneFilter,
|
|
||||||
}
|
|
||||||
|
|
||||||
// define the plugin
|
|
||||||
|
|
||||||
pub struct SaveLoadPlugin {
|
|
||||||
pub component_filter: SceneFilter,
|
|
||||||
pub resource_filter: SceneFilter,
|
|
||||||
pub save_path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SaveLoadPlugin {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
component_filter: SceneFilter::default(),
|
|
||||||
resource_filter: SceneFilter::default(),
|
|
||||||
save_path: PathBuf::from("scenes"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/// Marker component to Flag the root entity of all static entities (immutables)
|
||||||
#[derive(Component, Reflect, Debug, Default)]
|
#[derive(Component, Reflect, Debug, Default)]
|
||||||
#[reflect(Component)]
|
#[reflect(Component)]
|
||||||
pub struct StaticEntitiesRoot;
|
pub struct StaticEntitiesRoot;
|
||||||
|
|
||||||
|
/// Marker component to Flag the root entity of all dynamic entities (mutables)
|
||||||
#[derive(Component, Reflect, Debug, Default)]
|
#[derive(Component, Reflect, Debug, Default)]
|
||||||
#[reflect(Component)]
|
#[reflect(Component)]
|
||||||
pub struct DynamicEntitiesRoot;
|
pub struct DynamicEntitiesRoot;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Resource, Clone, Debug, Default, Reflect)]
|
||||||
|
#[reflect(Resource)]
|
||||||
|
pub struct StaticEntitiesBlueprintInfo {
|
||||||
|
//pub blueprint_info: BlueprintInfo,
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub mod saving;
|
||||||
|
pub use saving::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
/// Plugin for saving & loading
|
||||||
|
pub struct SaveLoadPlugin {}
|
||||||
|
|
||||||
impl Plugin for SaveLoadPlugin {
|
impl Plugin for SaveLoadPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.register_type::<Dynamic>()
|
app.register_type::<Dynamic>()
|
||||||
.register_type::<StaticEntitiesRoot>()
|
.register_type::<StaticEntitiesRoot>()
|
||||||
// TODO: remove these in bevy 0.13, as these are now registered by default
|
|
||||||
.register_type::<Camera3dDepthTextureUsage>()
|
|
||||||
.register_type::<ScreenSpaceTransmissionQuality>()
|
|
||||||
.register_type::<StaticEntitiesStorage>()
|
|
||||||
.add_event::<SaveRequest>()
|
|
||||||
.add_event::<LoadRequest>()
|
|
||||||
.add_event::<LoadingFinished>()
|
|
||||||
.add_event::<SavingFinished>()
|
|
||||||
.insert_resource(SaveLoadConfig {
|
|
||||||
save_path: self.save_path.clone(),
|
|
||||||
|
|
||||||
component_filter: self.component_filter.clone(),
|
.add_event::<SaveRequest>()
|
||||||
resource_filter: self.resource_filter.clone(),
|
.add_event::<SaveFinished>()
|
||||||
})
|
|
||||||
.configure_sets(
|
|
||||||
Update,
|
|
||||||
(LoadingSet::Load).chain().before(GltfBlueprintsSet::Spawn), //.before(GltfComponentsSet::Injection)
|
|
||||||
)
|
|
||||||
.add_systems(
|
.add_systems(
|
||||||
PreUpdate,
|
Update,
|
||||||
(prepare_save_game, apply_deferred, save_game, cleanup_save)
|
(prepare_save_game, apply_deferred, save_game, cleanup_save)
|
||||||
.chain()
|
.chain()
|
||||||
.run_if(should_save),
|
.run_if(should_save),
|
||||||
)
|
)
|
||||||
.add_systems(Update, mark_load_requested)
|
;
|
||||||
.add_systems(
|
|
||||||
Update,
|
|
||||||
(unload_world, apply_deferred, load_game)
|
|
||||||
.chain()
|
|
||||||
.run_if(resource_exists::<LoadRequested>)
|
|
||||||
.run_if(not(resource_exists::<LoadFirstStageDone>))
|
|
||||||
.in_set(LoadingSet::Load),
|
|
||||||
)
|
|
||||||
.add_systems(
|
|
||||||
Update,
|
|
||||||
(load_static, apply_deferred, cleanup_loaded_scene)
|
|
||||||
.chain()
|
|
||||||
.run_if(resource_exists::<LoadFirstStageDone>)
|
|
||||||
// .run_if(in_state(AppState::LoadingGame))
|
|
||||||
.in_set(LoadingSet::Load),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
76
crates/blenvy/src/save_load/old/mod_old.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
pub mod saveable;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub use saveable::*;
|
||||||
|
|
||||||
|
pub mod saving;
|
||||||
|
pub use saving::*;
|
||||||
|
|
||||||
|
pub mod loading;
|
||||||
|
pub use loading::*;
|
||||||
|
|
||||||
|
use bevy::core_pipeline::core_3d::{Camera3dDepthTextureUsage, ScreenSpaceTransmissionQuality};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy::prelude::{App, IntoSystemConfigs, Plugin};
|
||||||
|
use blenvy::GltfBlueprintsSet;
|
||||||
|
|
||||||
|
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
|
||||||
|
pub enum SavingSet {
|
||||||
|
Save,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
|
||||||
|
pub enum LoadingSet {
|
||||||
|
Load,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Component, Reflect, Debug, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct StaticEntitiesRoot;
|
||||||
|
|
||||||
|
#[derive(Component, Reflect, Debug, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct DynamicEntitiesRoot;
|
||||||
|
|
||||||
|
impl Plugin for SaveLoadPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.register_type::<Dynamic>()
|
||||||
|
.register_type::<StaticEntitiesRoot>()
|
||||||
|
// TODO: remove these in bevy 0.13, as these are now registered by default
|
||||||
|
.register_type::<Camera3dDepthTextureUsage>()
|
||||||
|
.register_type::<ScreenSpaceTransmissionQuality>()
|
||||||
|
.register_type::<StaticEntitiesStorage>()
|
||||||
|
.add_event::<SaveRequest>()
|
||||||
|
.add_event::<LoadRequest>()
|
||||||
|
.add_event::<LoadingFinished>()
|
||||||
|
.add_event::<SavingFinished>()
|
||||||
|
.configure_sets(
|
||||||
|
Update,
|
||||||
|
(LoadingSet::Load).chain().before(GltfBlueprintsSet::Spawn), //.before(GltfComponentsSet::Injection)
|
||||||
|
)
|
||||||
|
.add_systems(
|
||||||
|
PreUpdate,
|
||||||
|
(prepare_save_game, apply_deferred, save_game, cleanup_save)
|
||||||
|
.chain()
|
||||||
|
.run_if(should_save),
|
||||||
|
)
|
||||||
|
.add_systems(Update, mark_load_requested)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(unload_world, apply_deferred, load_game)
|
||||||
|
.chain()
|
||||||
|
.run_if(resource_exists::<LoadRequested>)
|
||||||
|
.run_if(not(resource_exists::<LoadFirstStageDone>))
|
||||||
|
.in_set(LoadingSet::Load),
|
||||||
|
)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(load_static, apply_deferred, cleanup_loaded_scene)
|
||||||
|
.chain()
|
||||||
|
.run_if(resource_exists::<LoadFirstStageDone>)
|
||||||
|
// .run_if(in_state(AppState::LoadingGame))
|
||||||
|
.in_set(LoadingSet::Load),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
196
crates/blenvy/src/save_load/old/saving.rs
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy::tasks::IoTaskPool;
|
||||||
|
use blenvy::{BlueprintName, InBlueprint, Library, SpawnHere};
|
||||||
|
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::{DynamicEntitiesRoot, SaveLoadConfig, StaticEntitiesRoot};
|
||||||
|
|
||||||
|
#[derive(Event, Debug)]
|
||||||
|
pub struct SaveRequest {
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Event)]
|
||||||
|
pub struct SavingFinished;
|
||||||
|
|
||||||
|
pub fn should_save(save_requests: EventReader<SaveRequest>) -> bool {
|
||||||
|
!save_requests.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Clone, Debug, Default, Reflect)]
|
||||||
|
#[reflect(Resource)]
|
||||||
|
pub struct StaticEntitiesStorage {
|
||||||
|
pub name: String,
|
||||||
|
pub library_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Reflect, Debug, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
/// marker component for entities that do not have parents, or whose parents should be ignored when serializing
|
||||||
|
pub(crate) struct RootEntity;
|
||||||
|
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
/// internal helper component to store parents before resetting them
|
||||||
|
pub(crate) struct OriginalParent(pub(crate) Entity);
|
||||||
|
|
||||||
|
// any child of dynamic/ saveable entities that is not saveable itself should be removed from the list of children
|
||||||
|
pub(crate) fn prepare_save_game(
|
||||||
|
saveables: Query<Entity, (With<Dynamic>, With<BlueprintName>)>,
|
||||||
|
root_entities: Query<Entity, Or<(With<DynamicEntitiesRoot>, Without<Parent>)>>, // With<DynamicEntitiesRoot>
|
||||||
|
dynamic_entities: Query<(Entity, &Parent, Option<&Children>), With<Dynamic>>,
|
||||||
|
static_entities: Query<(Entity, &BlueprintName, Option<&Library>), With<StaticEntitiesRoot>>,
|
||||||
|
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
for entity in saveables.iter() {
|
||||||
|
commands.entity(entity).insert(SpawnHere);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (entity, parent, children) in dynamic_entities.iter() {
|
||||||
|
let parent = parent.get();
|
||||||
|
if root_entities.contains(parent) {
|
||||||
|
commands.entity(entity).insert(RootEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(children) = children {
|
||||||
|
for sub_child in children.iter() {
|
||||||
|
if !dynamic_entities.contains(*sub_child) {
|
||||||
|
commands.entity(*sub_child).insert(OriginalParent(entity));
|
||||||
|
commands.entity(entity).remove_children(&[*sub_child]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (_, blueprint_name, library) in static_entities.iter() {
|
||||||
|
let library_path: String = library.map_or_else(|| "", |l| l.0.to_str().unwrap()).into();
|
||||||
|
commands.insert_resource(StaticEntitiesStorage {
|
||||||
|
name: blueprint_name.0.clone(),
|
||||||
|
library_path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn save_game(world: &mut World) {
|
||||||
|
info!("saving");
|
||||||
|
|
||||||
|
let mut save_path: String = "".into();
|
||||||
|
let mut events = world.resource_mut::<Events<SaveRequest>>();
|
||||||
|
|
||||||
|
for event in events.get_reader().read(&events) {
|
||||||
|
info!("SAVE EVENT !! {:?}", event);
|
||||||
|
save_path.clone_from(&event.path);
|
||||||
|
}
|
||||||
|
events.clear();
|
||||||
|
|
||||||
|
let saveable_entities: Vec<Entity> = world
|
||||||
|
.query_filtered::<Entity, (With<Dynamic>, Without<InBlueprint>, Without<RootEntity>)>()
|
||||||
|
.iter(world)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let saveable_root_entities: Vec<Entity> = world
|
||||||
|
.query_filtered::<Entity, (With<Dynamic>, Without<InBlueprint>, With<RootEntity>)>()
|
||||||
|
.iter(world)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
info!("saveable entities {}", saveable_entities.len());
|
||||||
|
info!("saveable root entities {}", saveable_root_entities.len());
|
||||||
|
|
||||||
|
let save_load_config = world
|
||||||
|
.get_resource::<SaveLoadConfig>()
|
||||||
|
.expect("SaveLoadConfig should exist at this stage");
|
||||||
|
|
||||||
|
// we hardcode some of the always allowed types
|
||||||
|
let filter = save_load_config
|
||||||
|
.component_filter
|
||||||
|
.clone()
|
||||||
|
.allow::<Parent>()
|
||||||
|
.allow::<Children>()
|
||||||
|
.allow::<BlueprintName>()
|
||||||
|
.allow::<SpawnHere>()
|
||||||
|
.allow::<Dynamic>()
|
||||||
|
|
||||||
|
|
||||||
|
;
|
||||||
|
|
||||||
|
// for root entities, it is the same EXCEPT we make sure parents are not included
|
||||||
|
let filter_root = filter.clone().deny::<Parent>();
|
||||||
|
|
||||||
|
let filter_resources = save_load_config
|
||||||
|
.resource_filter
|
||||||
|
.clone()
|
||||||
|
.allow::<StaticEntitiesStorage>()
|
||||||
|
;
|
||||||
|
|
||||||
|
// for default stuff
|
||||||
|
let scene_builder = DynamicSceneBuilder::from_world(world)
|
||||||
|
.with_filter(filter.clone())
|
||||||
|
.with_resource_filter(filter_resources.clone());
|
||||||
|
|
||||||
|
let mut dyn_scene = scene_builder
|
||||||
|
.extract_resources()
|
||||||
|
.extract_entities(saveable_entities.clone().into_iter())
|
||||||
|
.remove_empty_entities()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// for root entities
|
||||||
|
let scene_builder_root = DynamicSceneBuilder::from_world(world)
|
||||||
|
.with_filter(filter_root.clone())
|
||||||
|
.with_resource_filter(filter_resources.clone());
|
||||||
|
|
||||||
|
// FIXME : add back
|
||||||
|
let mut dyn_scene_root = scene_builder_root
|
||||||
|
.extract_resources()
|
||||||
|
.extract_entities(
|
||||||
|
saveable_root_entities.clone().into_iter(), // .chain(static_world_markers.into_iter()),
|
||||||
|
)
|
||||||
|
.remove_empty_entities()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
dyn_scene.entities.append(&mut dyn_scene_root.entities);
|
||||||
|
// dyn_scene.resources.append(&mut dyn_scene_root.resources);
|
||||||
|
|
||||||
|
let serialized_scene = dyn_scene
|
||||||
|
.serialize(&world.resource::<AppTypeRegistry>().read())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let save_path = Path::new("assets")
|
||||||
|
.join(&save_load_config.save_path)
|
||||||
|
.join(Path::new(save_path.as_str())); // Path::new(&save_load_config.save_path).join(Path::new(save_path.as_str()));
|
||||||
|
info!("saving game to {:?}", save_path);
|
||||||
|
|
||||||
|
// world.send_event(SavingFinished);
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
IoTaskPool::get()
|
||||||
|
.spawn(async move {
|
||||||
|
// Write the scene RON data to file
|
||||||
|
File::create(save_path)
|
||||||
|
.and_then(|mut file| file.write(serialized_scene.as_bytes()))
|
||||||
|
.expect("Error while writing save to file");
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn cleanup_save(
|
||||||
|
needs_parent_reset: Query<(Entity, &OriginalParent)>,
|
||||||
|
mut saving_finished: EventWriter<SavingFinished>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
for (entity, original_parent) in needs_parent_reset.iter() {
|
||||||
|
commands.entity(original_parent.0).add_child(entity);
|
||||||
|
}
|
||||||
|
commands.remove_resource::<StaticEntitiesStorage>();
|
||||||
|
saving_finished.send(SavingFinished);
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
pub(crate) fn cleanup_save(mut world: &mut World) {
|
||||||
|
|
||||||
|
let mut query = world.query::<(Entity, &OriginalParent)>();
|
||||||
|
for (mut entity, original_parent) in query.iter_mut(&mut world) {
|
||||||
|
let e = world.entity_mut(original_parent.0);
|
||||||
|
// .add_child(entity);
|
||||||
|
}
|
||||||
|
}*/
|
@ -1,7 +0,0 @@
|
|||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Component, Reflect, Debug, Default)]
|
|
||||||
#[reflect(Component)]
|
|
||||||
/// component used to mark any entity as Dynamic: aka add this to make sure your entity is going to be saved
|
|
||||||
pub struct Dynamic(pub bool);
|
|
||||||
|
|
@ -1,55 +1,42 @@
|
|||||||
use bevy::prelude::*;
|
|
||||||
use bevy::tasks::IoTaskPool;
|
|
||||||
use blenvy::{BlueprintName, InBlueprint, Library, SpawnHere};
|
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::{Dynamic, DynamicEntitiesRoot, SaveLoadConfig, StaticEntitiesRoot};
|
use bevy::render::camera::{CameraMainTextureUsages, CameraRenderGraph};
|
||||||
|
use bevy::{prelude::*, tasks::IoTaskPool};
|
||||||
|
use bevy::prelude::World;
|
||||||
|
|
||||||
|
use crate::{BlenvyConfig, BlueprintInfo, Dynamic, FromBlueprint, RootEntity, SpawnBlueprint};
|
||||||
|
|
||||||
|
use super::{DynamicEntitiesRoot, OriginalParent, StaticEntitiesRoot};
|
||||||
|
|
||||||
#[derive(Event, Debug)]
|
#[derive(Event, Debug)]
|
||||||
pub struct SaveRequest {
|
pub struct SaveRequest {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Event)]
|
#[derive(Event)]
|
||||||
pub struct SavingFinished;
|
pub struct SaveFinished; // TODO: merge the the events above
|
||||||
|
|
||||||
pub fn should_save(save_requests: EventReader<SaveRequest>) -> bool {
|
pub fn should_save(save_requests: EventReader<SaveRequest>) -> bool {
|
||||||
!save_requests.is_empty()
|
!save_requests.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource, Clone, Debug, Default, Reflect)]
|
|
||||||
#[reflect(Resource)]
|
|
||||||
pub struct StaticEntitiesStorage {
|
|
||||||
pub name: String,
|
|
||||||
pub library_path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Component, Reflect, Debug, Default)]
|
|
||||||
#[reflect(Component)]
|
|
||||||
/// marker component for entities that do not have parents, or whose parents should be ignored when serializing
|
|
||||||
pub(crate) struct RootEntity;
|
|
||||||
|
|
||||||
#[derive(Component, Debug)]
|
|
||||||
/// internal helper component to store parents before resetting them
|
|
||||||
pub(crate) struct OriginalParent(pub(crate) Entity);
|
|
||||||
|
|
||||||
// any child of dynamic/ saveable entities that is not saveable itself should be removed from the list of children
|
// any child of dynamic/ saveable entities that is not saveable itself should be removed from the list of children
|
||||||
pub(crate) fn prepare_save_game(
|
pub(crate) fn prepare_save_game(
|
||||||
saveables: Query<Entity, (With<Dynamic>, With<BlueprintName>)>,
|
saveables: Query<Entity, (With<Dynamic>, With<BlueprintInfo>)>,
|
||||||
root_entities: Query<Entity, Or<(With<DynamicEntitiesRoot>, Without<Parent>)>>, // With<DynamicEntitiesRoot>
|
root_entities: Query<Entity, Or<(With<DynamicEntitiesRoot>, Without<Parent>)>>, // With<DynamicEntitiesRoot>
|
||||||
dynamic_entities: Query<(Entity, &Parent, Option<&Children>), With<Dynamic>>,
|
dynamic_entities: Query<(Entity, &Parent, Option<&Children>), With<Dynamic>>,
|
||||||
static_entities: Query<(Entity, &BlueprintName, Option<&Library>), With<StaticEntitiesRoot>>,
|
static_entities: Query<(Entity, &BlueprintInfo), With<StaticEntitiesRoot>>,
|
||||||
|
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
) {
|
) {
|
||||||
for entity in saveables.iter() {
|
for entity in saveables.iter() {
|
||||||
commands.entity(entity).insert(SpawnHere);
|
commands.entity(entity).insert(SpawnBlueprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (entity, parent, children) in dynamic_entities.iter() {
|
for (entity, parent, children) in dynamic_entities.iter() {
|
||||||
|
println!("prepare save game");
|
||||||
let parent = parent.get();
|
let parent = parent.get();
|
||||||
if root_entities.contains(parent) {
|
if root_entities.contains(parent) {
|
||||||
commands.entity(entity).insert(RootEntity);
|
commands.entity(entity).insert(RootEntity);
|
||||||
@ -64,14 +51,17 @@ pub(crate) fn prepare_save_game(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (_, blueprint_name, library) in static_entities.iter() {
|
/*for (_, blueprint_name) in static_entities.iter() {
|
||||||
let library_path: String = library.map_or_else(|| "", |l| l.0.to_str().unwrap()).into();
|
let library_path: String = library.map_or_else(|| "", |l| l.0.to_str().unwrap()).into();
|
||||||
commands.insert_resource(StaticEntitiesStorage {
|
commands.insert_resource(StaticEntitiesStorage {
|
||||||
name: blueprint_name.0.clone(),
|
name: blueprint_name.0.clone(),
|
||||||
library_path,
|
library_path,
|
||||||
});
|
});
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub(crate) fn save_game(world: &mut World) {
|
pub(crate) fn save_game(world: &mut World) {
|
||||||
info!("saving");
|
info!("saving");
|
||||||
|
|
||||||
@ -85,39 +75,49 @@ pub(crate) fn save_game(world: &mut World) {
|
|||||||
events.clear();
|
events.clear();
|
||||||
|
|
||||||
let saveable_entities: Vec<Entity> = world
|
let saveable_entities: Vec<Entity> = world
|
||||||
.query_filtered::<Entity, (With<Dynamic>, Without<InBlueprint>, Without<RootEntity>)>()
|
// .query_filtered::<Entity, (With<Dynamic>, Without<FromBlueprint>, Without<RootEntity>)>()
|
||||||
|
.query_filtered::<Entity, (With<Dynamic>, Without<RootEntity>)>()
|
||||||
.iter(world)
|
.iter(world)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let saveable_root_entities: Vec<Entity> = world
|
let saveable_root_entities: Vec<Entity> = world
|
||||||
.query_filtered::<Entity, (With<Dynamic>, Without<InBlueprint>, With<RootEntity>)>()
|
.query_filtered::<Entity, (With<Dynamic>, With<RootEntity>)>()
|
||||||
|
//.query_filtered::<Entity, (With<Dynamic>, Without<FromBlueprint>, With<RootEntity>)>()
|
||||||
.iter(world)
|
.iter(world)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
info!("saveable entities {}", saveable_entities.len());
|
info!("saveable entities {}", saveable_entities.len());
|
||||||
info!("saveable root entities {}", saveable_root_entities.len());
|
info!("saveable root entities {}", saveable_root_entities.len());
|
||||||
|
|
||||||
let save_load_config = world
|
let config = world
|
||||||
.get_resource::<SaveLoadConfig>()
|
.get_resource::<BlenvyConfig>()
|
||||||
.expect("SaveLoadConfig should exist at this stage");
|
.expect("Blenvy configuration should exist at this stage");
|
||||||
|
|
||||||
// we hardcode some of the always allowed types
|
// we hardcode some of the always allowed types
|
||||||
let filter = save_load_config
|
let filter = config
|
||||||
.component_filter
|
.save_component_filter
|
||||||
.clone()
|
.clone()
|
||||||
.allow::<Parent>()
|
.allow::<Parent>()
|
||||||
.allow::<Children>()
|
.allow::<Children>()
|
||||||
.allow::<BlueprintName>()
|
.allow::<BlueprintInfo>()
|
||||||
.allow::<SpawnHere>()
|
.allow::<SpawnBlueprint>()
|
||||||
.allow::<Dynamic>();
|
.allow::<Dynamic>()
|
||||||
|
|
||||||
|
/*.deny::<CameraRenderGraph>()
|
||||||
|
.deny::<CameraMainTextureUsages>()
|
||||||
|
.deny::<Handle<Mesh>>()
|
||||||
|
.deny::<Handle<StandardMaterial>>() */
|
||||||
|
;
|
||||||
|
|
||||||
// for root entities, it is the same EXCEPT we make sure parents are not included
|
// for root entities, it is the same EXCEPT we make sure parents are not included
|
||||||
let filter_root = filter.clone().deny::<Parent>();
|
let filter_root = filter.clone().deny::<Parent>();
|
||||||
|
|
||||||
let filter_resources = save_load_config
|
let filter_resources = config.clone()
|
||||||
.resource_filter
|
.save_resource_filter
|
||||||
.clone()
|
.deny::<Time<Real>>()
|
||||||
.allow::<StaticEntitiesStorage>();
|
|
||||||
|
.clone();
|
||||||
|
//.allow::<StaticEntitiesStorage>();
|
||||||
|
|
||||||
// for default stuff
|
// for default stuff
|
||||||
let scene_builder = DynamicSceneBuilder::from_world(world)
|
let scene_builder = DynamicSceneBuilder::from_world(world)
|
||||||
@ -143,15 +143,15 @@ pub(crate) fn save_game(world: &mut World) {
|
|||||||
.remove_empty_entities()
|
.remove_empty_entities()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
dyn_scene.entities.append(&mut dyn_scene_root.entities);
|
// dyn_scene.entities.append(&mut dyn_scene_root.entities);
|
||||||
// dyn_scene.resources.append(&mut dyn_scene_root.resources);
|
// dyn_scene.resources.append(&mut dyn_scene_root.resources);
|
||||||
|
|
||||||
let serialized_scene = dyn_scene
|
let serialized_scene = dyn_scene
|
||||||
.serialize(&world.resource::<AppTypeRegistry>().read())
|
.serialize(&world.resource::<AppTypeRegistry>().read())
|
||||||
.unwrap();
|
.expect("filtered scene should serialize correctly");
|
||||||
|
|
||||||
let save_path = Path::new("assets")
|
let save_path = Path::new("assets")
|
||||||
.join(&save_load_config.save_path)
|
//.join(&config.save_path)
|
||||||
.join(Path::new(save_path.as_str())); // Path::new(&save_load_config.save_path).join(Path::new(save_path.as_str()));
|
.join(Path::new(save_path.as_str())); // Path::new(&save_load_config.save_path).join(Path::new(save_path.as_str()));
|
||||||
info!("saving game to {:?}", save_path);
|
info!("saving game to {:?}", save_path);
|
||||||
|
|
||||||
@ -170,21 +170,12 @@ pub(crate) fn save_game(world: &mut World) {
|
|||||||
|
|
||||||
pub(crate) fn cleanup_save(
|
pub(crate) fn cleanup_save(
|
||||||
needs_parent_reset: Query<(Entity, &OriginalParent)>,
|
needs_parent_reset: Query<(Entity, &OriginalParent)>,
|
||||||
mut saving_finished: EventWriter<SavingFinished>,
|
mut saving_finished: EventWriter<SaveFinished>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
) {
|
) {
|
||||||
for (entity, original_parent) in needs_parent_reset.iter() {
|
for (entity, original_parent) in needs_parent_reset.iter() {
|
||||||
commands.entity(original_parent.0).add_child(entity);
|
commands.entity(original_parent.0).add_child(entity);
|
||||||
}
|
}
|
||||||
commands.remove_resource::<StaticEntitiesStorage>();
|
// commands.remove_resource::<StaticEntitiesStorage>();
|
||||||
saving_finished.send(SavingFinished);
|
saving_finished.send(SaveFinished);
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
pub(crate) fn cleanup_save(mut world: &mut World) {
|
|
||||||
|
|
||||||
let mut query = world.query::<(Entity, &OriginalParent)>();
|
|
||||||
for (mut entity, original_parent) in query.iter_mut(&mut world) {
|
|
||||||
let e = world.entity_mut(original_parent.0);
|
|
||||||
// .add_child(entity);
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
Before Width: | Height: | Size: 459 KiB After Width: | Height: | Size: 459 KiB |
Before Width: | Height: | Size: 362 KiB After Width: | Height: | Size: 1.2 MiB |
BIN
docs/avian/img/direct_in_world.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
BIN
docs/avian/img/falling_concave.gif
Normal file
After Width: | Height: | Size: 2.5 MiB |
BIN
docs/avian/img/falling_convex.gif
Normal file
After Width: | Height: | Size: 2.8 MiB |
BIN
docs/avian/img/falling_direct.gif
Normal file
After Width: | Height: | Size: 96 KiB |
BIN
docs/avian/img/falling_direct_on_static.gif
Normal file
After Width: | Height: | Size: 102 KiB |
Before Width: | Height: | Size: 2.5 MiB |
BIN
docs/avian/img/falling_empty.gif
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
docs/avian/img/falling_wireframe.gif
Normal file
After Width: | Height: | Size: 1.9 MiB |
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 317 KiB |
Before Width: | Height: | Size: 242 KiB After Width: | Height: | Size: 149 KiB |
@ -3,6 +3,20 @@
|
|||||||
This guide assumes that you have a basic Blenvy setup ready to tinker in.
|
This guide assumes that you have a basic Blenvy setup ready to tinker in.
|
||||||
If you don't have that yet, please refer to the [quickstart](../quickstart/readme.md) guide.
|
If you don't have that yet, please refer to the [quickstart](../quickstart/readme.md) guide.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Add Avian to Bevy](#add-avian-to-bevy)
|
||||||
|
- [Prepare your Scenes](#prepare-your-scenes)
|
||||||
|
- [Create a Rigid Body](#create-a-rigid-body)
|
||||||
|
- [Add Primitive Colliders](#add-primitive-colliders)
|
||||||
|
- [Direct](#direct)
|
||||||
|
- [With Empty](#with-empty)
|
||||||
|
- [Wireframes](#wireframes)
|
||||||
|
- [Add Dynamic Colliders](#add-dynamic-colliders)
|
||||||
|
- [Convex](#convex)
|
||||||
|
- [Concave](#concave)
|
||||||
|
- [Other useful components](#other-useful-components)
|
||||||
|
|
||||||
## Add Avian to Bevy
|
## Add Avian to Bevy
|
||||||
|
|
||||||
No big surprises here. Simply add `avian3d` as a dependency by running the following from your project root:
|
No big surprises here. Simply add `avian3d` as a dependency by running the following from your project root:
|
||||||
@ -42,76 +56,41 @@ fn setup(mut commands: Commands) {
|
|||||||
|
|
||||||
Run this once with `cargo.run` to generate a `registry.json` that contains the Avian components.
|
Run this once with `cargo.run` to generate a `registry.json` that contains the Avian components.
|
||||||
|
|
||||||
## Prepare the Blueprints
|
## Prepare your Scenes
|
||||||
|
|
||||||
Set up your `World` and `Library` scenes in Blender. Switch to the `Library` scene.
|
Set up your `World` and `Library` scenes in Blender.
|
||||||
|
|
||||||
|
Go into your `World` scene. If you are coming from the [quickstart guide](../quickstart/readme.md), you can remove the `Player` instance as we don't need it in this guide.
|
||||||
|
If you have created this scene yourself in advance, make sure that it contains a camera, a light, and some kind of ground.
|
||||||
|
|
||||||
|
Since the objects are quite big, you may need to move the camera a bit further away to see them all.
|
||||||
|
We set its Y location to `-15` and the X rotation to `90` for this reason.
|
||||||
|
Pressing `0` on your numpad will show you a preview of what the camera sees.
|
||||||
|
|
||||||
|
For reference, this is how our world setup looks:
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>The world setup before adding any physics</summary>
|
||||||
|
<img src="img/empty_world.png" width = 100%/>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
Now switch to the `Library` scene.
|
||||||
If you're coming from the [quickstart](../quickstart/readme.md) guide, you may now delete the `Player` collection by
|
If you're coming from the [quickstart](../quickstart/readme.md) guide, you may now delete the `Player` collection by
|
||||||
right-clicking it in the outliner and selecting `Delete Hierarchy` as we don't need it in this guide.
|
right-clicking it in the outliner and selecting `Delete Hierarchy`.
|
||||||
Remember, you can find the outliner all the way to the right.
|
Remember, you can find the outliner all the way to the right.
|
||||||
|
|
||||||
We will be showing different ways to add colliders, so we need to add a blueprint for each approach.
|
## Create a Rigid Body
|
||||||
Create three new collections in the outliner by doing `rightclick` -> `New Collection` and name them as follows:
|
|
||||||
|
|
||||||
- Cube
|
Create a new collection with `rightclick` -> `New Collection` and name it `Direct`. This name will make sense in the next section.
|
||||||
- Board
|
|
||||||
- Cylinder
|
|
||||||
|
|
||||||
Your outliner should now look like this:
|
Click on the `Direct` collection we just created to select it. Then, go to `Add` -> `Mesh` -> `Cube` in the upper left corner to add a cube to the collection. Leave it at the default transform.
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Our empty collections</summary>
|
|
||||||
<img src="img/empty_collections.png" width = 50%/>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
If you accidentally created a collection as a child of another, simply drag-and-drop them around to reorder them until they look like the image above.
|
|
||||||
|
|
||||||
### Cube
|
|
||||||
|
|
||||||
Click on the `Cube` collection we just created to select it. Then, go to `Add` -> `Mesh` -> `Cube` in the upper left corner to add a cube to the collection. Leave it at the default transform.
|
|
||||||
|
|
||||||
### Board
|
|
||||||
|
|
||||||
Click on the `Board` collection. Again, go to `Add` -> `Mesh` -> `Cube`. This time, scale it until it looks like a flat board:
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>The cylinder in Blender</summary>
|
|
||||||
<img src="img/cylinder.png" width = 50%/>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
> [!TIP]
|
|
||||||
> The above screenshot was made after disabling the visibility of the `Cube` collection by clicking the eye icon in the outliner.
|
|
||||||
>
|
|
||||||
> <details>
|
|
||||||
> <summary>Hiding objects</summary>
|
|
||||||
> <img src="img/hiding.png" width = 50%/>
|
|
||||||
> </details>
|
|
||||||
>
|
|
||||||
> Hiding other collections becomes quickly essential when working with blueprints.
|
|
||||||
|
|
||||||
The scaling we used was the following:
|
|
||||||
|
|
||||||
- X: `2.5`
|
|
||||||
- Y: `0.5`
|
|
||||||
- Z: `1.5`
|
|
||||||
|
|
||||||
### Cylinder
|
|
||||||
|
|
||||||
Finally, click on the `Cylinder` collection. Go to `Add` -> `Mesh` -> `Cylinder`. Leave it at the default transform.
|
|
||||||
|
|
||||||
You should now have three collections with different shapes in them:
|
|
||||||
<details>
|
|
||||||
<summary>Collections with objects in the outliner</summary>
|
|
||||||
<img src="img/three_object_collection.png" width = 50%/>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Add RigidBody Components
|
|
||||||
|
|
||||||
Avian makes a distinction between a *rigid body* and its associated *colliders*.
|
Avian makes a distinction between a *rigid body* and its associated *colliders*.
|
||||||
In general, the best practice is to have a parent object be a rigid body and then have at least one descendant object be a collider.
|
In general, the best practice is to have a parent object be a rigid body and then have at least one descendant object be a collider.
|
||||||
|
|
||||||
Adding the `RigidBody` is the same for all approaches:
|
Add the `RigidBody` as follows:
|
||||||
|
|
||||||
- select the object in the viewport
|
- select the object in the viewport, i.e. the cube.
|
||||||
- go to the Blenvy menu's component manager. Remember, if are missing the side menu, you can open it with `N`.
|
- go to the Blenvy menu's component manager. Remember, if are missing the side menu, you can open it with `N`.
|
||||||
- type `rigidbody` in the search bar
|
- type `rigidbody` in the search bar
|
||||||
- select `avian3d::dynamics::rigid_body::RigidBody`
|
- select `avian3d::dynamics::rigid_body::RigidBody`
|
||||||
@ -129,7 +108,7 @@ The result should look like this:
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
The default value for `RigidBody` is `Dynamic`, which is what we want for all three objects.
|
The default value for `RigidBody` is `Dynamic`, which is what we want for all three objects.
|
||||||
It means that they will be affected by gravity and other forces. Repeat this step for the `Board` and `Cylinder` objects.
|
It means that they will be affected by gravity and other forces.
|
||||||
|
|
||||||
## Add Primitive Colliders
|
## Add Primitive Colliders
|
||||||
|
|
||||||
@ -137,9 +116,9 @@ Colliders come in two flavors: primitive and dynamic. Primitives are made up of
|
|||||||
|
|
||||||
There are three different ways to add primitive colliders to the objects, in order of increasing complexity.
|
There are three different ways to add primitive colliders to the objects, in order of increasing complexity.
|
||||||
|
|
||||||
### Quick and Dirty
|
### Direct
|
||||||
|
|
||||||
Select the cube and search in the components for `colliderconstructor`. Select `avian3d::collision::collider::constructor::ColliderConstructor` and add it.
|
Select the cube we just created and search in the components for `colliderconstructor`. Select `avian3d::collision::collider::constructor::ColliderConstructor` and add it.
|
||||||
By default, the collider will be of the variant `Sphere`. Change it to `Cuboid`.
|
By default, the collider will be of the variant `Sphere`. Change it to `Cuboid`.
|
||||||
Since the standard cube in Blender is of size 2 m, set the `x_length`, `y_length`, and `z_length` all to `2.0`:
|
Since the standard cube in Blender is of size 2 m, set the `x_length`, `y_length`, and `z_length` all to `2.0`:
|
||||||
<details>
|
<details>
|
||||||
@ -153,10 +132,83 @@ That's already it.
|
|||||||
> This method brings a major footgun: Blender uses Z-up coordinates, while Bevy uses Y-up coordinates.
|
> This method brings a major footgun: Blender uses Z-up coordinates, while Bevy uses Y-up coordinates.
|
||||||
> The information you enter into the `ColliderConstructor` is in Bevy's coordinate system, so don't mix them up!
|
> The information you enter into the `ColliderConstructor` is in Bevy's coordinate system, so don't mix them up!
|
||||||
|
|
||||||
### Using Empties
|
To see it in action, we switch to the `World` scene and add and instance of our `Direct` collection with `Add` -> `Collection Instance`.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>The world scene with the direct collider cube</summary>
|
||||||
|
<img src="img/direct_in_world.png" width = 50%/>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
Save the scene to let Blenvy export everything and run the game with `cargo run`.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>The cube falls down</summary>
|
||||||
|
<img src="img/falling_direct.gif" width = 50%/>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
If everything went right, your cube should fall into the void due to gravity.
|
||||||
|
Note that it phases right through the ground because we have not yet added a rigid body and collider to it yet.
|
||||||
|
|
||||||
|
Click on the ground and add a `RigidBody` component as described before to it, but this time set it to `Static`.
|
||||||
|
This means that the ground itself will not react to forces such as gravity, but will still affect other rigid bodies.
|
||||||
|
|
||||||
|
Add a collider to the ground as before. Make sure that the dimensions of the collider match the dimensions of the ground.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Ground collider</summary>
|
||||||
|
<img src="img/ground collider.png" width = 100%/>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> As mentioned before, when using this method you should be aware that the component
|
||||||
|
> is in Bevy's coordinate system, so set the `y_length` to the *height* of the ground.
|
||||||
|
|
||||||
|
Run your game again with `cargo run` to see the cube landing on the ground.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>The cube falls onto the ground</summary>
|
||||||
|
<img src="img/falling_direct_on_static.gif" width = 50%/>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> If your scene is doing something weird, try adding Avian's
|
||||||
|
> [`PhysicsDebugPlugin`](https://docs.rs/avian3d/latest/avian3d/debug_render/struct.PhysicsDebugPlugin.html)
|
||||||
|
> to your Bevy app to see the colliders at runtime.
|
||||||
|
> If the collider looks flipped, try switching the Y and Z lengths.
|
||||||
|
|
||||||
|
### With Empty
|
||||||
|
|
||||||
|
Go back to the `Library` scene. Add a collection named `With Empty`.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> If you accidentally created a collection as a child of another, simply drag-and-drop them around to reorder them
|
||||||
|
|
||||||
|
With the new collection selected, go to `Add` -> `Mesh` -> `Cube`. Name the new object `Board`.
|
||||||
|
This time, scale it until it looks like a flat board:
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>The board in Blender</summary>
|
||||||
|
<img src="img/board.png" width = 50%/>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> The above screenshot was made after disabling the visibility of the `Direct` collection by clicking the eye icon in the outliner.
|
||||||
|
>
|
||||||
|
> <details>
|
||||||
|
> <summary>Hiding objects</summary>
|
||||||
|
> <img src="img/hiding.png" width = 50%/>
|
||||||
|
> </details>
|
||||||
|
>
|
||||||
|
> Hiding other collections becomes quickly essential when working with blueprints.
|
||||||
|
|
||||||
|
The scaling we used was the following:
|
||||||
|
|
||||||
|
- X: `2.5`
|
||||||
|
- Y: `0.5`
|
||||||
|
- Z: `1.5`
|
||||||
|
|
||||||
You'll notice that the last variant does not actually show you a preview of the collider. Let's fix that.
|
You'll notice that the last variant does not actually show you a preview of the collider. Let's fix that.
|
||||||
Click on the `Board` and then select `Add` -> `Empty` -> `Cube`.
|
Click on the `With Empty` collection and then select `Add` -> `Empty` -> `Cube`.
|
||||||
To make its properties a bit nice to work with, go to the `Data` tab of the `Properties` window in the lower right:
|
To make its properties a bit nice to work with, go to the `Data` tab of the `Properties` window in the lower right:
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@ -171,7 +223,7 @@ You'll notice that it says "Size: 1m". This is a little bit misleading, as we've
|
|||||||
<img src="img/data.png" width = 50%/>
|
<img src="img/data.png" width = 50%/>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
Add a collider to this empty like you did in the ["Quick and Dirty" section](#quick-and-dirty).
|
Add a collider to this empty like you did in the ["Direct" section](#direct).
|
||||||
Set its lengths to `1` this time.
|
Set its lengths to `1` this time.
|
||||||
|
|
||||||
If you have only the `Empty` set to visible and selected it, your viewport should now look as follows:
|
If you have only the `Empty` set to visible and selected it, your viewport should now look as follows:
|
||||||
@ -235,7 +287,18 @@ You can just use these values as the scale for the `Empty`. After everything is
|
|||||||
|
|
||||||
Note that the orange collider outlines should align nicely with the board's mesh.
|
Note that the orange collider outlines should align nicely with the board's mesh.
|
||||||
|
|
||||||
### Using Wireframes
|
Add an instance of the `With Empty` collection to the `World` scene just as before and run the game.
|
||||||
|
You should now see both objects fall to the ground.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>The cube and board falling to the ground</summary>
|
||||||
|
<img src="img/falling_empty.gif" width = 50%/>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Wireframes
|
||||||
|
|
||||||
|
Add a new collection named `Wireframe`. With it selected,
|
||||||
|
go to `Add` -> `Mesh` -> `Cylinder`. Leave it at the default transform.
|
||||||
|
|
||||||
The last variant is a bit of a workaround for the fact that empties in Blender cannot have an arbitrary shape.
|
The last variant is a bit of a workaround for the fact that empties in Blender cannot have an arbitrary shape.
|
||||||
For example, a cylinder is not supported. So, we are going to create a new cylinder preview by hand.
|
For example, a cylinder is not supported. So, we are going to create a new cylinder preview by hand.
|
||||||
@ -289,46 +352,13 @@ The rest of the steps are identical to the empty: Drag-and-drop the cylinder col
|
|||||||
> You can use the builtin [Add Mesh Extra Objects](https://docs.blender.org/manual/en/latest/addons/add_mesh/mesh_extra_objects.html)
|
> You can use the builtin [Add Mesh Extra Objects](https://docs.blender.org/manual/en/latest/addons/add_mesh/mesh_extra_objects.html)
|
||||||
> extension to fill this gap.
|
> extension to fill this gap.
|
||||||
|
|
||||||
## Populate the world
|
Add an instance of the `Wireframe` collection to the `World` scene and run the game to see all kinds of primitive colliders tumble around.
|
||||||
|
|
||||||
Go into your `World` scene. If you are coming from the [quickstart guide](../quickstart/readme.md), you can remove the `Player` empty that is left over.
|
|
||||||
If you have created this scene yourself in advance, make sure that it contains a camera, a light, and some kind of ground.
|
|
||||||
For reference, this is how our world setup looks:
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>The world setup before adding any physics</summary>
|
<summary>Cylinder collider falling down</summary>
|
||||||
<img src="img/empty_world.png" width = 100%/>
|
<img src="img/falling_wireframe.gif" width = 50%/>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
Before we add any objects, we'll make the ground a rigid body as well. Add a `RigidBody` component as described before to it, but this time set it to `Static`. Add a collider to it in any of the ways described above. We used the `Quick and Dirty` method for this:
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Ground collider</summary>
|
|
||||||
<img src="img/ground collider.png" width = 100%/>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
> [!CAUTION]
|
|
||||||
> As mentioned before, when using this method you should be aware that the component
|
|
||||||
> is in Bevy's coordinate system, so set the `y_length` to the height of the ground.
|
|
||||||
|
|
||||||
Now add instances of the `Cube`, `Board`, and `Cylinder` to the world by selecting `Add` -> `Collection Instance`.
|
|
||||||
|
|
||||||
Since the objects are quite big, you may need to move the camera a bit further away to see them all.
|
|
||||||
We set its Y location to `-15` and the X rotation to `90` for this reason.
|
|
||||||
Pressing `0` on your numpad will show you a preview of what the camera sees.
|
|
||||||
|
|
||||||
Save the scene to let Blenvy export everything. Run your game with `cargo run` and you should see some objects falling onto the ground!
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Objects falling onto the ground</summary>
|
|
||||||
<img src="img/falling.gif" width = 100%/>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
> [!TIP]
|
|
||||||
> If your scene is doing something weird, try adding Avian's
|
|
||||||
> [`PhysicsDebugPlugin`](https://docs.rs/avian3d/latest/avian3d/debug_render/struct.PhysicsDebugPlugin.html)
|
|
||||||
> to your Bevy app to see the colliders at runtime.
|
|
||||||
|
|
||||||
## Add Dynamic Colliders
|
## Add Dynamic Colliders
|
||||||
|
|
||||||
Now let's go for some more complex shapes.
|
Now let's go for some more complex shapes.
|
||||||
@ -337,7 +367,7 @@ or just quickly want to test something. For this, we are going to use dynamic co
|
|||||||
|
|
||||||
### Convex
|
### Convex
|
||||||
|
|
||||||
Go back to the `Library` scene, add a new collection, and name it `Torus`. Select `Add` -> `Mesh` -> `Torus`. Leave it at the default transform. Add a `RigidBody` to it. Your scene should now look like this:
|
Go back to the `Library` scene, add a new collection, and name it `Convex`. Select `Add` -> `Mesh` -> `Torus`. Leave it at the default transform. Add a `RigidBody` to it. Your scene should now look like this:
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>A simple torus</summary>
|
<summary>A simple torus</summary>
|
||||||
@ -355,7 +385,7 @@ You can access it by expanding your object in the outliner. Its icon is a green
|
|||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>The selected mesh</summary>
|
<summary>The selected mesh</summary>
|
||||||
<img src="img/selected_mesh.png" width = 50%/>
|
<img src="img/select_mesh.png" width = 50%/>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
With the *mesh* selected, add a `ColliderConstructor` to it. Set the variant to `ConvexHullFromMesh`.
|
With the *mesh* selected, add a `ColliderConstructor` to it. Set the variant to `ConvexHullFromMesh`.
|
||||||
@ -366,11 +396,21 @@ If you did everything correctly, the component manager should say "Components fo
|
|||||||
<img src="img/torus_component.png" width = 50%/>
|
<img src="img/torus_component.png" width = 50%/>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
That's all for now
|
Go to the `World` scene and add an instance of the `Convex` collection. Save the scene, then run the game to see the torus fall down.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>The convex collider falling</summary>
|
||||||
|
<img src="img/falling_convex.gif" width = 50%/>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Is your game crashing with `Tried to add a collider to entity Torus via ConvexHullFromMesh that requires a mesh, but no mesh handle was found`?
|
||||||
|
> That means you added your `ColliderConstructor` to the object instead of the mesh.
|
||||||
|
> Go back to the screenshots above and make sure you have the mesh selected when adding the component.
|
||||||
|
|
||||||
### Concave
|
### Concave
|
||||||
|
|
||||||
Add a new collection and name it `Monkey`. Select `Add` -> `Mesh` -> `Monkey`.
|
Add a new collection and name it `Concave`. Select `Add` -> `Mesh` -> `Monkey`.
|
||||||
Yes, Blender has a builtin method for creating Suzanne, its monkey mascot. Isn't it great?
|
Yes, Blender has a builtin method for creating Suzanne, its monkey mascot. Isn't it great?
|
||||||
Anyways, add a rigid body to it. Afterwards, just as before, select the *mesh* of the monkey.
|
Anyways, add a rigid body to it. Afterwards, just as before, select the *mesh* of the monkey.
|
||||||
Add a `ColliderConstructor` to it. This time, set the variant to `TrimeshFromMesh`.
|
Add a `ColliderConstructor` to it. This time, set the variant to `TrimeshFromMesh`.
|
||||||
@ -381,22 +421,13 @@ Add a `ColliderConstructor` to it. This time, set the variant to `TrimeshFromMes
|
|||||||
> That means that any objects that are completely inside the mesh will not collide with it.
|
> That means that any objects that are completely inside the mesh will not collide with it.
|
||||||
> Only use a concave collider if you *really* need it.
|
> Only use a concave collider if you *really* need it.
|
||||||
|
|
||||||
## Add the Dynamic Colliders to the World
|
Just as before, go to the `World` scene and add an instance of the `Concave` collection. Save the scene, then run the game to see the torus fall down.
|
||||||
|
|
||||||
Save the scene to let Blenvy export everything.
|
|
||||||
Go back to the `World` scene. Add instances of the `Torus` and `Monkey` collections to the world and run the game with `cargo run`.
|
|
||||||
They should now fall onto the ground and interact with the other objects:
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>The primitive and dynamic colliders falling down</summary>
|
<summary>The concave collider falling</summary>
|
||||||
<img src="img/falling_dyn.gif" width = 100%/>
|
<img src="img/falling_concave.gif" width = 50%/>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
> [!TIP]
|
|
||||||
> Is your game crashing with `Tried to add a collider to entity Torus via <ConvexHullFromMesh or TrimeshFromMesh> that requires a mesh, but no mesh handle was found`?
|
|
||||||
> That means you added your `ColliderConstructor` to the object instead of the mesh.
|
|
||||||
> Go back to the screenshots above and make sure you have the mesh selected when adding the component.
|
|
||||||
|
|
||||||
## Other useful components
|
## Other useful components
|
||||||
|
|
||||||
The object holding the `ColliderConstructor` can hold some additional components that are useful for tweaking the physics behavior.
|
The object holding the `ColliderConstructor` can hold some additional components that are useful for tweaking the physics behavior.
|
||||||
|
BIN
examples/save_load/art/save_load.blend
Normal file
@ -1 +0,0 @@
|
|||||||
({})
|
|
@ -1,8 +0,0 @@
|
|||||||
({
|
|
||||||
"world":File (path: "models/World.glb"),
|
|
||||||
"world_dynamic":File (path: "models/World_dynamic.glb"),
|
|
||||||
|
|
||||||
"models": Folder (
|
|
||||||
path: "models/library",
|
|
||||||
),
|
|
||||||
})
|
|
BIN
examples/save_load/assets/blueprints/Mover.glb
Normal file
5
examples/save_load/assets/blueprints/Mover.meta.ron
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
(
|
||||||
|
assets:
|
||||||
|
[
|
||||||
|
]
|
||||||
|
)
|
BIN
examples/save_load/assets/blueprints/Pillar.glb
Normal file
5
examples/save_load/assets/blueprints/Pillar.meta.ron
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
(
|
||||||
|
assets:
|
||||||
|
[
|
||||||
|
]
|
||||||
|
)
|
BIN
examples/save_load/assets/levels/World.glb
Normal file
9
examples/save_load/assets/levels/World.meta.ron
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
(
|
||||||
|
assets:
|
||||||
|
[
|
||||||
|
("Pillar", File ( path: "blueprints/Pillar.glb" )),
|
||||||
|
("Stone", File ( path: "materials/Stone.glb" )),
|
||||||
|
("Mover", File ( path: "blueprints/Mover.glb" )),
|
||||||
|
("Material.001", File ( path: "materials/Material.001.glb" )),
|
||||||
|
]
|
||||||
|
)
|
BIN
examples/save_load/assets/levels/World_dynamic.glb
Normal file
14228
examples/save_load/assets/registry.json
Normal file
@ -1,14 +1,11 @@
|
|||||||
use std::any::TypeId;
|
use std::any::TypeId;
|
||||||
|
|
||||||
use bevy::{prelude::*, utils::hashbrown::HashSet};
|
use bevy::{prelude::*, utils::hashbrown::HashSet};
|
||||||
use blenvy::{AddToGameWorld, BlenvyPlugin, BluePrintBundle, BlueprintInfo, DynamicBlueprintInstance, GameWorldTag, HideUntilReady, SpawnBlueprint};
|
use blenvy::{AddToGameWorld, BlenvyPlugin, BluePrintBundle, BlueprintInfo, Dynamic, DynamicBlueprintInstance, GameWorldTag, HideUntilReady, SaveRequest, SpawnBlueprint};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
|
||||||
mod core;
|
// mod game;
|
||||||
use crate::core::*;
|
// use game::*;
|
||||||
|
|
||||||
mod game;
|
|
||||||
use game::*;
|
|
||||||
|
|
||||||
mod component_examples;
|
mod component_examples;
|
||||||
use component_examples::*;
|
use component_examples::*;
|
||||||
@ -37,13 +34,12 @@ fn main() {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
// our custom plugins
|
// our custom plugins
|
||||||
CorePlugin, // reusable plugins
|
// GamePlugin, // specific to our game
|
||||||
GamePlugin, // specific to our game
|
|
||||||
ComponentsExamplesPlugin, // Showcases different type of components /structs
|
ComponentsExamplesPlugin, // Showcases different type of components /structs
|
||||||
))
|
))
|
||||||
|
|
||||||
.add_systems(Startup, setup_game)
|
.add_systems(Startup, setup_game)
|
||||||
.add_systems(Update, (spawn_blueprint_instance, save_game, load_game))
|
.add_systems(Update, (spawn_blueprint_instance, move_movers, save_game, load_game))
|
||||||
|
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
@ -60,6 +56,13 @@ fn setup_game(
|
|||||||
HideUntilReady, // only reveal the level once it is ready
|
HideUntilReady, // only reveal the level once it is ready
|
||||||
GameWorldTag,
|
GameWorldTag,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// here we spawn our game world/level, which is also a blueprint !
|
||||||
|
commands.spawn((
|
||||||
|
BlueprintInfo::from_path("levels/World_dynamic.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
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// you can also spawn blueprint instances at runtime
|
// you can also spawn blueprint instances at runtime
|
||||||
@ -90,15 +93,47 @@ fn spawn_blueprint_instance(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_game(
|
fn move_movers(
|
||||||
keycode: Res<ButtonInput<KeyCode>>,
|
mut movers: Query<(&mut Transform), With<Dynamic>>
|
||||||
|
|
||||||
) {
|
) {
|
||||||
if keycode.just_pressed(KeyCode::KeyS) {
|
for mut transform in movers.iter_mut(){
|
||||||
|
println!("moving dynamic entity");
|
||||||
|
transform.translation.x += 0.01;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn save_game(
|
||||||
|
keycode: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut save_requests: EventWriter<SaveRequest>,
|
||||||
|
) {
|
||||||
|
if keycode.just_pressed(KeyCode::KeyS) {
|
||||||
|
save_requests.send(SaveRequest {
|
||||||
|
path: "scenes/save.scn.ron".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
pub fn request_save(
|
||||||
|
mut save_requests: EventWriter<SaveRequest>,
|
||||||
|
keycode: Res<ButtonInput<KeyCode>>,
|
||||||
|
|
||||||
|
current_state: Res<State<GameState>>,
|
||||||
|
mut next_game_state: ResMut<NextState<GameState>>,
|
||||||
|
) {
|
||||||
|
if keycode.just_pressed(KeyCode::KeyS)
|
||||||
|
&& (current_state.get() != &GameState::InLoading)
|
||||||
|
&& (current_state.get() != &GameState::InSaving)
|
||||||
|
{
|
||||||
|
next_game_state.set(GameState::InSaving);
|
||||||
|
save_requests.send(SaveRequest {
|
||||||
|
path: "save.scn.ron".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fn load_game(
|
fn load_game(
|
||||||
keycode: Res<ButtonInput<KeyCode>>,
|
keycode: Res<ButtonInput<KeyCode>>,
|
||||||
) {
|
) {
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
from ...bevy_components.components.metadata import get_bevy_component_value_by_long_name
|
||||||
|
|
||||||
# checks if an object is dynamic
|
# checks if an object is dynamic
|
||||||
# TODO: for efficiency, it might make sense to write this flag semi automatically at the root level of the object so we can skip the inner loop
|
# TODO: for efficiency, it might make sense to write this flag semi automatically at the root level of the object so we can skip the inner loop
|
||||||
# TODO: we need to recompute these on blueprint changes too
|
# TODO: we need to recompute these on blueprint changes too
|
||||||
# even better, keep a list of dynamic objects per scene , updated only when needed ?
|
# even better, keep a list of dynamic objects per scene , updated only when needed ?
|
||||||
def is_object_dynamic(object):
|
def is_object_dynamic(object):
|
||||||
is_dynamic = object['Dynamic'] if 'Dynamic' in object else False
|
is_dynamic = get_bevy_component_value_by_long_name(object, 'blenvy::save_load::Dynamic') is not None
|
||||||
|
#is_dynamic = object['Dynamic'] if 'Dynamic' in object else False
|
||||||
# only look for data in the original collection if it is not alread marked as dynamic at instance level
|
# only look for data in the original collection if it is not alread marked as dynamic at instance level
|
||||||
if not is_dynamic and object.type == 'EMPTY' and hasattr(object, 'instance_collection') and object.instance_collection is not None :
|
if not is_dynamic and object.type == 'EMPTY' and hasattr(object, 'instance_collection') and object.instance_collection is not None :
|
||||||
#print("collection", object.instance_collection, "object", object.name)
|
#print("collection", object.instance_collection, "object", object.name)
|
||||||
@ -14,15 +16,18 @@ def is_object_dynamic(object):
|
|||||||
collection_name = object.instance_collection.name
|
collection_name = object.instance_collection.name
|
||||||
original_collection = bpy.data.collections[collection_name]
|
original_collection = bpy.data.collections[collection_name]
|
||||||
|
|
||||||
|
is_dynamic = get_bevy_component_value_by_long_name(original_collection, 'blenvy::save_load::Dynamic') is not None
|
||||||
# scan original collection, look for a 'Dynamic' flag
|
# scan original collection, look for a 'Dynamic' flag
|
||||||
for object in original_collection.objects:
|
"""for object in original_collection.objects:
|
||||||
#print(" inner", object)
|
#print(" inner", object)
|
||||||
if object.type == 'EMPTY' and object.name.endswith("components"):
|
if object.type == 'EMPTY': #and object.name.endswith("components"):
|
||||||
for component_name in object.keys():
|
for component_name in object.keys():
|
||||||
#print(" compo", component_name)
|
#print(" compo", component_name)
|
||||||
if component_name == 'Dynamic':
|
if component_name == 'Dynamic':
|
||||||
is_dynamic = True
|
is_dynamic = True
|
||||||
break
|
break"""
|
||||||
|
print("IS OBJECT DYNAMIC", object, is_dynamic)
|
||||||
|
|
||||||
return is_dynamic
|
return is_dynamic
|
||||||
|
|
||||||
def is_object_static(object):
|
def is_object_static(object):
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import os
|
import os
|
||||||
import bpy
|
import bpy
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from blenvy.core.helpers_collections import (traverse_tree)
|
from blenvy.core.helpers_collections import (traverse_tree)
|
||||||
from blenvy.core.object_makers import make_cube
|
from blenvy.core.object_makers import make_cube
|
||||||
from blenvy.materials.materials_helpers import add_material_info_to_objects, get_all_materials
|
|
||||||
from ..common.generate_temporary_scene_and_export import generate_temporary_scene_and_export
|
from ..common.generate_temporary_scene_and_export import generate_temporary_scene_and_export
|
||||||
from ..common.export_gltf import (generate_gltf_export_settings)
|
from ..common.export_gltf import (generate_gltf_export_settings)
|
||||||
|
|
||||||
|