feat(Save & load): new crate bevy_gltf_save_load + lots of upgrades & improvements (#95)

* feat(bevy_gltf_save_load): saving & loading implemented
   * created new crate for save & load features, uses & filters out blueprints for efficient loading
   * saving & loading, even with dynamically spawned nested hierarchies works 
   * component filter , resource filter & save path root are configurable
   * for saving: added removal & cleanup logic for children component with children
  that have been filtered out: ie no more invalid children getting stored in the save files !
   * added sending of event once saving is done

* feat(examples/save-load): example for the new crate
   * loading level static & dynamic data as blueprints
   * added a bit of ui when entering saving & loading states & cleanup when exiting

* feat(bevy_gltf_blueprints): significant rewrite of how the crate works
     * simplified spawning process, no more spawning children containing blueprints etc
     * simplified post process : instead of copying original entity  into blueprint root
      we now copy blueprint root data (components & children) into the original entity ! fixes #96 
     * much simpler code wise
     * solves issue with needing to register components that we only use on the bevy side (not gltf)
    since we are not copying the bevy data into the blueprints data
     * added **copyComponents** helper to copy components from one entity to another, excluding existing
     components on the target entity, & some bells & whistles
     * **Name** is now optional when spawning a blueprint: closes #97 
     * **Transform** is now optional when spawning a blueprint: closes #98 
     * removed transform from bundle (BREAKING change)
     * added (optional)  **NoInBlueprint** component to have finer control over whether to inject the **InBlueprint** component inside spawned blueprint entities
     * added (optional) **Library**  component, so we can override library path when we want
     * added (optional) **AddToGameWorld** component for convenience   to automatically add entities to the game world, if there is one

* chore(bevy_gltf_components): removed verbose output, cleaned it up a bit

* feat(tools/auto_export): added option to split out "dynamic" objects in main scenes
   * ie if a collection instance (or its original collection) has a "dynamic" (aka mutable, saveable etc)
     flag it can get exported to a seperate gltf file (essentially acting like an "initial save")
   * the rest of the levels (the "static" data) is exported without the dynamic objects and
     can be reused with save files !
This commit is contained in:
Mark Moissette 2024-01-10 14:49:29 +01:00 committed by GitHub
parent 42c1c71b03
commit 5429bf4779
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
109 changed files with 5820 additions and 2475 deletions

77
Cargo.lock generated
View File

@ -779,10 +779,20 @@ dependencies = [
[[package]]
name = "bevy_gltf_blueprints"
version = "0.5.1"
version = "0.6.0"
dependencies = [
"bevy",
"bevy_gltf_components 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bevy_gltf_components 0.2.0",
]
[[package]]
name = "bevy_gltf_blueprints"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e2123036f5738c3bc75607b04799c105dff8a5626c454369bcd1401b4ec0184"
dependencies = [
"bevy",
"bevy_gltf_components 0.2.0",
]
[[package]]
@ -792,7 +802,7 @@ dependencies = [
"bevy",
"bevy_asset_loader",
"bevy_editor_pls",
"bevy_gltf_blueprints",
"bevy_gltf_blueprints 0.6.0",
"bevy_rapier3d",
"rand",
]
@ -804,7 +814,7 @@ dependencies = [
"bevy",
"bevy_asset_loader",
"bevy_editor_pls",
"bevy_gltf_blueprints",
"bevy_gltf_blueprints 0.6.0",
"bevy_rapier3d",
"rand",
]
@ -816,7 +826,7 @@ dependencies = [
"bevy",
"bevy_asset_loader",
"bevy_editor_pls",
"bevy_gltf_blueprints",
"bevy_gltf_blueprints 0.6.0",
"bevy_rapier3d",
"rand",
]
@ -828,7 +838,7 @@ dependencies = [
"bevy",
"bevy_asset_loader",
"bevy_editor_pls",
"bevy_gltf_blueprints",
"bevy_gltf_blueprints 0.6.0",
"bevy_rapier3d",
"rand",
]
@ -840,7 +850,7 @@ dependencies = [
"bevy",
"bevy_asset_loader",
"bevy_editor_pls",
"bevy_gltf_blueprints",
"bevy_gltf_blueprints 0.6.0",
"bevy_xpbd_3d",
"rand",
]
@ -852,7 +862,7 @@ dependencies = [
"bevy",
"bevy_asset_loader",
"bevy_editor_pls",
"bevy_gltf_blueprints",
"bevy_gltf_blueprints 0.6.0",
"bevy_rapier3d",
"rand",
]
@ -864,7 +874,7 @@ dependencies = [
"bevy",
"bevy_asset_loader",
"bevy_editor_pls",
"bevy_gltf_blueprints",
"bevy_gltf_blueprints 0.6.0",
"bevy_rapier3d",
"rand",
]
@ -876,7 +886,7 @@ dependencies = [
"bevy",
"bevy_asset_loader",
"bevy_editor_pls",
"bevy_gltf_blueprints",
"bevy_gltf_blueprints 0.6.0",
"bevy_rapier3d",
"rand",
]
@ -888,20 +898,11 @@ dependencies = [
"bevy",
"bevy_asset_loader",
"bevy_editor_pls",
"bevy_gltf_blueprints",
"bevy_gltf_blueprints 0.6.0",
"bevy_rapier3d",
"rand",
]
[[package]]
name = "bevy_gltf_components"
version = "0.2.0"
dependencies = [
"bevy",
"ron",
"serde",
]
[[package]]
name = "bevy_gltf_components"
version = "0.2.0"
@ -913,13 +914,22 @@ dependencies = [
"serde",
]
[[package]]
name = "bevy_gltf_components"
version = "0.2.1"
dependencies = [
"bevy",
"ron",
"serde",
]
[[package]]
name = "bevy_gltf_components_basic_example"
version = "0.3.0"
dependencies = [
"bevy",
"bevy_editor_pls",
"bevy_gltf_components 0.2.0",
"bevy_gltf_components 0.2.1",
"bevy_rapier3d",
]
@ -929,10 +939,33 @@ version = "0.3.0"
dependencies = [
"bevy",
"bevy_editor_pls",
"bevy_gltf_components 0.2.0",
"bevy_gltf_components 0.2.1",
"bevy_rapier3d",
]
[[package]]
name = "bevy_gltf_save_load"
version = "0.1.0"
dependencies = [
"bevy",
"bevy_gltf_blueprints 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "bevy_gltf_save_load_basic_example"
version = "0.3.0"
dependencies = [
"bevy",
"bevy_asset_loader",
"bevy_editor_pls",
"bevy_gltf_blueprints 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bevy_gltf_save_load",
"bevy_rapier3d",
"rand",
"serde",
"serde_json",
]
[[package]]
name = "bevy_hierarchy"
version = "0.12.1"

View File

@ -2,6 +2,7 @@
members = [
"crates/bevy_gltf_components",
"crates/bevy_gltf_blueprints",
"crates/bevy_gltf_save_load",
"examples/bevy_gltf_components/basic/",
"examples/bevy_gltf_components/basic_wasm/",
"examples/bevy_gltf_blueprints/basic/",
@ -12,7 +13,9 @@ members = [
"examples/bevy_gltf_blueprints/animation/",
"examples/bevy_gltf_blueprints/multiple_levels/",
"examples/bevy_gltf_blueprints/multiple_levels_multiple_blendfiles",
"examples/bevy_gltf_blueprints/materials/"
"examples/bevy_gltf_blueprints/materials/",
"examples/bevy_gltf_save_load/basic/",
]
resolver = "2"

View File

@ -37,6 +37,10 @@ There is a [video tutorial/explanation](https://youtu.be/CgyNtwgYwdM) for this o
The examples for the crate are [here](./examples/bevy_gltf_blueprints/)
> Note: this is the recomended crate to use and uses ```bevy_gltf_components``` under the hood
- [bevy_gltf_save_load](./crates/bevy_gltf_save_load/) This crate adds the ability to save & load your game state in a relatively simple way, by leveraging the blueprint functionality of
bevy_gltf_blueprints to only save a minimal subset of dynamic data, seperating dynamic & static parts of levels etc.
The examples for the crate are [here](./examples/bevy_gltf_save_load/)
> Note: this uses ```bevy_gltf_blueprints``` under the hood
## Tools

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_gltf_blueprints"
version = "0.5.1"
version = "0.6.0"
authors = ["Mark 'kaosat-dev' Moissette"]
description = "Adds the ability to define Blueprints/Prefabs for [Bevy](https://bevyengine.org/) inside gltf files and spawn them in Bevy."
homepage = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"

View File

@ -26,7 +26,7 @@ Here's a minimal usage example:
# Cargo.toml
[dependencies]
bevy="0.12"
bevy_gltf_blueprints = { version = "0.5"}
bevy_gltf_blueprints = { version = "0.6"}
```
@ -64,7 +64,7 @@ fn spawn_blueprint(
Add the following to your `[dependencies]` section in `Cargo.toml`:
```toml
bevy_gltf_blueprints = "0.5"
bevy_gltf_blueprints = "0.6"
```
Or use `cargo add`:
@ -97,8 +97,7 @@ use bevy_gltf_blueprints::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugin(
.add_plugins((
BlueprintsPlugin{
library_folder: "advanced/models/library".into() // replace this with your blueprints library path , relative to the assets folder,
format: GltfFormat::GLB,// optional, use either format: GltfFormat::GLB, or format: GltfFormat::GLTF, or ..Default::default() if you want to keep the default .glb extension, this sets what extensions/ gltf files will be looked for by the library
@ -107,7 +106,7 @@ fn main() {
material_library_folder: "materials".into() //defaults to "materials" the folder to look for for the material files
..Default::default()
}
)
))
.run();
}
@ -120,7 +119,8 @@ You can spawn entities from blueprints like this:
commands.spawn((
BlueprintName("Health_Pickup".to_string()), // mandatory !!
SpawnHere, // mandatory !!
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)), // VERY important !!
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)), // optional
// any other component you want to insert
))
@ -168,11 +168,51 @@ commands.spawn((
There is also a bundle for convenience , which just has
* a ```BlueprintName``` component
* a ```SpawnHere``` component
* a ```TransformBundle``` sub-bundle (so we know where to spawn)
[```BluePrintBundle```](./src/lib.rs#22)
## Additional information
- When a blueprint is spawned, all its children entities (and nested children etc) also have an ```InBlueprint``` component that gets insert
- In cases where that is undesirable, you can add a ```NoInBlueprint``` component on the entity you spawn the blueprint with, and the components above will not be add
- if you want to overwrite the **path** where this crate looks for blueprints (gltf files) , you can add a ```Library``` component , and that will be used instead of the default path
ie :
```rust no_run
commands
.spawn((
Name::from("test"),
BluePrintBundle {
blueprint: BlueprintName("TestBlueprint".to_string()),
..Default::default()
},
Library("models".into()) // now the path to the blueprint above will be /assets/models/TestBlueprint.glb
))
```
- 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"
This way all your levels, your dynamic entities etc, are kept seperated from UI nodes & other entities that are not relevant to the game world
> Note: you should only have a SINGLE entity tagged with that component !
```rust no_run
commands.spawn((
SceneBundle {
scene: models
.get(game_assets.world.id())
.expect("main level should have been loaded")
.scenes[0]
.clone(),
..default()
},
bevy::prelude::Name::from("world"),
GameWorldTag, // here it is
));
```
## SystemSet
the ordering of systems is very important !
@ -239,9 +279,9 @@ pub fn animation_change_on_proximity_foxes(
}
```
see https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/animation for how to set it up correctly
see https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/animation for how to set it up correctly
particularly from https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/animation/game/in_game.rs#86
particularly from https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/animation/game/in_game.rs#86
onward
@ -263,27 +303,27 @@ material_library_folder: "materials".into() //defaults to "materials" the folder
```bevy_gltf_blueprints``` currently does NOT take care of loading those at runtime
see https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/materials for how to set it up correctly
see https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/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)
## Examples
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/basic
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/basic
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/basic_xpbd_physics
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/basic_xpbd_physics
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/basic_scene_components
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/basic_scene_components
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/animation
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/animation
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/multiple_levels
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/multiple_levels
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/materials
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/materials
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/nested_blueprints
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/nested_blueprints
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/multiple_levels_multiple_blendfiles
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_blueprints/multiple_levels_multiple_blendfiles
## Compatible Bevy versions
@ -293,7 +333,7 @@ The main branch is compatible with the latest Bevy release, while the branch `be
Compatibility of `bevy_gltf_blueprints` versions:
| `bevy_gltf_blueprints` | `bevy` |
| :-- | :-- |
| `0.3 - 0.5` | `0.12` |
| `0.3 - 0.6` | `0.12` |
| `0.1 - 0.2` | `0.11` |
| branch `main` | `0.12` |
| branch `bevy_main` | `main` |

View File

@ -1,13 +1,10 @@
use bevy::{math::Vec3A, prelude::*, render::primitives::Aabb};
use crate::{BluePrintsConfig, BlueprintName, SpawnedRoot};
use crate::{BluePrintsConfig, Spawned};
/// helper system that computes the compound aabbs of the scenes/blueprints
pub fn compute_scene_aabbs(
root_entities: Query<
(Entity, &Name, &Children, &BlueprintName),
(With<SpawnedRoot>, Without<Aabb>),
>,
root_entities: Query<(Entity, &Name), (With<Spawned>, Without<Aabb>)>,
children: Query<&Children>,
existing_aabbs: Query<&Aabb>,
@ -15,10 +12,8 @@ pub fn compute_scene_aabbs(
mut commands: Commands,
) {
// compute compound aabb
for root_entity in root_entities.iter() {
let name = &root_entity.3 .0;
let root_entity = root_entity.2.first().unwrap();
for (root_entity, name) in root_entities.iter() {
// info!("generating aabb for {:?}", name);
// only recompute aabb if it has not already been done before
if blueprints_config.aabb_cache.contains_key(&name.to_string()) {
@ -26,10 +21,10 @@ pub fn compute_scene_aabbs(
.aabb_cache
.get(&name.to_string())
.expect("we should have the aabb available");
commands.entity(*root_entity).insert(*aabb);
commands.entity(root_entity).insert(*aabb);
} else {
let aabb = compute_descendant_aabb(*root_entity, &children, &existing_aabbs);
commands.entity(*root_entity).insert(aabb);
let aabb = compute_descendant_aabb(root_entity, &children, &existing_aabbs);
commands.entity(root_entity).insert(aabb);
blueprints_config.aabb_cache.insert(name.to_string(), aabb);
}
}

View File

@ -1,70 +0,0 @@
use bevy::ecs::system::Command;
use bevy::prelude::*;
// modified version from https://github.com/bevyengine/bevy/issues/1515,
// more specifically https://gist.github.com/nwtnni/85d6b87ae75337a522166c500c9a8418
// to work with Bevy 0.11
pub struct CloneEntity {
pub source: Entity,
pub destination: Entity,
}
impl CloneEntity {
// Copy all components from an entity to another.
// Using an entity with no components as the destination creates a copy of the source entity.
// Panics if:
// - the components are not registered in the type registry,
// - the world does not have a type registry
// - the source or destination entity do not exist
fn clone_entity(self, world: &mut World) {
let components = {
let registry = world
.get_resource::<AppTypeRegistry>()
.expect("the world should have a type registry")
.read();
world
.get_entity(self.source)
.expect("source entity should exist")
.archetype()
.components()
.map(|component_id| {
let component_info = world
.components()
.get_info(component_id)
.expect("component info should be available");
let type_id = component_info.type_id().unwrap();
let type_id = registry.get(type_id).expect(
format!(
"cannot clone entity: component: {:?} is not registered",
component_info.name()
)
.as_str(),
);
return type_id.data::<ReflectComponent>().unwrap().clone();
})
.collect::<Vec<_>>()
};
for component in components {
let source = component
.reflect(world.get_entity(self.source).unwrap())
.unwrap()
.clone_value();
let mut destination = world
.get_entity_mut(self.destination)
.expect("destination entity should exist");
component.apply_or_insert(&mut destination, &*source);
}
}
}
// This allows the command to be used in systems
impl Command for CloneEntity {
fn apply(self, world: &mut World) {
self.clone_entity(world)
}
}

View File

@ -0,0 +1,109 @@
use bevy::ecs::system::Command;
use bevy::prelude::*;
use std::any::TypeId;
// originally based https://github.com/bevyengine/bevy/issues/1515,
// more specifically https://gist.github.com/nwtnni/85d6b87ae75337a522166c500c9a8418
// to work with Bevy 0.11
// to copy components between entities but NOT overwriting any existing components
// plus some bells & whistles
pub struct CopyComponents {
pub source: Entity,
pub destination: Entity,
pub exclude: Vec<TypeId>,
pub stringent: bool,
}
impl CopyComponents {
// Copy all components from an entity to another.
// Using an entity with no components as the destination creates a copy of the source entity.
// Panics if:
// - the components are not registered in the type registry,
// - the world does not have a type registry
// - the source or destination entity do not exist
fn transfer_components(self, world: &mut World) {
let components = {
let registry = world
.get_resource::<AppTypeRegistry>()
.expect("the world should have a type registry")
.read();
world
.get_entity(self.source)
.expect("source entity should exist")
.archetype()
.components()
.filter_map(|component_id| {
let component_info = world
.components()
.get_info(component_id)
.expect("component info should be available");
let type_id = component_info.type_id().unwrap();
if self.exclude.contains(&type_id) {
debug!("excluding component: {:?}", component_info.name());
return None;
} else {
debug!(
"cloning: component: {:?} {:?}",
component_info.name(),
type_id
);
if let Some(type_registration) = registry.get(type_id) {
return Some(type_registration);
} else {
if self.stringent {
return Some(
registry.get(type_id).expect(
format!(
"cannot clone entity: component: {:?} is not registered",
component_info.name()
)
.as_str(),
),
);
} else {
warn!(
"cannot clone component: component: {:?} is not registered",
component_info.name()
);
return None;
}
}
}
})
.map(|type_id| {
return (
type_id.data::<ReflectComponent>().unwrap().clone(),
type_id.type_info().type_id().clone(), // we need the original type_id down the line
);
})
.collect::<Vec<_>>()
};
for (component, type_id) in components {
let source = component
.reflect(world.get_entity(self.source).unwrap())
.unwrap()
.clone_value();
let mut destination = world
.get_entity_mut(self.destination)
.expect("destination entity should exist");
// println!("contains typeid {:?} {}", type_id, destination.contains_type_id(type_id));
// we only want to copy components that are NOT already in the destination (ie no overwriting existing components)
if !destination.contains_type_id(type_id) {
component.insert(&mut destination, &*source);
}
}
}
}
// This allows the command to be used in systems
impl Command for CopyComponents {
fn apply(self, world: &mut World) {
self.transfer_components(world)
}
}

View File

@ -13,8 +13,8 @@ pub use aabb::*;
pub mod materials;
pub use materials::*;
pub mod clone_entity;
pub use clone_entity::*;
pub mod copy_components;
pub use copy_components::*;
use core::fmt;
use std::path::PathBuf;
@ -33,14 +33,12 @@ pub enum GltfBlueprintsSet {
pub struct BluePrintBundle {
pub blueprint: BlueprintName,
pub spawn_here: SpawnHere,
pub transform: TransformBundle,
}
impl Default for BluePrintBundle {
fn default() -> Self {
BluePrintBundle {
blueprint: BlueprintName("default".into()),
spawn_here: SpawnHere,
transform: TransformBundle::default(),
}
}
}
@ -140,6 +138,7 @@ impl Plugin for BlueprintsPlugin {
spawn_from_blueprints,
compute_scene_aabbs.run_if(aabbs_enabled),
apply_deferred.run_if(aabbs_enabled),
apply_deferred,
materials_inject.run_if(materials_library_enabled),
)
.chain()
@ -147,12 +146,7 @@ impl Plugin for BlueprintsPlugin {
)
.add_systems(
Update,
(
update_spawned_root_first_child,
apply_deferred,
cleanup_scene_instances,
apply_deferred,
)
(spawned_blueprint_post_process, apply_deferred)
.chain()
.in_set(GltfBlueprintsSet::AfterSpawn),
);

View File

@ -1,4 +1,4 @@
use std::path::Path;
use std::path::{Path, PathBuf};
use bevy::{gltf::Gltf, prelude::*};
@ -19,44 +19,87 @@ pub struct BlueprintName(pub String);
pub struct SpawnHere;
#[derive(Component)]
/// FlagComponent for spawned entity
/// FlagComponent for dynamically spawned scenes
pub struct Spawned;
#[derive(Component)]
/// helper component, just to transfer some data
pub(crate) struct Original(pub Entity);
#[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 spwaned 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)]
/// FlagComponent for dynamically spawned scenes
pub struct SpawnedRoot;
/// helper component, just to transfer child data
pub(crate) struct OriginalChildren(pub Vec<Entity>);
/// main spawning functions,
/// * also takes into account the already exisiting "override" components, ie "override components" > components from blueprint
pub(crate) fn spawn_from_blueprints(
spawn_placeholders: Query<
(Entity, &Name, &BlueprintName, &Transform, Option<&Parent>),
(
Added<BlueprintName>,
Added<SpawnHere>,
Without<Spawned>,
Without<SpawnedRoot>,
Entity,
&BlueprintName,
Option<&Transform>,
Option<&Parent>,
Option<&Library>,
Option<&AddToGameWorld>,
Option<&Name>,
),
(Added<BlueprintName>, Added<SpawnHere>, Without<Spawned>),
>,
mut commands: Commands,
mut game_world: Query<(Entity, &Children), With<GameWorldTag>>,
mut game_world: Query<Entity, With<GameWorldTag>>,
assets_gltf: Res<Assets<Gltf>>,
asset_server: Res<AssetServer>,
blueprints_config: Res<BluePrintsConfig>,
children: Query<&Children>,
) {
for (entity, name, blupeprint_name, transform, original_parent) in spawn_placeholders.iter() {
debug!("need to spawn {:?}, id: {:?}", blupeprint_name.0, entity);
for (
entity,
blupeprint_name,
transform,
original_parent,
library_override,
add_to_world,
name,
) in spawn_placeholders.iter()
{
debug!(
"need to spawn {:?} for entity {:?}, id: {:?}, parent:{:?}",
blupeprint_name.0, name, entity, original_parent
);
let mut original_children: Vec<Entity> = vec![];
if let Ok(c) = children.get(entity) {
for child in c.iter() {
original_children.push(*child);
}
}
let what = &blupeprint_name.0;
let model_file_name = format!("{}.{}", &what, &blueprints_config.format);
let model_path =
Path::new(&blueprints_config.library_folder).join(Path::new(model_file_name.as_str()));
// library path is either defined at the plugin level or overriden by optional Library components
let library_path =
library_override.map_or_else(|| &blueprints_config.library_folder, |l| &l.0);
let model_path = Path::new(&library_path).join(Path::new(model_file_name.as_str()));
debug!("attempting to spawn {:?}", model_path);
let model_handle: Handle<Gltf> = asset_server.load(model_path);
@ -73,32 +116,30 @@ pub(crate) fn spawn_from_blueprints(
.expect("there should be at least one named scene in the gltf file to spawn");
let scene = &gltf.named_scenes[main_scene_name];
let child_scene = commands
.spawn((
SceneBundle {
scene: scene.clone(),
transform: transform.clone(),
..Default::default()
},
name.clone(),
// Parent(world) // FIXME/ would be good if this worked directly
SpawnedRoot,
BlueprintName(blupeprint_name.0.clone()),
Original(entity),
Animations {
named_animations: gltf.named_animations.clone(),
},
))
.id();
let world = game_world.single_mut();
let mut parent = world.1[0]; // FIXME: dangerous hack because our gltf data have a single child like this, but might not always be the case
// ideally, insert the newly created entity as a child of the original parent, if any, the world otherwise
if let Some(original_parent) = original_parent {
parent = original_parent.get();
// transforms are optional, but still deal with them correctly
let mut transforms: Transform = Transform::default();
if transform.is_some() {
transforms = transform.unwrap().clone();
}
commands.entity(parent).add_child(child_scene);
commands.entity(entity).insert((
SceneBundle {
scene: scene.clone(),
transform: transforms,
..Default::default()
},
Animations {
named_animations: gltf.named_animations.clone(),
},
Spawned,
OriginalChildren(original_children),
));
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,128 +1,94 @@
use std::any::TypeId;
use bevy::prelude::*;
use bevy::scene::SceneInstance;
// use bevy::utils::hashbrown::HashSet;
use super::{AnimationPlayerLink, Animations};
use super::{CloneEntity, SpawnHere};
use super::{Original, SpawnedRoot};
use super::{SpawnHere, Spawned};
use crate::{CopyComponents, InBlueprint, NoInBlueprint, OriginalChildren};
#[derive(Component)]
/// FlagComponent for dynamically spawned scenes
pub(crate) struct SpawnedRootProcessed;
/// this system updates the first (and normally only) child of a scene flaged SpawnedRoot
/// - adds a name based on parent component (spawned scene) which is named on the scene name/prefab to be instanciated
// FIXME: updating hierarchy does not work in all cases ! this is sadly dependant on the structure of the exported blend data
// - blender root-> object with properties => WORKS
// - scene instance -> does not work
// it might be due to how we add components to the PARENT item in gltf to components
pub(crate) fn update_spawned_root_first_child(
//
/// 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(
unprocessed_entities: Query<
(Entity, &Children, &Name, &Parent, &Original),
(With<SpawnedRoot>, Without<SpawnedRootProcessed>),
(
Entity,
&Children,
&OriginalChildren,
&Animations,
Option<&NoInBlueprint>,
Option<&Name>,
),
(With<SpawnHere>, With<SceneInstance>, With<Spawned>),
>,
mut commands: Commands,
animations: Query<&Animations>,
added_animation_players: Query<(Entity, &Parent), Added<AnimationPlayer>>,
all_children: Query<&Children>,
mut commands: Commands,
) {
/*
currently we have
- scene instance
- root node ?
- the actual stuff
we want to remove the root node
so we wend up with
- scene instance
- the actual stuff
for (original, children, original_children, animations, no_inblueprint, name) in
unprocessed_entities.iter()
{
debug!("post processing blueprint for entity {:?}", name);
so
- get root node
- add its children to the scene instance
- remove root node
Another issue is, the scene instance become empty if we have a pickabke as the "actual stuff", so that would lead to a lot of
empty scenes if we spawn pickables
- perhaps another system that cleans up empty scene instances ?
FIME: this is all highly dependent on the hierachy ;..
*/
for (scene_instance, children, name, parent, original) in unprocessed_entities.iter() {
//
if children.len() == 0 {
warn!("timing issue ! no children found, please restart your bevy app (bug being investigated)");
// println!("children of scene {:?}", children);
continue;
}
// the root node is the first & normally only child inside a scene, it is the one that has all relevant components
let root_entity = children.first().unwrap(); //FIXME: and what about childless ones ?? => should not be possible normally
// let root_entity_data = all_children.get(*root_entity).unwrap();
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;
}
}
// fixme : randomization should be controlled via parameters, perhaps even the seed could be specified ?
// use this https://rust-random.github.io/book/guide-seeding.html#a-simple-number, blenders seeds are also uInts
// also this is not something we want every time, this should be a settable parameter when requesting a spawn
// 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);
}
}
// add missing name of entity, based on the wrapper's name
let name = name.clone();
// 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,
});
// this is our new actual entity
commands.entity(*root_entity).insert((
bevy::prelude::Name::from(name.clone()),
// ItemType {name},
));
// 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);
}
}
// flag the spawned_root as being processed
commands.entity(scene_instance).insert(SpawnedRootProcessed);
// parent is either the world or an entity with a marker (BlueprintName)
commands.entity(parent.get()).add_child(*root_entity);
let matching_animations = animations.get(scene_instance);
if let Ok(animations) = matching_animations {
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(*root_entity)
.insert(AnimationPlayerLink(added));
commands.entity(*root_entity).insert(Animations {
named_animations: animations.named_animations.clone(),
});
}
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(AnimationPlayerLink(added));
}
}
}
commands.add(CloneEntity {
source: original.0,
destination: *root_entity,
});
// remove the original entity, now that we have cloned it into the spawned scenes first child
commands.entity(original.0).despawn_recursive();
commands.entity(*root_entity).remove::<SpawnHere>();
}
}
/// cleans up dynamically spawned scenes so that they get despawned if they have no more children
pub(crate) fn cleanup_scene_instances(
scene_instances: Query<(Entity, &Children), With<SpawnedRootProcessed>>,
without_children: Query<Entity, (With<SpawnedRootProcessed>, Without<Children>)>, // if there are not children left, bevy removes Children ?
mut commands: Commands,
) {
for (entity, children) in scene_instances.iter() {
if children.len() == 0 {
// it seems this does not happen ?
debug!("cleaning up emptied spawned scene instance");
commands.entity(entity).despawn_recursive();
}
}
for entity in without_children.iter() {
debug!("cleaning up emptied spawned scene instance");
commands.entity(entity).despawn_recursive();
commands.entity(original).remove::<SpawnHere>();
commands.entity(original).remove::<Spawned>();
commands.entity(original).remove::<Handle<Scene>>();
commands.entity(root_entity).despawn_recursive();
}
}

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_gltf_components"
version = "0.2.0"
version = "0.2.1"
authors = ["Mark 'kaosat-dev' Moissette"]
description = "Allows you to define [Bevy](https://bevyengine.org/) components direclty inside gltf files and instanciate the components on the Bevy side."
homepage = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"

View File

@ -10,7 +10,7 @@ use bevy::reflect::{Reflect, TypeInfo, TypeRegistry};
use bevy::scene::Scene;
use bevy::utils::HashMap;
use bevy::{
log::{debug, info, warn},
log::{debug, warn},
prelude::{Assets, Name, Parent, ResMut},
};
@ -252,5 +252,5 @@ pub fn gltf_extras_to_components(
}
}
}
info!("done injecting components from gltf_extras /n");
debug!("done injecting components from gltf_extras");
}

View File

@ -0,0 +1,18 @@
[package]
name = "bevy_gltf_save_load"
version = "0.1.0"
authors = ["Mark 'kaosat-dev' Moissette"]
description = "Save & load your bevy games"
homepage = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"
repository = "https://github.com/kaosat-dev/Blender_bevy_components_workflow"
keywords = ["gamedev", "bevy", "save", "load", "serialize"]
categories = ["game-development"]
edition = "2021"
license = "MIT OR Apache-2.0"
[dev-dependencies]
bevy = { version = "0.12", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf"] }
[dependencies]
bevy = { version = "0.12", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf"] }
bevy_gltf_blueprints = "0.6"

View File

@ -0,0 +1,4 @@
This crate is available under either:
* The [MIT License](./LICENSE_MIT)
* The [Apache License, Version 2.0](./LICENSE_APACHE)

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2023] [Mark "kaosat-dev" Moissette]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Mark "kaosat-dev" Moissette
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,312 @@
[![Crates.io](https://img.shields.io/crates/v/bevy_gltf_save_load)](https://crates.io/crates/bevy_gltf_save_load)
[![Docs](https://img.shields.io/docsrs/bevy_gltf_save_load)](https://docs.rs/bevy_gltf_save_load/latest/bevy_gltf_save_load/)
[![License](https://img.shields.io/crates/l/bevy_gltf_save_load)](https://github.com/kaosat-dev/Blender_bevy_components_workflow/blob/main/crates/bevy_gltf_save_load/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_gltf_save_load
Built upon [bevy_gltf_blueprints](https://crates.io/crates/bevy_gltf_blueprints) this crate 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)
A bit of heads up:
* very opinionated !
* still in the early stages & not 100% feature complete
* fun fact: as the static level structure is stored seperatly, you can change your level layout & **still** reload an existing save file
## Usage
Here's a minimal usage example:
```toml
# Cargo.toml
[dependencies]
bevy="0.12"
bevy_gltf_save_load = "0.1"
bevy_gltf_blueprints = "0.6" // also needed
```
```rust no_run
use bevy::prelude::*;
use bevy_gltf_save_load::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
SaveLoadPlugin::default()
))
.run();
}
// add a system to trigger saving
pub fn request_save(
mut save_requests: EventWriter<SaveRequest>,
keycode: Res<Input<KeyCode>>,
)
{
if keycode.just_pressed(KeyCode::S) {
save_requests.send(SaveRequest {
path: "save.scn.ron".into(),
})
}
}
// add a system to trigger loading
pub fn request_load(
mut load_requests: EventWriter<LoadRequest>,
keycode: Res<Input<KeyCode>>,
)
{
if keycode.just_pressed(KeyCode::L) {
save_requests.send(LoadRequest {
path: "save.scn.ron".into(),
})
}
}
// setting up your world
// on initial setup, the static entities & the dynamic entities are kept seperate for clarity & loaded as blueprints from 2 seperate files
pub fn setup_game(
mut commands: Commands,
mut next_game_state: ResMut<NextState<GameState>>,
) {
info!("setting up game world");
// here we actually spawn our game world/level
let world_root = commands
.spawn((
Name::from("world"),
GameWorldTag,
InAppRunning,
TransformBundle::default(),
InheritedVisibility::default(),
))
.id();
// and we fill it with static entities
let static_data = commands
.spawn((
Name::from("static"),
BluePrintBundle {
blueprint: BlueprintName("World".to_string()),
..Default::default()
},
StaticEntitiesRoot,
Library("models".into())
))
.id();
// and we fill it with dynamic entities
let dynamic_data = commands
.spawn((
Name::from("dynamic"),
BluePrintBundle {
blueprint: BlueprintName("World_dynamic".to_string()),
..Default::default()
},
DynamicEntitiesRoot,
NoInBlueprint,
Library("models".into())
))
.id();
commands.entity(world_root).add_child(static_data);
commands.entity(world_root).add_child(dynamic_data);
next_game_state.set(GameState::InGame)
}
```
take a look at the [example]('https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_save_load/basic/src/game/mod.rs) for more clarity
## Installation
Add the following to your `[dependencies]` section in `Cargo.toml`:
```toml
bevy_gltf_save_load = "0.1"
bevy_gltf_blueprints = "0.6" // also needed, as bevy_gltf_save_load does not re-export it at this time
```
Or use `cargo add`:
```toml
cargo add bevy_gltf_save_load
```
## Setup
```rust no_run
use bevy::prelude::*;
use bevy_gltf_save_load::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins
SaveLoadPlugin::default()
))
.run();
}
```
you likely need to configure your settings (otherwise, not much will be saved)
```rust no_run
use bevy::prelude::*;
use bevy_gltf_save_load::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
SaveLoadPlugin {
save_path: "scenes".into(), // where do we save files to (under assets for now) defaults to "scenes"
component_filter: SceneFilter::Allowlist(HashSet::from([ // this is using Bevy's build in SceneFilter, you can compose what components you want to allow/deny
TypeId::of::<Name>(),
TypeId::of::<Transform>(),
TypeId::of::<Velocity>(),
// and any other commponent you want to include/exclude
])),
resource_filter: SceneFilter::deny_all(), // same logic as above, but for resources : also be careful & remember to register your resources !
..Default::default()
},
// you need to configure the blueprints plugin as well (might be pre_configured in the future, but for now you need to do it manually)
BlueprintsPlugin {
library_folder: "models/library".into(),
format: GltfFormat::GLB,
aabbs: true,
..Default::default()
},
))
.run();
}
```
### How to make sure your entites will be saved
- only entites that have a **Dynamic** component will be saved ! (the component is provided as part of the crate)
- you can either add that component at runtime or have it baked-in in the Blueprint
### Component Filter:
- by default only the following components are going to be saved
- **Parent**
- **Children**
- **BlueprintName** : part of bevy_gltf_blueprints, used under the hood
- **SpawnHere** :part of bevy_gltf_blueprints, used under the hood
- **Dynamic** : included in this crate, allows you to tag components as dynamic aka saveable ! Use this to make sure your entities are saved !
- you **CANNOT** remove these as they are part of the boilerplate
- you **CAN** add however many other components you want, allow them all etc as you see fit
- you can find more information about the SceneFilter object [here](https://bevyengine.org/news/bevy-0-11/#scene-filtering) and [here](https://docs.rs/bevy/latest/bevy/scene/enum.SceneFilter.html)
## Events
- to trigger **saving** use the ```SaveRequest``` event
```rust no_run
// add a system to trigger saving
pub fn request_save(
mut save_requests: EventWriter<SaveRequest>,
keycode: Res<Input<KeyCode>>,
)
{
if keycode.just_pressed(KeyCode::S) {
save_requests.send(SaveRequest {
path: "save.scn.ron".into(),
})
}
}
```
- to trigger **loading** use the ```LoadRequest``` event
```rust no_run
// add a system to trigger saving
pub fn request_load(
mut load_requests: EventWriter<LoadRequest>,
keycode: Res<Input<KeyCode>>,
)
{
if keycode.just_pressed(KeyCode::L) {
save_requests.send(LoadRequest {
path: "save.scn.ron".into(),
})
}
}
```
- you also notified when saving / loading is done
- ```SavingFinished``` for saving
- ```LoadingFinished``` for loading
> Note: I **highly** recomend you change states when you start/finish saving & loading, otherwise things **will** get unpredictable
Please see [the example]('https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_save_load/basic/src/game/mod.rs#77') for this.
## Additional notes
- the name + path of the **static** level blueprint/gltf file will be saved as part of the save file, and reused to dynamically
load the correct static assets, which is necessary when you have multiple levels, and thus all required information to reload a save is contained within the save
## SystemSet
For convenience ```bevy_gltf_save_load``` provides two **SystemSets**
- [```LoadingSet```](./src/lib.rs#19)
- [```SavingSet```](./src/lib.rs#24)
## Examples
Highly advised to get a better understanding of how things work !
To get started I recomend looking at
- [world setup]('https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_save_load/basic/src/game/in_game.rs#13')
- [various events & co]('https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_save_load/basic/src/game/mod.rs#77')
All examples are here:
- https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/bevy_gltf_save_load/basic
## Compatible Bevy versions
The main branch is compatible with the latest Bevy release, while the branch `bevy_main` tries to track the `main` branch of Bevy (PRs updating the tracked commit are welcome).
Compatibility of `bevy_gltf_save_load` versions:
| `bevy_gltf_save_load` | `bevy` |
| :-- | :-- |
| `0.1 ` | `0.12` |
| branch `main` | `0.12` |
| branch `bevy_main` | `main` |
## License
This crate, all its code, contents & assets is Dual-licensed under either of
- Apache License, Version 2.0, ([LICENSE-APACHE](./LICENSE_APACHE.md) or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](./LICENSE_MIT.md) or https://opensource.org/licenses/MIT)

View File

@ -0,0 +1,216 @@
use gltf_json as json;
use json::camera::Type;
use json::validation::{Checked, Validate};
use serde_json::value::{to_raw_value, RawValue};
use serde::Serialize;
use bevy::reflect::TypeRegistryArc;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Output {
/// Output standard glTF.
Standard,
/// Output binary glTF.
Binary,
}
#[derive(Serialize)]
struct MyExtraData {
a: u32,
b: u32,
BlueprintName: String,
SpawnHere: String,
}
/*
pub fn serialize_gltf_inner<S>(serialize: S) -> Result<String, json::Error>
where
S: Serialize,
{
let pretty_config = ron::ser::PrettyConfig::default()
.indentor(" ".to_string())
.new_line("\n".to_string());
ron::ser::to_string_pretty(&serialize, pretty_config)
}*/
pub fn serialize_gltf(scene:&DynamicScene, registry: &TypeRegistryArc) {
}
pub fn save_game(
world: &mut World,
) {
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 = event.path.clone();
}
info!("SAVING TO {}", save_path);
events.clear();
let saveable_entities: Vec<Entity> = world
.query_filtered::<Entity, With<Dynamic>>()
.iter(world)
.collect();
debug!("saveable entities {}", saveable_entities.len());
let components = HashSet::from([
TypeId::of::<Name>(),
TypeId::of::<Transform>(),
TypeId::of::<Velocity>() ,
TypeId::of::<BlueprintName>(),
TypeId::of::<SpawnHere>(),
TypeId::of::<Dynamic>(),
TypeId::of::<Camera>(),
TypeId::of::<Camera3d>(),
TypeId::of::<Tonemapping>(),
TypeId::of::<CameraTrackingOffset>(),
TypeId::of::<Projection>(),
TypeId::of::<CameraRenderGraph>(),
TypeId::of::<Frustum>(),
TypeId::of::<GlobalTransform>(),
TypeId::of::<VisibleEntities>(),
TypeId::of::<Pickable>(),
]);
let filter = SceneFilter::Allowlist(components);
let mut scene_builder = DynamicSceneBuilder::from_world(world).with_filter(filter);
let dyn_scene = scene_builder
/* .allow::<Transform>()
.allow::<Velocity>()
.allow::<BlueprintName>()*/
/* .deny::<Children>()
.deny::<Parent>()
.deny::<InheritedVisibility>()
.deny::<Visibility>()
.deny::<GltfExtras>()
.deny::<GlobalTransform>()
.deny::<Collider>()
.deny::<RigidBody>()
.deny::<Saveable>()
// camera stuff
.deny::<Camera>()
.deny::<CameraRenderGraph>()
.deny::<Camera3d>()
.deny::<Clusters>()
.deny::<VisibleEntities>()
.deny::<VisiblePointLights>()
//.deny::<HasGizmoMarker>()
*/
.extract_entities(saveable_entities.into_iter())
.build();
let serialized_scene = dyn_scene
.serialize_ron(world.resource::<AppTypeRegistry>())
.unwrap();
let mut root = gltf_json::Root::default();
// unfortunatly, not available yet
/*let node = root.push(json::Node {
//mesh: Some(mesh),
..Default::default()
});
root.push(json::Scene {
extensions: Default::default(),
extras: Default::default(),
name: None,
nodes: vec![node],
});*/
let camera = json::camera::Perspective{
aspect_ratio: Some(0.5),
yfov: 32.0,
zfar: Some(30.),
znear: 0.0,
extensions: None,
extras: None
};
/*let camera = json::Camera{
name:Some("Camera".into()),
orthographic: None,
perspective:None,
extensions: None,
extras: None,
type_: Checked<Type::Perspective>,
};*/
let gna = to_raw_value(&MyExtraData { a: 1, b: 2, BlueprintName: "Foo".into(), SpawnHere:"".into() }).unwrap() ;
let node = json::Node {
camera: None,//Some(camera),
children: None,
extensions: None,
extras: Some(gna),
matrix: None,
mesh:None,
name: Some("yeah".into()),
rotation: None,
scale: None,
translation: Some([0.5, 10.0 ,-100.]),
skin: None,
weights: None
// mesh: Some(json::Index::new(0)),
//..Default::default()
};
let root = json::Root {
accessors: vec![], //[positions, colors],
buffers: vec![],
buffer_views: vec![],
meshes: vec![],
nodes: vec![node],
scenes: vec![json::Scene {
extensions: Default::default(),
extras: Default::default(),
name: Some("Foo".to_string()),
nodes: vec![json::Index::new(0)],
}],
..Default::default()
};
let gltf_save_name = "test.gltf";
let writer = fs::File::create(format!("assets/scenes/{gltf_save_name}") ).expect("I/O error");
json::serialize::to_writer_pretty(writer, &root).expect("Serialization error");
// let bin = to_padded_byte_vector(triangle_vertices);
// let mut writer = fs::File::create("triangle/buffer0.bin").expect("I/O error");
// writer.write_all(&bin).expect("I/O error");
#[cfg(not(target_arch = "wasm32"))]
IoTaskPool::get()
.spawn(async move {
// Write the scene RON data to file
File::create(format!("assets/scenes/{save_path}"))
.and_then(|mut file| file.write(serialized_scene.as_bytes()))
.expect("Error while writing scene to file");
})
.detach();
}

View File

@ -0,0 +1,108 @@
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 bevy_gltf_blueprints::GltfBlueprintsSet;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum SavingSet {
Save,
}
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum LoadingSet {
Load,
}
// Plugin configuration
#[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"),
}
}
}
#[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>()
.insert_resource(SaveLoadConfig {
save_path: self.save_path.clone(),
component_filter: self.component_filter.clone(),
resource_filter: self.resource_filter.clone(),
})
.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),
);
}
}

View File

@ -0,0 +1,156 @@
use bevy::{prelude::*, scene::SceneInstance};
use bevy_gltf_blueprints::{BluePrintBundle, BlueprintName, GameWorldTag, Library};
use std::path::Path;
use crate::{DynamicEntitiesRoot, SaveLoadConfig, StaticEntitiesRoot, StaticEntitiesStorage};
#[derive(Event)]
pub struct LoadRequest {
pub path: String,
}
#[derive(Event)]
pub struct LoadingFinished;
#[derive(Resource, Default)]
pub struct LoadRequested {
pub path: String,
}
#[derive(Resource, Default)]
pub(crate) struct LoadFirstStageDone;
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
pub(crate) struct CleanupScene;
/// helper system that "converts" loadRequest events to LoadRequested resources
pub(crate) fn mark_load_requested(
mut load_requests: EventReader<LoadRequest>,
mut commands: Commands,
) {
let mut save_path: String = "".into();
for load_request in load_requests.read() {
if load_request.path != "" {
save_path = load_request.path.clone();
}
}
if save_path != "" {
commands.insert_resource(LoadRequested { path: save_path });
}
}
// TODO: replace with generic despawner ?
pub(crate) fn unload_world(mut commands: Commands, gameworlds: Query<Entity, With<GameWorldTag>>) {
for e in gameworlds.iter() {
info!("--loading: despawn old world/level");
commands.entity(e).despawn_recursive();
}
}
pub(crate) fn load_game(
mut commands: Commands,
asset_server: Res<AssetServer>,
load_request: Res<LoadRequested>,
save_load_config: Res<SaveLoadConfig>,
) {
info!("--loading: load dynamic data");
let save_path = load_request.path.clone();
let save_path = Path::new(&save_load_config.save_path).join(Path::new(save_path.as_str()));
info!("LOADING FROM {:?}", save_path);
let world_root = commands
.spawn((
bevy::prelude::Name::from("world"),
GameWorldTag,
TransformBundle::default(),
InheritedVisibility::default(),
))
.id();
// and we fill it with dynamic data
// let input = std::fs::read(&path)?;
let dynamic_data = commands
.spawn((
DynamicSceneBundle {
scene: asset_server.load(save_path),
..default()
},
bevy::prelude::Name::from("dynamic"),
DynamicEntitiesRoot,
))
.id();
// commands.entity(world_root).add_child(static_data);
commands.entity(world_root).add_child(dynamic_data);
commands.insert_resource(LoadFirstStageDone);
info!("--loading: loaded dynamic data");
}
pub(crate) fn load_static(
dynamic_worlds: Query<Entity, With<SceneInstance>>,
world_root: Query<Entity, With<GameWorldTag>>,
mut commands: Commands,
mut loading_finished: EventWriter<LoadingFinished>,
static_entities: Option<Res<StaticEntitiesStorage>>,
) {
if let Some(info) = static_entities {
info!("--loading static data {:?}", info.name);
let static_data = commands
.spawn((
Name::from("static"),
BluePrintBundle {
blueprint: BlueprintName(info.name.clone()),
..Default::default()
},
StaticEntitiesRoot,
))
.id();
if info.library_path != "" {
commands
.entity(static_data)
.insert(Library(info.library_path.clone().into()));
}
let world_root = world_root.get_single().unwrap();
commands.entity(world_root).add_child(static_data);
info!("--loading: loaded static data");
for entity in dynamic_worlds.iter() {
commands.entity(entity).insert(
CleanupScene, // we mark this scene as needing a cleanup
);
}
loading_finished.send(LoadingFinished);
}
}
pub(crate) fn cleanup_loaded_scene(
loaded_scenes: Query<
Entity,
(
Added<CleanupScene>,
With<SceneInstance>,
With<DynamicEntitiesRoot>,
),
>,
mut commands: Commands,
) {
for loaded_scene in loaded_scenes.iter() {
info!("REMOVING DynamicScene");
commands
.entity(loaded_scene)
.remove::<Handle<DynamicScene>>()
.remove::<SceneInstance>()
.remove::<CleanupScene>();
commands.remove_resource::<LoadRequested>();
commands.remove_resource::<LoadFirstStageDone>();
}
}

View File

@ -0,0 +1,6 @@
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);

View File

@ -0,0 +1,192 @@
use bevy::prelude::*;
use bevy::tasks::IoTaskPool;
use bevy_gltf_blueprints::{BlueprintName, InBlueprint, Library, SpawnHere};
use std::fs::File;
use std::io::Write;
use std::path::Path;
use crate::{Dynamic, 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 {
return save_requests.len() > 0;
}
#[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 = event.path.clone();
}
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());
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_ron(world.resource::<AppTypeRegistry>())
.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);
}
}*/

View File

@ -72,13 +72,12 @@ pub fn spawn_test(
.spawn((
BluePrintBundle {
blueprint: BlueprintName("Fox".to_string()),
transform: TransformBundle::from_transform(Transform::from_xyz(x, 0.0, y)),
..Default::default()
},
bevy::prelude::Name::from(format!("Spawned{}", name_index)),
// BlueprintName("Health_Pickup".to_string()),
// SpawnHere,
// TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
TransformBundle::from_transform(Transform::from_xyz(x, 0.0, y)),
Velocity {
linvel: Vec3::new(vel_x, vel_y, vel_z),
angvel: Vec3::new(0.0, 0.0, 0.0),

View File

@ -10,9 +10,6 @@ pub use relationships::*;
pub mod physics;
pub use physics::*;
// pub mod save_load;
// pub use save_load::*;
use bevy::prelude::*;
use bevy_gltf_blueprints::*;
@ -23,7 +20,6 @@ impl Plugin for CorePlugin {
LightingPlugin,
CameraPlugin,
PhysicsPlugin,
// SaveLoadPlugin,
BlueprintsPlugin {
library_folder: "models/library".into(),
format: GltfFormat::GLB,

View File

@ -1,218 +0,0 @@
use bevy::prelude::*;
use bevy_gltf_blueprints::{clone_entity::CloneEntity, GameWorldTag, SpawnHere};
use crate::{
assets::GameAssets,
state::{AppState, GameState, InAppRunning},
};
use super::Saveable;
const SCENE_FILE_PATH: &str = "scenes/save.scn.ron";
#[derive(Component, Debug)]
pub struct TempLoadedSceneMarker;
#[derive(Component, Debug)]
pub struct SaveablesToRemove(Vec<(Entity, Name)>);
#[derive(Component, Event)]
pub struct LoadRequest {
pub path: String,
}
pub fn should_load(save_requested_events: EventReader<LoadRequest>) -> bool {
return save_requested_events.len() > 0;
}
pub fn load_prepare(
mut next_app_state: ResMut<NextState<AppState>>,
mut next_game_state: ResMut<NextState<GameState>>,
) {
next_app_state.set(AppState::LoadingGame);
next_game_state.set(GameState::None);
info!("--loading: prepare")
}
/// unload the level recursively
pub fn _unload_world_old(world: &mut World) {
let entities: Vec<Entity> = world
// .query_filtered::<Entity, Or<(With<Save>, With<Unload>)>>()
.query_filtered::<Entity, With<GameWorldTag>>() // our level/world contains this component
.iter(world)
.collect();
for entity in entities {
// Check the entity again in case it was despawned recursively
if world.get_entity(entity).is_some() {
world.entity_mut(entity).despawn_recursive();
}
}
}
pub fn unload_world(mut commands: Commands, gameworlds: Query<Entity, With<GameWorldTag>>) {
for e in gameworlds.iter() {
info!("--loading: despawn old world/level");
commands.entity(e).despawn_recursive();
}
}
// almost identical to setup_game, !!??
pub fn load_world(
mut commands: Commands,
game_assets: Res<GameAssets>,
// scenes: ResMut<Scene>,
) {
info!("--loading: loading world/level");
commands.spawn((
SceneBundle {
scene: game_assets.world.clone(),
..default()
},
bevy::prelude::Name::from("world"),
GameWorldTag,
InAppRunning,
));
}
pub fn load_saved_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((
DynamicSceneBundle {
// Scenes are loaded just like any other asset.
scene: asset_server.load(SCENE_FILE_PATH),
..default()
},
TempLoadedSceneMarker,
));
// commands.entity(world).add_child(child_scene);
info!("--loading: loaded saved scene");
}
pub fn process_loaded_scene(
loaded_scene: Query<(Entity, &Children), With<TempLoadedSceneMarker>>,
named_entities: Query<(Entity, &Name, &Parent)>, // FIXME: very inneficient
mut commands: Commands,
mut game_world: Query<(Entity, &Children), With<GameWorldTag>>,
saveables: Query<(Entity, &Name), With<Saveable>>,
asset_server: Res<AssetServer>,
) {
for (loaded_scene, children) in loaded_scene.iter() {
info!("--loading: post processing loaded scene");
let mut entities_to_load: Vec<(Entity, Name)> = vec![];
for loaded_entity in children.iter() {
if let Ok((source, name, _)) = named_entities.get(*loaded_entity) {
entities_to_load.push((source, name.clone()));
let mut found = false;
for (e, n, p) in named_entities.iter() {
// if we have an entity with the same name as in same file, overwrite
if e != source && name.as_str() == n.as_str() {
// println!("found entity with same name {} {} {:?} {:?}", name, n, source, e);
// source is entity within the newly loaded scene (source), e is within the existing world (destination)
info!("copying data from {:?} to {:?}", source, e);
commands.add(CloneEntity {
source: source,
destination: e,
});
// FIXME: issue with hierarchy & parenting, would be nice to be able to filter out components from CloneEntity
commands.entity(p.get()).add_child(e);
commands.entity(source).despawn_recursive();
found = true;
break;
}
}
// entity not found in the list of existing entities (ie entities that came as part of the level)
// so we spawn a new one
if !found {
info!("generating new entity");
let world = game_world.single_mut();
let world = world.1[0];
let new_entity = commands
.spawn((bevy::prelude::Name::from(name.clone()), SpawnHere))
.id();
commands.add(CloneEntity {
source: source,
destination: new_entity,
});
commands.entity(world).add_child(new_entity);
info!("copying data from {:?} to {:?}", source, new_entity);
}
}
}
commands.spawn(SaveablesToRemove(entities_to_load.clone()));
// if an entity is present in the world but NOT in the saved entities , it should be removed from the world
// ideally this should be run between spawning of the world/level AND spawn_placeholders
// remove the dynamic scene
info!("--loading: DESPAWNING LOADED SCENE");
commands.entity(loaded_scene).despawn_recursive();
asset_server.mark_unused_assets();
asset_server.free_unused_assets();
}
//for saveable in saveables.iter(){
// println!("SAVEABLE BEFORE {:?}", saveable)
//}
}
pub fn final_cleanup(
saveables_to_remove: Query<(Entity, &SaveablesToRemove)>,
mut commands: Commands,
saveables: Query<(Entity, &Name), With<Saveable>>,
mut next_app_state: ResMut<NextState<AppState>>,
mut next_game_state: ResMut<NextState<GameState>>,
) {
if let Ok((e, entities_to_load)) = saveables_to_remove.get_single() {
info!("saveables to remove {:?}", entities_to_load);
for (e, n) in saveables.iter() {
let mut found = false;
println!("SAVEABLE {}", n);
//let entities_to_load = saveables_to_remove.single();
for (en, na) in entities_to_load.0.iter() {
found = na.as_str() == n.as_str();
if found {
break;
}
}
if !found {
println!("REMOVING THIS ONE {}", n);
commands.entity(e).despawn_recursive();
}
}
// if there is a saveable that is NOT in the list of entities to load, despawn it
// despawn list
commands.entity(e).despawn_recursive();
info!("--loading: done, move to InGame state");
// next_app_state.set(AppState::AppRunning);
next_game_state.set(GameState::InGame);
}
}
fn process_loaded_scene_load_alt(
entities: Query<(Entity, &Children), With<TempLoadedSceneMarker>>,
named_entities: Query<(Entity, &Name, &Parent)>, // FIXME: very inneficient
mut commands: Commands,
) {
for (entity, children) in entities.iter() {
let mut entities_to_load: Vec<(Entity, Name)> = vec![];
for saved_source in children.iter() {
if let Ok((source, name, _)) = named_entities.get(*saved_source) {
println!("AAAAAAA {}", name);
entities_to_load.push((source, name.clone()));
}
}
println!("entities to load {:?}", entities_to_load);
commands.entity(entity).despawn_recursive();
}
}

View File

@ -1,70 +0,0 @@
pub mod saveable;
use bevy::asset::free_unused_assets_system;
use bevy_gltf_components::GltfComponentsSet;
pub use saveable::*;
pub mod saving;
pub use saving::*;
pub mod loading;
pub use loading::*;
use bevy::prelude::*;
use bevy::prelude::{App, IntoSystemConfigs, Plugin};
use bevy::utils::Uuid;
use bevy_gltf_blueprints::GltfBlueprintsSet;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum LoadingSet {
Load,
PostLoad,
}
pub struct SaveLoadPlugin;
impl Plugin for SaveLoadPlugin {
fn build(&self, app: &mut App) {
app
.register_type::<Uuid>()
.register_type::<Saveable>()
.add_event::<SaveRequest>()
.add_event::<LoadRequest>()
.configure_sets(
Update,
(LoadingSet::Load, LoadingSet::PostLoad)
.chain()
.before(GltfBlueprintsSet::Spawn)
.before(GltfComponentsSet::Injection)
)
.add_systems(PreUpdate, save_game.run_if(should_save))
.add_systems(Update,
(
load_prepare,
unload_world,
load_world,
load_saved_scene,
// process_loaded_scene
)
.chain()
.run_if(should_load) // .run_if(in_state(AppState::AppRunning))
.in_set(LoadingSet::Load)
)
.add_systems(Update,
(
process_loaded_scene,
apply_deferred,
final_cleanup,
apply_deferred,
free_unused_assets_system
)
.chain()
.in_set(LoadingSet::PostLoad)
)
// .add_systems(Update, bla)
;
}
}

View File

@ -1,137 +0,0 @@
const NEW_SCENE_FILE_PATH:&str="save.scn.ron";
use bevy::ecs::component::Components;
use bevy::ecs::entity::EntityMap;
use serde::{Deserialize, Serialize};
use std::io::Read;
use bevy::scene::serde::SceneDeserializer;
use ron::Deserializer;
use serde::de::DeserializeSeed;
#[derive(Debug, Deserialize)]
struct Components2;
#[derive(Debug, Deserialize)]
struct Fake {
resources: HashMap<u32, String>,
entities: HashMap<u32, Components2>
}
fn ron_test(){
let full_path = "/home/ckaos/projects/grappling-boom-bot/assets/save.ron";
match File::open(full_path) {
Ok(mut file) => {
let mut serialized_scene = Vec::new();
if let Err(why) = file.read_to_end(&mut serialized_scene) {
error!("file read failed: {why:?}");
}
match Deserializer::from_bytes(&serialized_scene) {
Ok(mut deserializer) => {
// deserializer.
let bla:Fake = ron::from_str("(
resources: {},
entities: {}
)").unwrap();
info!("testing {:?}", bla);
info!("YOYO DONE YO !")
}
Err(why) => {
error!("deserializer creation failed: {why:?}");
}
}
}
Err(why) => {
error!("load failed: {why:?}");
}
}
}
fn inject_component_data(world: &mut World, scene: DynamicScene){
let mut entity_map = EntityMap::default();
if let Err(why) = scene.write_to_world(world, &mut entity_map) {
panic!("world write failed: {why:?}");
}
println!("entity map {:?}", entity_map);
// TODO: EntityMap doesn't implement `iter()`
for old_entity in entity_map.keys() {
let entity = entity_map.get(old_entity).unwrap();
info!("entity update required: {old_entity:?} -> {entity:?}");
let e_mut = world
.entity_mut(entity);
}
info!("done loading scene");
}
fn post_load(world: &mut World){
let full_path = "/home/ckaos/projects/grappling-boom-bot/assets/save.ron";
match File::open(full_path) {
Ok(mut file) => {
let mut serialized_scene = Vec::new();
if let Err(why) = file.read_to_end(&mut serialized_scene) {
error!("file read failed: {why:?}");
}
match Deserializer::from_bytes(&serialized_scene) {
Ok(mut deserializer) => {
let result = SceneDeserializer {
type_registry: &world.resource::<AppTypeRegistry>().read(),
}
.deserialize(&mut deserializer);
info!("deserialize done");
match result {
Ok(scene) => {
info!("scene loaded");
// scene.write_to_world(world, entity_map)
// println!("{:?}", scene.entities);
inject_component_data(world, scene);
/*for dyn_ent in scene.entities.iter(){
// let mut query = scene.world.query::<(Entity, &Name, &GltfExtras, &Parent)>();
}*/
}
Err(why) => {
error!("deserialization failed: {why:?}");
}
}
}
Err(why) => {
error!("deserializer creation failed: {why:?}");
}
}
}
Err(why) => {
error!("load failed: {why:?}");
}
}
}
#[derive(Component, Reflect, Debug, Default )]
#[reflect(Component)]
pub struct Hackish;
/// unload saveables
fn unload_saveables(world: &mut World) {
let entities: Vec<Entity> = world
.query_filtered::<Entity, With<Saveable>>()// our level/world contains this component
.iter(world)
.collect();
for entity in entities {
// Check the entity again in case it was despawned recursively
if world.get_entity(entity).is_some() {
info!("despawning");
world.entity_mut(entity).despawn_recursive();
}
}
}

View File

@ -1,14 +0,0 @@
use bevy::prelude::*;
use bevy::utils::Uuid;
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
pub struct Saveable {
id: Uuid,
}
impl Default for Saveable {
fn default() -> Self {
Saveable { id: Uuid::new_v4() }
}
}

View File

@ -1,87 +0,0 @@
use bevy::pbr::{Clusters, VisiblePointLights};
use bevy::render::camera::CameraRenderGraph;
use bevy::render::view::VisibleEntities;
use bevy::tasks::IoTaskPool;
use bevy::{gltf::GltfExtras, prelude::*};
use bevy_rapier3d::prelude::RigidBody;
use std::fs::File;
use std::io::Write;
use crate::core::physics::Collider;
use crate::game::{Pickable, Player};
use super::Saveable;
const NEW_SCENE_FILE_PATH: &str = "save.scn.ron";
#[derive(Component, Event)]
pub struct SaveRequest {
pub path: String,
}
pub fn should_save(
// keycode: Res<Input<KeyCode>>,
save_requested_events: EventReader<SaveRequest>,
) -> bool {
return save_requested_events.len() > 0;
// return keycode.just_pressed(KeyCode::S)
}
pub fn save_game(
world: &mut World,
// save_requested_events: EventReader<SaveRequest>,
) {
info!("saving");
// world.
/*for bli in save_requested_events.iter(){
println!("SAAAAVE TO THISSSSS {:?}", bli.path)
}*/
let saveable_entities: Vec<Entity> = world
.query_filtered::<Entity, With<Saveable>>()
.iter(world)
.collect();
/*let static_entities: Vec<Entity> = world
.query_filtered::<Entity, Without<Saveable>>()
.iter(world)
.collect();*/
println!("saveable entities {}", saveable_entities.len());
let mut scene_builder = DynamicSceneBuilder::from_world(world);
scene_builder
.deny::<Children>()
.deny::<Parent>()
.deny::<ComputedVisibility>()
.deny::<Visibility>()
.deny::<GltfExtras>()
.deny::<GlobalTransform>()
.deny::<Collider>()
.deny::<RigidBody>()
.deny::<Saveable>()
// camera stuff
.deny::<Camera>()
.deny::<CameraRenderGraph>()
.deny::<Camera3d>()
.deny::<Clusters>()
.deny::<VisibleEntities>()
.deny::<VisiblePointLights>()
//.deny::<HasGizmoMarker>()
.extract_entities(saveable_entities.into_iter());
let dyn_scene = scene_builder.build();
let serialized_scene = dyn_scene
.serialize_ron(world.resource::<AppTypeRegistry>())
.unwrap();
#[cfg(not(target_arch = "wasm32"))]
IoTaskPool::get()
.spawn(async move {
// Write the scene RON data to file
File::create(format!("assets/scenes/{NEW_SCENE_FILE_PATH}"))
.and_then(|mut file| file.write(serialized_scene.as_bytes()))
.expect("Error while writing scene to file");
})
.detach();
}

View File

@ -71,13 +71,12 @@ pub fn spawn_test(
.spawn((
BluePrintBundle {
blueprint: BlueprintName("Health_Pickup".to_string()),
transform: TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
..Default::default()
},
bevy::prelude::Name::from(format!("test{}", name_index)),
// BlueprintName("Health_Pickup".to_string()),
// SpawnHere,
// TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
Velocity {
linvel: Vec3::new(vel_x, vel_y, vel_z),
angvel: Vec3::new(0.0, 0.0, 0.0),
@ -88,7 +87,7 @@ pub fn spawn_test(
}
}
pub fn spawn_test_failing(
pub fn spawn_test_unregisted_components(
keycode: Res<Input<KeyCode>>,
mut commands: Commands,
@ -115,13 +114,12 @@ pub fn spawn_test_failing(
.spawn((
BluePrintBundle {
blueprint: BlueprintName("Health_Pickup".to_string()),
transform: TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
..Default::default()
},
bevy::prelude::Name::from(format!("test{}", name_index)),
// BlueprintName("Health_Pickup".to_string()),
// SpawnHere,
// TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
Velocity {
linvel: Vec3::new(vel_x, vel_y, vel_z),
angvel: Vec3::new(0.0, 0.0, 0.0),

View File

@ -104,7 +104,7 @@ impl Plugin for GamePlugin {
player_move_demo, //.run_if(in_state(AppState::Running)),
// test_collision_events
spawn_test,
spawn_test_failing,
spawn_test_unregisted_components,
)
.run_if(in_state(GameState::InGame)),
)

View File

@ -63,13 +63,12 @@ pub fn spawn_test(
.spawn((
BluePrintBundle {
blueprint: BlueprintName("Health_Pickup".to_string()),
transform: TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
..Default::default()
},
bevy::prelude::Name::from(format!("test{}", name_index)),
// BlueprintName("Health_Pickup".to_string()),
// SpawnHere,
// TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
Velocity {
linvel: Vec3::new(vel_x, vel_y, vel_z),
angvel: Vec3::new(0.0, 0.0, 0.0),

View File

@ -8,20 +8,22 @@ pub struct GameAssets {
#[asset(key = "world")]
pub world: Handle<Gltf>,
#[asset(paths("models/library/Container.glb", "models/library/Health_Pickup.glb", "models/library/MagicTeapot.glb",
"models/library/Pillar.glb",
"models/library/Player.glb",
"models/library/Unused_in_level_test.glb"), collection(typed))]
#[asset(
paths(
"models/library/Container.glb",
"models/library/Health_Pickup.glb",
"models/library/MagicTeapot.glb",
"models/library/Pillar.glb",
"models/library/Player.glb",
"models/library/Unused_in_level_test.glb"
),
collection(typed)
)]
pub models: Vec<Handle<Gltf>>,
/*
#[asset(key = "models", collection(typed, mapped))]
pub models: HashMap<String, Handle<Gltf>>,
#[asset(key = "models", collection(typed))]
pub models: Vec<Handle<Gltf>>,*/
}

View File

@ -71,13 +71,12 @@ pub fn spawn_test(
.spawn((
BluePrintBundle {
blueprint: BlueprintName("Health_Pickup".to_string()),
transform: TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
..Default::default()
},
bevy::prelude::Name::from(format!("test{}", name_index)),
// BlueprintName("Health_Pickup".to_string()),
// SpawnHere,
// TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
Velocity {
linvel: Vec3::new(vel_x, vel_y, vel_z),
angvel: Vec3::new(0.0, 0.0, 0.0),
@ -88,7 +87,7 @@ pub fn spawn_test(
}
}
pub fn spawn_test_failing(
pub fn spawn_test_unregisted_components(
keycode: Res<Input<KeyCode>>,
mut commands: Commands,
@ -115,13 +114,10 @@ pub fn spawn_test_failing(
.spawn((
BluePrintBundle {
blueprint: BlueprintName("Health_Pickup".to_string()),
transform: TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
..Default::default()
},
bevy::prelude::Name::from(format!("test{}", name_index)),
// BlueprintName("Health_Pickup".to_string()),
// SpawnHere,
// TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
Velocity {
linvel: Vec3::new(vel_x, vel_y, vel_z),
angvel: Vec3::new(0.0, 0.0, 0.0),

View File

@ -104,7 +104,7 @@ impl Plugin for GamePlugin {
player_move_demo, //.run_if(in_state(AppState::Running)),
// test_collision_events
spawn_test,
spawn_test_failing,
spawn_test_unregisted_components,
)
.run_if(in_state(GameState::InGame)),
)

View File

@ -69,13 +69,12 @@ pub fn spawn_test(
.spawn((
BluePrintBundle {
blueprint: BlueprintName("Health_Pickup".to_string()),
transform: TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
..Default::default()
},
bevy::prelude::Name::from(format!("test{}", name_index)),
// BlueprintName("Health_Pickup".to_string()),
// SpawnHere,
// TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
LinearVelocity(Vec3::new(vel_x, vel_y, vel_z)),
AngularVelocity::ZERO,
))

View File

@ -72,13 +72,12 @@ pub fn spawn_test(
.spawn((
BluePrintBundle {
blueprint: BlueprintName("Watermelon2".to_string()),
transform: TransformBundle::from_transform(Transform::from_xyz(x, 3.0, y)),
..Default::default()
},
bevy::prelude::Name::from(format!("Watermelon{}", name_index)),
// BlueprintName("Health_Pickup".to_string()),
// SpawnHere,
// TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
TransformBundle::from_transform(Transform::from_xyz(x, 3.0, y)),
Velocity {
linvel: Vec3::new(vel_x, vel_y, vel_z),
angvel: Vec3::new(0.0, 0.0, 0.0),

View File

@ -10,9 +10,6 @@ pub use relationships::*;
pub mod physics;
pub use physics::*;
// pub mod save_load;
// pub use save_load::*;
use bevy::prelude::*;
use bevy_gltf_blueprints::*;
@ -23,7 +20,6 @@ impl Plugin for CorePlugin {
LightingPlugin,
CameraPlugin,
PhysicsPlugin,
// SaveLoadPlugin,
BlueprintsPlugin {
library_folder: "models/library".into(),
..Default::default()

View File

@ -1,218 +0,0 @@
use bevy::prelude::*;
use bevy_gltf_blueprints::{clone_entity::CloneEntity, GameWorldTag, SpawnHere};
use crate::{
assets::GameAssets,
state::{AppState, GameState, InAppRunning},
};
use super::Saveable;
const SCENE_FILE_PATH: &str = "scenes/save.scn.ron";
#[derive(Component, Debug)]
pub struct TempLoadedSceneMarker;
#[derive(Component, Debug)]
pub struct SaveablesToRemove(Vec<(Entity, Name)>);
#[derive(Component, Event)]
pub struct LoadRequest {
pub path: String,
}
pub fn should_load(save_requested_events: EventReader<LoadRequest>) -> bool {
return save_requested_events.len() > 0;
}
pub fn load_prepare(
mut next_app_state: ResMut<NextState<AppState>>,
mut next_game_state: ResMut<NextState<GameState>>,
) {
next_app_state.set(AppState::LoadingGame);
next_game_state.set(GameState::None);
info!("--loading: prepare")
}
/// unload the level recursively
pub fn _unload_world_old(world: &mut World) {
let entities: Vec<Entity> = world
// .query_filtered::<Entity, Or<(With<Save>, With<Unload>)>>()
.query_filtered::<Entity, With<GameWorldTag>>() // our level/world contains this component
.iter(world)
.collect();
for entity in entities {
// Check the entity again in case it was despawned recursively
if world.get_entity(entity).is_some() {
world.entity_mut(entity).despawn_recursive();
}
}
}
pub fn unload_world(mut commands: Commands, gameworlds: Query<Entity, With<GameWorldTag>>) {
for e in gameworlds.iter() {
info!("--loading: despawn old world/level");
commands.entity(e).despawn_recursive();
}
}
// almost identical to setup_game, !!??
pub fn load_world(
mut commands: Commands,
game_assets: Res<GameAssets>,
// scenes: ResMut<Scene>,
) {
info!("--loading: loading world/level");
commands.spawn((
SceneBundle {
scene: game_assets.world.clone(),
..default()
},
bevy::prelude::Name::from("world"),
GameWorldTag,
InAppRunning,
));
}
pub fn load_saved_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((
DynamicSceneBundle {
// Scenes are loaded just like any other asset.
scene: asset_server.load(SCENE_FILE_PATH),
..default()
},
TempLoadedSceneMarker,
));
// commands.entity(world).add_child(child_scene);
info!("--loading: loaded saved scene");
}
pub fn process_loaded_scene(
loaded_scene: Query<(Entity, &Children), With<TempLoadedSceneMarker>>,
named_entities: Query<(Entity, &Name, &Parent)>, // FIXME: very inneficient
mut commands: Commands,
mut game_world: Query<(Entity, &Children), With<GameWorldTag>>,
saveables: Query<(Entity, &Name), With<Saveable>>,
asset_server: Res<AssetServer>,
) {
for (loaded_scene, children) in loaded_scene.iter() {
info!("--loading: post processing loaded scene");
let mut entities_to_load: Vec<(Entity, Name)> = vec![];
for loaded_entity in children.iter() {
if let Ok((source, name, _)) = named_entities.get(*loaded_entity) {
entities_to_load.push((source, name.clone()));
let mut found = false;
for (e, n, p) in named_entities.iter() {
// if we have an entity with the same name as in same file, overwrite
if e != source && name.as_str() == n.as_str() {
// println!("found entity with same name {} {} {:?} {:?}", name, n, source, e);
// source is entity within the newly loaded scene (source), e is within the existing world (destination)
info!("copying data from {:?} to {:?}", source, e);
commands.add(CloneEntity {
source: source,
destination: e,
});
// FIXME: issue with hierarchy & parenting, would be nice to be able to filter out components from CloneEntity
commands.entity(p.get()).add_child(e);
commands.entity(source).despawn_recursive();
found = true;
break;
}
}
// entity not found in the list of existing entities (ie entities that came as part of the level)
// so we spawn a new one
if !found {
info!("generating new entity");
let world = game_world.single_mut();
let world = world.1[0];
let new_entity = commands
.spawn((bevy::prelude::Name::from(name.clone()), SpawnHere))
.id();
commands.add(CloneEntity {
source: source,
destination: new_entity,
});
commands.entity(world).add_child(new_entity);
info!("copying data from {:?} to {:?}", source, new_entity);
}
}
}
commands.spawn(SaveablesToRemove(entities_to_load.clone()));
// if an entity is present in the world but NOT in the saved entities , it should be removed from the world
// ideally this should be run between spawning of the world/level AND spawn_placeholders
// remove the dynamic scene
info!("--loading: DESPAWNING LOADED SCENE");
commands.entity(loaded_scene).despawn_recursive();
asset_server.mark_unused_assets();
asset_server.free_unused_assets();
}
//for saveable in saveables.iter(){
// println!("SAVEABLE BEFORE {:?}", saveable)
//}
}
pub fn final_cleanup(
saveables_to_remove: Query<(Entity, &SaveablesToRemove)>,
mut commands: Commands,
saveables: Query<(Entity, &Name), With<Saveable>>,
mut next_app_state: ResMut<NextState<AppState>>,
mut next_game_state: ResMut<NextState<GameState>>,
) {
if let Ok((e, entities_to_load)) = saveables_to_remove.get_single() {
info!("saveables to remove {:?}", entities_to_load);
for (e, n) in saveables.iter() {
let mut found = false;
println!("SAVEABLE {}", n);
//let entities_to_load = saveables_to_remove.single();
for (en, na) in entities_to_load.0.iter() {
found = na.as_str() == n.as_str();
if found {
break;
}
}
if !found {
println!("REMOVING THIS ONE {}", n);
commands.entity(e).despawn_recursive();
}
}
// if there is a saveable that is NOT in the list of entities to load, despawn it
// despawn list
commands.entity(e).despawn_recursive();
info!("--loading: done, move to InGame state");
// next_app_state.set(AppState::AppRunning);
next_game_state.set(GameState::InGame);
}
}
fn process_loaded_scene_load_alt(
entities: Query<(Entity, &Children), With<TempLoadedSceneMarker>>,
named_entities: Query<(Entity, &Name, &Parent)>, // FIXME: very inneficient
mut commands: Commands,
) {
for (entity, children) in entities.iter() {
let mut entities_to_load: Vec<(Entity, Name)> = vec![];
for saved_source in children.iter() {
if let Ok((source, name, _)) = named_entities.get(*saved_source) {
println!("AAAAAAA {}", name);
entities_to_load.push((source, name.clone()));
}
}
println!("entities to load {:?}", entities_to_load);
commands.entity(entity).despawn_recursive();
}
}

View File

@ -1,70 +0,0 @@
pub mod saveable;
use bevy::asset::free_unused_assets_system;
use bevy_gltf_components::GltfComponentsSet;
pub use saveable::*;
pub mod saving;
pub use saving::*;
pub mod loading;
pub use loading::*;
use bevy::prelude::*;
use bevy::prelude::{App, IntoSystemConfigs, Plugin};
use bevy::utils::Uuid;
use bevy_gltf_blueprints::GltfBlueprintsSet;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum LoadingSet {
Load,
PostLoad,
}
pub struct SaveLoadPlugin;
impl Plugin for SaveLoadPlugin {
fn build(&self, app: &mut App) {
app
.register_type::<Uuid>()
.register_type::<Saveable>()
.add_event::<SaveRequest>()
.add_event::<LoadRequest>()
.configure_sets(
Update,
(LoadingSet::Load, LoadingSet::PostLoad)
.chain()
.before(GltfBlueprintsSet::Spawn)
.before(GltfComponentsSet::Injection)
)
.add_systems(PreUpdate, save_game.run_if(should_save))
.add_systems(Update,
(
load_prepare,
unload_world,
load_world,
load_saved_scene,
// process_loaded_scene
)
.chain()
.run_if(should_load) // .run_if(in_state(AppState::AppRunning))
.in_set(LoadingSet::Load)
)
.add_systems(Update,
(
process_loaded_scene,
apply_deferred,
final_cleanup,
apply_deferred,
free_unused_assets_system
)
.chain()
.in_set(LoadingSet::PostLoad)
)
// .add_systems(Update, bla)
;
}
}

View File

@ -1,137 +0,0 @@
const NEW_SCENE_FILE_PATH:&str="save.scn.ron";
use bevy::ecs::component::Components;
use bevy::ecs::entity::EntityMap;
use serde::{Deserialize, Serialize};
use std::io::Read;
use bevy::scene::serde::SceneDeserializer;
use ron::Deserializer;
use serde::de::DeserializeSeed;
#[derive(Debug, Deserialize)]
struct Components2;
#[derive(Debug, Deserialize)]
struct Fake {
resources: HashMap<u32, String>,
entities: HashMap<u32, Components2>
}
fn ron_test(){
let full_path = "/home/ckaos/projects/grappling-boom-bot/assets/save.ron";
match File::open(full_path) {
Ok(mut file) => {
let mut serialized_scene = Vec::new();
if let Err(why) = file.read_to_end(&mut serialized_scene) {
error!("file read failed: {why:?}");
}
match Deserializer::from_bytes(&serialized_scene) {
Ok(mut deserializer) => {
// deserializer.
let bla:Fake = ron::from_str("(
resources: {},
entities: {}
)").unwrap();
info!("testing {:?}", bla);
info!("YOYO DONE YO !")
}
Err(why) => {
error!("deserializer creation failed: {why:?}");
}
}
}
Err(why) => {
error!("load failed: {why:?}");
}
}
}
fn inject_component_data(world: &mut World, scene: DynamicScene){
let mut entity_map = EntityMap::default();
if let Err(why) = scene.write_to_world(world, &mut entity_map) {
panic!("world write failed: {why:?}");
}
println!("entity map {:?}", entity_map);
// TODO: EntityMap doesn't implement `iter()`
for old_entity in entity_map.keys() {
let entity = entity_map.get(old_entity).unwrap();
info!("entity update required: {old_entity:?} -> {entity:?}");
let e_mut = world
.entity_mut(entity);
}
info!("done loading scene");
}
fn post_load(world: &mut World){
let full_path = "/home/ckaos/projects/grappling-boom-bot/assets/save.ron";
match File::open(full_path) {
Ok(mut file) => {
let mut serialized_scene = Vec::new();
if let Err(why) = file.read_to_end(&mut serialized_scene) {
error!("file read failed: {why:?}");
}
match Deserializer::from_bytes(&serialized_scene) {
Ok(mut deserializer) => {
let result = SceneDeserializer {
type_registry: &world.resource::<AppTypeRegistry>().read(),
}
.deserialize(&mut deserializer);
info!("deserialize done");
match result {
Ok(scene) => {
info!("scene loaded");
// scene.write_to_world(world, entity_map)
// println!("{:?}", scene.entities);
inject_component_data(world, scene);
/*for dyn_ent in scene.entities.iter(){
// let mut query = scene.world.query::<(Entity, &Name, &GltfExtras, &Parent)>();
}*/
}
Err(why) => {
error!("deserialization failed: {why:?}");
}
}
}
Err(why) => {
error!("deserializer creation failed: {why:?}");
}
}
}
Err(why) => {
error!("load failed: {why:?}");
}
}
}
#[derive(Component, Reflect, Debug, Default )]
#[reflect(Component)]
pub struct Hackish;
/// unload saveables
fn unload_saveables(world: &mut World) {
let entities: Vec<Entity> = world
.query_filtered::<Entity, With<Saveable>>()// our level/world contains this component
.iter(world)
.collect();
for entity in entities {
// Check the entity again in case it was despawned recursively
if world.get_entity(entity).is_some() {
info!("despawning");
world.entity_mut(entity).despawn_recursive();
}
}
}

View File

@ -1,14 +0,0 @@
use bevy::prelude::*;
use bevy::utils::Uuid;
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
pub struct Saveable {
id: Uuid,
}
impl Default for Saveable {
fn default() -> Self {
Saveable { id: Uuid::new_v4() }
}
}

View File

@ -1,87 +0,0 @@
use bevy::pbr::{Clusters, VisiblePointLights};
use bevy::render::camera::CameraRenderGraph;
use bevy::render::view::VisibleEntities;
use bevy::tasks::IoTaskPool;
use bevy::{gltf::GltfExtras, prelude::*};
use bevy_rapier3d::prelude::RigidBody;
use std::fs::File;
use std::io::Write;
use crate::core::physics::Collider;
use crate::game::{Pickable, Player};
use super::Saveable;
const NEW_SCENE_FILE_PATH: &str = "save.scn.ron";
#[derive(Component, Event)]
pub struct SaveRequest {
pub path: String,
}
pub fn should_save(
// keycode: Res<Input<KeyCode>>,
save_requested_events: EventReader<SaveRequest>,
) -> bool {
return save_requested_events.len() > 0;
// return keycode.just_pressed(KeyCode::S)
}
pub fn save_game(
world: &mut World,
// save_requested_events: EventReader<SaveRequest>,
) {
info!("saving");
// world.
/*for bli in save_requested_events.iter(){
println!("SAAAAVE TO THISSSSS {:?}", bli.path)
}*/
let saveable_entities: Vec<Entity> = world
.query_filtered::<Entity, With<Saveable>>()
.iter(world)
.collect();
/*let static_entities: Vec<Entity> = world
.query_filtered::<Entity, Without<Saveable>>()
.iter(world)
.collect();*/
println!("saveable entities {}", saveable_entities.len());
let mut scene_builder = DynamicSceneBuilder::from_world(world);
scene_builder
.deny::<Children>()
.deny::<Parent>()
.deny::<ComputedVisibility>()
.deny::<Visibility>()
.deny::<GltfExtras>()
.deny::<GlobalTransform>()
.deny::<Collider>()
.deny::<RigidBody>()
.deny::<Saveable>()
// camera stuff
.deny::<Camera>()
.deny::<CameraRenderGraph>()
.deny::<Camera3d>()
.deny::<Clusters>()
.deny::<VisibleEntities>()
.deny::<VisiblePointLights>()
//.deny::<HasGizmoMarker>()
.extract_entities(saveable_entities.into_iter());
let dyn_scene = scene_builder.build();
let serialized_scene = dyn_scene
.serialize_ron(world.resource::<AppTypeRegistry>())
.unwrap();
#[cfg(not(target_arch = "wasm32"))]
IoTaskPool::get()
.spawn(async move {
// Write the scene RON data to file
File::create(format!("assets/scenes/{NEW_SCENE_FILE_PATH}"))
.and_then(|mut file| file.write(serialized_scene.as_bytes()))
.expect("Error while writing scene to file");
})
.detach();
}

View File

@ -67,13 +67,12 @@ pub fn spawn_test(
.spawn((
BluePrintBundle {
blueprint: BlueprintName("Health_Pickup".to_string()),
transform: TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
..Default::default()
},
bevy::prelude::Name::from(format!("test{}", name_index)),
// BlueprintName("Health_Pickup".to_string()),
// SpawnHere,
// TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
Velocity {
linvel: Vec3::new(vel_x, vel_y, vel_z),
angvel: Vec3::new(0.0, 0.0, 0.0),

View File

@ -10,9 +10,6 @@ pub use relationships::*;
pub mod physics;
pub use physics::*;
// pub mod save_load;
// pub use save_load::*;
use bevy::prelude::*;
use bevy_gltf_blueprints::*;
@ -23,7 +20,6 @@ impl Plugin for CorePlugin {
LightingPlugin,
CameraPlugin,
PhysicsPlugin,
// SaveLoadPlugin,
BlueprintsPlugin {
library_folder: "models/library".into(),
material_library: true,

View File

@ -1,218 +0,0 @@
use bevy::prelude::*;
use bevy_gltf_blueprints::{clone_entity::CloneEntity, GameWorldTag, SpawnHere};
use crate::{
assets::GameAssets,
state::{AppState, GameState, InAppRunning},
};
use super::Saveable;
const SCENE_FILE_PATH: &str = "scenes/save.scn.ron";
#[derive(Component, Debug)]
pub struct TempLoadedSceneMarker;
#[derive(Component, Debug)]
pub struct SaveablesToRemove(Vec<(Entity, Name)>);
#[derive(Component, Event)]
pub struct LoadRequest {
pub path: String,
}
pub fn should_load(save_requested_events: EventReader<LoadRequest>) -> bool {
return save_requested_events.len() > 0;
}
pub fn load_prepare(
mut next_app_state: ResMut<NextState<AppState>>,
mut next_game_state: ResMut<NextState<GameState>>,
) {
next_app_state.set(AppState::LoadingGame);
next_game_state.set(GameState::None);
info!("--loading: prepare")
}
/// unload the level recursively
pub fn _unload_world_old(world: &mut World) {
let entities: Vec<Entity> = world
// .query_filtered::<Entity, Or<(With<Save>, With<Unload>)>>()
.query_filtered::<Entity, With<GameWorldTag>>() // our level/world contains this component
.iter(world)
.collect();
for entity in entities {
// Check the entity again in case it was despawned recursively
if world.get_entity(entity).is_some() {
world.entity_mut(entity).despawn_recursive();
}
}
}
pub fn unload_world(mut commands: Commands, gameworlds: Query<Entity, With<GameWorldTag>>) {
for e in gameworlds.iter() {
info!("--loading: despawn old world/level");
commands.entity(e).despawn_recursive();
}
}
// almost identical to setup_game, !!??
pub fn load_world(
mut commands: Commands,
game_assets: Res<GameAssets>,
// scenes: ResMut<Scene>,
) {
info!("--loading: loading world/level");
commands.spawn((
SceneBundle {
scene: game_assets.world.clone(),
..default()
},
bevy::prelude::Name::from("world"),
GameWorldTag,
InAppRunning,
));
}
pub fn load_saved_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((
DynamicSceneBundle {
// Scenes are loaded just like any other asset.
scene: asset_server.load(SCENE_FILE_PATH),
..default()
},
TempLoadedSceneMarker,
));
// commands.entity(world).add_child(child_scene);
info!("--loading: loaded saved scene");
}
pub fn process_loaded_scene(
loaded_scene: Query<(Entity, &Children), With<TempLoadedSceneMarker>>,
named_entities: Query<(Entity, &Name, &Parent)>, // FIXME: very inneficient
mut commands: Commands,
mut game_world: Query<(Entity, &Children), With<GameWorldTag>>,
saveables: Query<(Entity, &Name), With<Saveable>>,
asset_server: Res<AssetServer>,
) {
for (loaded_scene, children) in loaded_scene.iter() {
info!("--loading: post processing loaded scene");
let mut entities_to_load: Vec<(Entity, Name)> = vec![];
for loaded_entity in children.iter() {
if let Ok((source, name, _)) = named_entities.get(*loaded_entity) {
entities_to_load.push((source, name.clone()));
let mut found = false;
for (e, n, p) in named_entities.iter() {
// if we have an entity with the same name as in same file, overwrite
if e != source && name.as_str() == n.as_str() {
// println!("found entity with same name {} {} {:?} {:?}", name, n, source, e);
// source is entity within the newly loaded scene (source), e is within the existing world (destination)
info!("copying data from {:?} to {:?}", source, e);
commands.add(CloneEntity {
source: source,
destination: e,
});
// FIXME: issue with hierarchy & parenting, would be nice to be able to filter out components from CloneEntity
commands.entity(p.get()).add_child(e);
commands.entity(source).despawn_recursive();
found = true;
break;
}
}
// entity not found in the list of existing entities (ie entities that came as part of the level)
// so we spawn a new one
if !found {
info!("generating new entity");
let world = game_world.single_mut();
let world = world.1[0];
let new_entity = commands
.spawn((bevy::prelude::Name::from(name.clone()), SpawnHere))
.id();
commands.add(CloneEntity {
source: source,
destination: new_entity,
});
commands.entity(world).add_child(new_entity);
info!("copying data from {:?} to {:?}", source, new_entity);
}
}
}
commands.spawn(SaveablesToRemove(entities_to_load.clone()));
// if an entity is present in the world but NOT in the saved entities , it should be removed from the world
// ideally this should be run between spawning of the world/level AND spawn_placeholders
// remove the dynamic scene
info!("--loading: DESPAWNING LOADED SCENE");
commands.entity(loaded_scene).despawn_recursive();
asset_server.mark_unused_assets();
asset_server.free_unused_assets();
}
//for saveable in saveables.iter(){
// println!("SAVEABLE BEFORE {:?}", saveable)
//}
}
pub fn final_cleanup(
saveables_to_remove: Query<(Entity, &SaveablesToRemove)>,
mut commands: Commands,
saveables: Query<(Entity, &Name), With<Saveable>>,
mut next_app_state: ResMut<NextState<AppState>>,
mut next_game_state: ResMut<NextState<GameState>>,
) {
if let Ok((e, entities_to_load)) = saveables_to_remove.get_single() {
info!("saveables to remove {:?}", entities_to_load);
for (e, n) in saveables.iter() {
let mut found = false;
println!("SAVEABLE {}", n);
//let entities_to_load = saveables_to_remove.single();
for (en, na) in entities_to_load.0.iter() {
found = na.as_str() == n.as_str();
if found {
break;
}
}
if !found {
println!("REMOVING THIS ONE {}", n);
commands.entity(e).despawn_recursive();
}
}
// if there is a saveable that is NOT in the list of entities to load, despawn it
// despawn list
commands.entity(e).despawn_recursive();
info!("--loading: done, move to InGame state");
// next_app_state.set(AppState::AppRunning);
next_game_state.set(GameState::InGame);
}
}
fn process_loaded_scene_load_alt(
entities: Query<(Entity, &Children), With<TempLoadedSceneMarker>>,
named_entities: Query<(Entity, &Name, &Parent)>, // FIXME: very inneficient
mut commands: Commands,
) {
for (entity, children) in entities.iter() {
let mut entities_to_load: Vec<(Entity, Name)> = vec![];
for saved_source in children.iter() {
if let Ok((source, name, _)) = named_entities.get(*saved_source) {
println!("AAAAAAA {}", name);
entities_to_load.push((source, name.clone()));
}
}
println!("entities to load {:?}", entities_to_load);
commands.entity(entity).despawn_recursive();
}
}

View File

@ -1,70 +0,0 @@
pub mod saveable;
use bevy::asset::free_unused_assets_system;
use bevy_gltf_components::GltfComponentsSet;
pub use saveable::*;
pub mod saving;
pub use saving::*;
pub mod loading;
pub use loading::*;
use bevy::prelude::*;
use bevy::prelude::{App, IntoSystemConfigs, Plugin};
use bevy::utils::Uuid;
use bevy_gltf_blueprints::GltfBlueprintsSet;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum LoadingSet {
Load,
PostLoad,
}
pub struct SaveLoadPlugin;
impl Plugin for SaveLoadPlugin {
fn build(&self, app: &mut App) {
app
.register_type::<Uuid>()
.register_type::<Saveable>()
.add_event::<SaveRequest>()
.add_event::<LoadRequest>()
.configure_sets(
Update,
(LoadingSet::Load, LoadingSet::PostLoad)
.chain()
.before(GltfBlueprintsSet::Spawn)
.before(GltfComponentsSet::Injection)
)
.add_systems(PreUpdate, save_game.run_if(should_save))
.add_systems(Update,
(
load_prepare,
unload_world,
load_world,
load_saved_scene,
// process_loaded_scene
)
.chain()
.run_if(should_load) // .run_if(in_state(AppState::AppRunning))
.in_set(LoadingSet::Load)
)
.add_systems(Update,
(
process_loaded_scene,
apply_deferred,
final_cleanup,
apply_deferred,
free_unused_assets_system
)
.chain()
.in_set(LoadingSet::PostLoad)
)
// .add_systems(Update, bla)
;
}
}

View File

@ -1,137 +0,0 @@
const NEW_SCENE_FILE_PATH:&str="save.scn.ron";
use bevy::ecs::component::Components;
use bevy::ecs::entity::EntityMap;
use serde::{Deserialize, Serialize};
use std::io::Read;
use bevy::scene::serde::SceneDeserializer;
use ron::Deserializer;
use serde::de::DeserializeSeed;
#[derive(Debug, Deserialize)]
struct Components2;
#[derive(Debug, Deserialize)]
struct Fake {
resources: HashMap<u32, String>,
entities: HashMap<u32, Components2>
}
fn ron_test(){
let full_path = "/home/ckaos/projects/grappling-boom-bot/assets/save.ron";
match File::open(full_path) {
Ok(mut file) => {
let mut serialized_scene = Vec::new();
if let Err(why) = file.read_to_end(&mut serialized_scene) {
error!("file read failed: {why:?}");
}
match Deserializer::from_bytes(&serialized_scene) {
Ok(mut deserializer) => {
// deserializer.
let bla:Fake = ron::from_str("(
resources: {},
entities: {}
)").unwrap();
info!("testing {:?}", bla);
info!("YOYO DONE YO !")
}
Err(why) => {
error!("deserializer creation failed: {why:?}");
}
}
}
Err(why) => {
error!("load failed: {why:?}");
}
}
}
fn inject_component_data(world: &mut World, scene: DynamicScene){
let mut entity_map = EntityMap::default();
if let Err(why) = scene.write_to_world(world, &mut entity_map) {
panic!("world write failed: {why:?}");
}
println!("entity map {:?}", entity_map);
// TODO: EntityMap doesn't implement `iter()`
for old_entity in entity_map.keys() {
let entity = entity_map.get(old_entity).unwrap();
info!("entity update required: {old_entity:?} -> {entity:?}");
let e_mut = world
.entity_mut(entity);
}
info!("done loading scene");
}
fn post_load(world: &mut World){
let full_path = "/home/ckaos/projects/grappling-boom-bot/assets/save.ron";
match File::open(full_path) {
Ok(mut file) => {
let mut serialized_scene = Vec::new();
if let Err(why) = file.read_to_end(&mut serialized_scene) {
error!("file read failed: {why:?}");
}
match Deserializer::from_bytes(&serialized_scene) {
Ok(mut deserializer) => {
let result = SceneDeserializer {
type_registry: &world.resource::<AppTypeRegistry>().read(),
}
.deserialize(&mut deserializer);
info!("deserialize done");
match result {
Ok(scene) => {
info!("scene loaded");
// scene.write_to_world(world, entity_map)
// println!("{:?}", scene.entities);
inject_component_data(world, scene);
/*for dyn_ent in scene.entities.iter(){
// let mut query = scene.world.query::<(Entity, &Name, &GltfExtras, &Parent)>();
}*/
}
Err(why) => {
error!("deserialization failed: {why:?}");
}
}
}
Err(why) => {
error!("deserializer creation failed: {why:?}");
}
}
}
Err(why) => {
error!("load failed: {why:?}");
}
}
}
#[derive(Component, Reflect, Debug, Default )]
#[reflect(Component)]
pub struct Hackish;
/// unload saveables
fn unload_saveables(world: &mut World) {
let entities: Vec<Entity> = world
.query_filtered::<Entity, With<Saveable>>()// our level/world contains this component
.iter(world)
.collect();
for entity in entities {
// Check the entity again in case it was despawned recursively
if world.get_entity(entity).is_some() {
info!("despawning");
world.entity_mut(entity).despawn_recursive();
}
}
}

View File

@ -1,14 +0,0 @@
use bevy::prelude::*;
use bevy::utils::Uuid;
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
pub struct Saveable {
id: Uuid,
}
impl Default for Saveable {
fn default() -> Self {
Saveable { id: Uuid::new_v4() }
}
}

View File

@ -1,87 +0,0 @@
use bevy::pbr::{Clusters, VisiblePointLights};
use bevy::render::camera::CameraRenderGraph;
use bevy::render::view::VisibleEntities;
use bevy::tasks::IoTaskPool;
use bevy::{gltf::GltfExtras, prelude::*};
use bevy_rapier3d::prelude::RigidBody;
use std::fs::File;
use std::io::Write;
use crate::core::physics::Collider;
use crate::game::{Pickable, Player};
use super::Saveable;
const NEW_SCENE_FILE_PATH: &str = "save.scn.ron";
#[derive(Component, Event)]
pub struct SaveRequest {
pub path: String,
}
pub fn should_save(
// keycode: Res<Input<KeyCode>>,
save_requested_events: EventReader<SaveRequest>,
) -> bool {
return save_requested_events.len() > 0;
// return keycode.just_pressed(KeyCode::S)
}
pub fn save_game(
world: &mut World,
// save_requested_events: EventReader<SaveRequest>,
) {
info!("saving");
// world.
/*for bli in save_requested_events.iter(){
println!("SAAAAVE TO THISSSSS {:?}", bli.path)
}*/
let saveable_entities: Vec<Entity> = world
.query_filtered::<Entity, With<Saveable>>()
.iter(world)
.collect();
/*let static_entities: Vec<Entity> = world
.query_filtered::<Entity, Without<Saveable>>()
.iter(world)
.collect();*/
println!("saveable entities {}", saveable_entities.len());
let mut scene_builder = DynamicSceneBuilder::from_world(world);
scene_builder
.deny::<Children>()
.deny::<Parent>()
.deny::<ComputedVisibility>()
.deny::<Visibility>()
.deny::<GltfExtras>()
.deny::<GlobalTransform>()
.deny::<Collider>()
.deny::<RigidBody>()
.deny::<Saveable>()
// camera stuff
.deny::<Camera>()
.deny::<CameraRenderGraph>()
.deny::<Camera3d>()
.deny::<Clusters>()
.deny::<VisibleEntities>()
.deny::<VisiblePointLights>()
//.deny::<HasGizmoMarker>()
.extract_entities(saveable_entities.into_iter());
let dyn_scene = scene_builder.build();
let serialized_scene = dyn_scene
.serialize_ron(world.resource::<AppTypeRegistry>())
.unwrap();
#[cfg(not(target_arch = "wasm32"))]
IoTaskPool::get()
.spawn(async move {
// Write the scene RON data to file
File::create(format!("assets/scenes/{NEW_SCENE_FILE_PATH}"))
.and_then(|mut file| file.write(serialized_scene.as_bytes()))
.expect("Error while writing scene to file");
})
.detach();
}

View File

@ -67,13 +67,12 @@ pub fn spawn_test(
.spawn((
BluePrintBundle {
blueprint: BlueprintName("Health_Pickup".to_string()),
transform: TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
..Default::default()
},
bevy::prelude::Name::from(format!("test{}", name_index)),
// BlueprintName("Health_Pickup".to_string()),
// SpawnHere,
// TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
Velocity {
linvel: Vec3::new(vel_x, vel_y, vel_z),
angvel: Vec3::new(0.0, 0.0, 0.0),

View File

@ -30,7 +30,6 @@ pub fn trigger_level_transition(
// we need to accomodate for the fact that the collider may be a child of the level transition (FIXME: is this a missunderstanding on my part about rapier child colliders ?)
let entity1_parent = parents.get(*entity1).unwrap();
let entity2_parent = parents.get(*entity2).unwrap();
if level_transition_triggers.get(*entity1).is_ok()
|| level_transition_triggers.get(*entity2).is_ok()
@ -51,10 +50,13 @@ pub fn trigger_level_transition(
level_transition_triggers.get(entity2_parent.get()).unwrap();
}
if players.get(*entity1).is_ok() || players.get(entity1_parent.get()).is_ok() || players.get(*entity2).is_ok() || players.get(entity2_parent.get()).is_ok() {
if players.get(*entity1).is_ok()
|| players.get(entity1_parent.get()).is_ok()
|| players.get(*entity2).is_ok()
|| players.get(entity2_parent.get()).is_ok()
{
println!("one entity is the player, we can enter")
}
else {
} else {
// if none of our entities is a player, bail out, as only entities with player components should trigger a transition
return;
}

View File

@ -10,9 +10,6 @@ pub use relationships::*;
pub mod physics;
pub use physics::*;
// pub mod save_load;
// pub use save_load::*;
use bevy::prelude::*;
use bevy_gltf_blueprints::*;
@ -23,7 +20,6 @@ impl Plugin for CorePlugin {
LightingPlugin,
CameraPlugin,
PhysicsPlugin,
// SaveLoadPlugin,
BlueprintsPlugin {
library_folder: "models/library".into(),
format: GltfFormat::GLB,

View File

@ -1,218 +0,0 @@
use bevy::prelude::*;
use bevy_gltf_blueprints::{clone_entity::CloneEntity, GameWorldTag, SpawnHere};
use crate::{
assets::GameAssets,
state::{AppState, GameState, InAppRunning},
};
use super::Saveable;
const SCENE_FILE_PATH: &str = "scenes/save.scn.ron";
#[derive(Component, Debug)]
pub struct TempLoadedSceneMarker;
#[derive(Component, Debug)]
pub struct SaveablesToRemove(Vec<(Entity, Name)>);
#[derive(Component, Event)]
pub struct LoadRequest {
pub path: String,
}
pub fn should_load(save_requested_events: EventReader<LoadRequest>) -> bool {
return save_requested_events.len() > 0;
}
pub fn load_prepare(
mut next_app_state: ResMut<NextState<AppState>>,
mut next_game_state: ResMut<NextState<GameState>>,
) {
next_app_state.set(AppState::LoadingGame);
next_game_state.set(GameState::None);
info!("--loading: prepare")
}
/// unload the level recursively
pub fn _unload_world_old(world: &mut World) {
let entities: Vec<Entity> = world
// .query_filtered::<Entity, Or<(With<Save>, With<Unload>)>>()
.query_filtered::<Entity, With<GameWorldTag>>() // our level/world contains this component
.iter(world)
.collect();
for entity in entities {
// Check the entity again in case it was despawned recursively
if world.get_entity(entity).is_some() {
world.entity_mut(entity).despawn_recursive();
}
}
}
pub fn unload_world(mut commands: Commands, gameworlds: Query<Entity, With<GameWorldTag>>) {
for e in gameworlds.iter() {
info!("--loading: despawn old world/level");
commands.entity(e).despawn_recursive();
}
}
// almost identical to setup_game, !!??
pub fn load_world(
mut commands: Commands,
game_assets: Res<GameAssets>,
// scenes: ResMut<Scene>,
) {
info!("--loading: loading world/level");
commands.spawn((
SceneBundle {
scene: game_assets.world.clone(),
..default()
},
bevy::prelude::Name::from("world"),
GameWorldTag,
InAppRunning,
));
}
pub fn load_saved_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((
DynamicSceneBundle {
// Scenes are loaded just like any other asset.
scene: asset_server.load(SCENE_FILE_PATH),
..default()
},
TempLoadedSceneMarker,
));
// commands.entity(world).add_child(child_scene);
info!("--loading: loaded saved scene");
}
pub fn process_loaded_scene(
loaded_scene: Query<(Entity, &Children), With<TempLoadedSceneMarker>>,
named_entities: Query<(Entity, &Name, &Parent)>, // FIXME: very inneficient
mut commands: Commands,
mut game_world: Query<(Entity, &Children), With<GameWorldTag>>,
saveables: Query<(Entity, &Name), With<Saveable>>,
asset_server: Res<AssetServer>,
) {
for (loaded_scene, children) in loaded_scene.iter() {
info!("--loading: post processing loaded scene");
let mut entities_to_load: Vec<(Entity, Name)> = vec![];
for loaded_entity in children.iter() {
if let Ok((source, name, _)) = named_entities.get(*loaded_entity) {
entities_to_load.push((source, name.clone()));
let mut found = false;
for (e, n, p) in named_entities.iter() {
// if we have an entity with the same name as in same file, overwrite
if e != source && name.as_str() == n.as_str() {
// println!("found entity with same name {} {} {:?} {:?}", name, n, source, e);
// source is entity within the newly loaded scene (source), e is within the existing world (destination)
info!("copying data from {:?} to {:?}", source, e);
commands.add(CloneEntity {
source: source,
destination: e,
});
// FIXME: issue with hierarchy & parenting, would be nice to be able to filter out components from CloneEntity
commands.entity(p.get()).add_child(e);
commands.entity(source).despawn_recursive();
found = true;
break;
}
}
// entity not found in the list of existing entities (ie entities that came as part of the level)
// so we spawn a new one
if !found {
info!("generating new entity");
let world = game_world.single_mut();
let world = world.1[0];
let new_entity = commands
.spawn((bevy::prelude::Name::from(name.clone()), SpawnHere))
.id();
commands.add(CloneEntity {
source: source,
destination: new_entity,
});
commands.entity(world).add_child(new_entity);
info!("copying data from {:?} to {:?}", source, new_entity);
}
}
}
commands.spawn(SaveablesToRemove(entities_to_load.clone()));
// if an entity is present in the world but NOT in the saved entities , it should be removed from the world
// ideally this should be run between spawning of the world/level AND spawn_placeholders
// remove the dynamic scene
info!("--loading: DESPAWNING LOADED SCENE");
commands.entity(loaded_scene).despawn_recursive();
asset_server.mark_unused_assets();
asset_server.free_unused_assets();
}
//for saveable in saveables.iter(){
// println!("SAVEABLE BEFORE {:?}", saveable)
//}
}
pub fn final_cleanup(
saveables_to_remove: Query<(Entity, &SaveablesToRemove)>,
mut commands: Commands,
saveables: Query<(Entity, &Name), With<Saveable>>,
mut next_app_state: ResMut<NextState<AppState>>,
mut next_game_state: ResMut<NextState<GameState>>,
) {
if let Ok((e, entities_to_load)) = saveables_to_remove.get_single() {
info!("saveables to remove {:?}", entities_to_load);
for (e, n) in saveables.iter() {
let mut found = false;
println!("SAVEABLE {}", n);
//let entities_to_load = saveables_to_remove.single();
for (en, na) in entities_to_load.0.iter() {
found = na.as_str() == n.as_str();
if found {
break;
}
}
if !found {
println!("REMOVING THIS ONE {}", n);
commands.entity(e).despawn_recursive();
}
}
// if there is a saveable that is NOT in the list of entities to load, despawn it
// despawn list
commands.entity(e).despawn_recursive();
info!("--loading: done, move to InGame state");
// next_app_state.set(AppState::AppRunning);
next_game_state.set(GameState::InGame);
}
}
fn process_loaded_scene_load_alt(
entities: Query<(Entity, &Children), With<TempLoadedSceneMarker>>,
named_entities: Query<(Entity, &Name, &Parent)>, // FIXME: very inneficient
mut commands: Commands,
) {
for (entity, children) in entities.iter() {
let mut entities_to_load: Vec<(Entity, Name)> = vec![];
for saved_source in children.iter() {
if let Ok((source, name, _)) = named_entities.get(*saved_source) {
println!("AAAAAAA {}", name);
entities_to_load.push((source, name.clone()));
}
}
println!("entities to load {:?}", entities_to_load);
commands.entity(entity).despawn_recursive();
}
}

View File

@ -1,70 +0,0 @@
pub mod saveable;
use bevy::asset::free_unused_assets_system;
use bevy_gltf_components::GltfComponentsSet;
pub use saveable::*;
pub mod saving;
pub use saving::*;
pub mod loading;
pub use loading::*;
use bevy::prelude::*;
use bevy::prelude::{App, IntoSystemConfigs, Plugin};
use bevy::utils::Uuid;
use bevy_gltf_blueprints::GltfBlueprintsSet;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum LoadingSet {
Load,
PostLoad,
}
pub struct SaveLoadPlugin;
impl Plugin for SaveLoadPlugin {
fn build(&self, app: &mut App) {
app
.register_type::<Uuid>()
.register_type::<Saveable>()
.add_event::<SaveRequest>()
.add_event::<LoadRequest>()
.configure_sets(
Update,
(LoadingSet::Load, LoadingSet::PostLoad)
.chain()
.before(GltfBlueprintsSet::Spawn)
.before(GltfComponentsSet::Injection)
)
.add_systems(PreUpdate, save_game.run_if(should_save))
.add_systems(Update,
(
load_prepare,
unload_world,
load_world,
load_saved_scene,
// process_loaded_scene
)
.chain()
.run_if(should_load) // .run_if(in_state(AppState::AppRunning))
.in_set(LoadingSet::Load)
)
.add_systems(Update,
(
process_loaded_scene,
apply_deferred,
final_cleanup,
apply_deferred,
free_unused_assets_system
)
.chain()
.in_set(LoadingSet::PostLoad)
)
// .add_systems(Update, bla)
;
}
}

View File

@ -1,137 +0,0 @@
const NEW_SCENE_FILE_PATH:&str="save.scn.ron";
use bevy::ecs::component::Components;
use bevy::ecs::entity::EntityMap;
use serde::{Deserialize, Serialize};
use std::io::Read;
use bevy::scene::serde::SceneDeserializer;
use ron::Deserializer;
use serde::de::DeserializeSeed;
#[derive(Debug, Deserialize)]
struct Components2;
#[derive(Debug, Deserialize)]
struct Fake {
resources: HashMap<u32, String>,
entities: HashMap<u32, Components2>
}
fn ron_test(){
let full_path = "/home/ckaos/projects/grappling-boom-bot/assets/save.ron";
match File::open(full_path) {
Ok(mut file) => {
let mut serialized_scene = Vec::new();
if let Err(why) = file.read_to_end(&mut serialized_scene) {
error!("file read failed: {why:?}");
}
match Deserializer::from_bytes(&serialized_scene) {
Ok(mut deserializer) => {
// deserializer.
let bla:Fake = ron::from_str("(
resources: {},
entities: {}
)").unwrap();
info!("testing {:?}", bla);
info!("YOYO DONE YO !")
}
Err(why) => {
error!("deserializer creation failed: {why:?}");
}
}
}
Err(why) => {
error!("load failed: {why:?}");
}
}
}
fn inject_component_data(world: &mut World, scene: DynamicScene){
let mut entity_map = EntityMap::default();
if let Err(why) = scene.write_to_world(world, &mut entity_map) {
panic!("world write failed: {why:?}");
}
println!("entity map {:?}", entity_map);
// TODO: EntityMap doesn't implement `iter()`
for old_entity in entity_map.keys() {
let entity = entity_map.get(old_entity).unwrap();
info!("entity update required: {old_entity:?} -> {entity:?}");
let e_mut = world
.entity_mut(entity);
}
info!("done loading scene");
}
fn post_load(world: &mut World){
let full_path = "/home/ckaos/projects/grappling-boom-bot/assets/save.ron";
match File::open(full_path) {
Ok(mut file) => {
let mut serialized_scene = Vec::new();
if let Err(why) = file.read_to_end(&mut serialized_scene) {
error!("file read failed: {why:?}");
}
match Deserializer::from_bytes(&serialized_scene) {
Ok(mut deserializer) => {
let result = SceneDeserializer {
type_registry: &world.resource::<AppTypeRegistry>().read(),
}
.deserialize(&mut deserializer);
info!("deserialize done");
match result {
Ok(scene) => {
info!("scene loaded");
// scene.write_to_world(world, entity_map)
// println!("{:?}", scene.entities);
inject_component_data(world, scene);
/*for dyn_ent in scene.entities.iter(){
// let mut query = scene.world.query::<(Entity, &Name, &GltfExtras, &Parent)>();
}*/
}
Err(why) => {
error!("deserialization failed: {why:?}");
}
}
}
Err(why) => {
error!("deserializer creation failed: {why:?}");
}
}
}
Err(why) => {
error!("load failed: {why:?}");
}
}
}
#[derive(Component, Reflect, Debug, Default )]
#[reflect(Component)]
pub struct Hackish;
/// unload saveables
fn unload_saveables(world: &mut World) {
let entities: Vec<Entity> = world
.query_filtered::<Entity, With<Saveable>>()// our level/world contains this component
.iter(world)
.collect();
for entity in entities {
// Check the entity again in case it was despawned recursively
if world.get_entity(entity).is_some() {
info!("despawning");
world.entity_mut(entity).despawn_recursive();
}
}
}

View File

@ -1,14 +0,0 @@
use bevy::prelude::*;
use bevy::utils::Uuid;
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
pub struct Saveable {
id: Uuid,
}
impl Default for Saveable {
fn default() -> Self {
Saveable { id: Uuid::new_v4() }
}
}

View File

@ -1,87 +0,0 @@
use bevy::pbr::{Clusters, VisiblePointLights};
use bevy::render::camera::CameraRenderGraph;
use bevy::render::view::VisibleEntities;
use bevy::tasks::IoTaskPool;
use bevy::{gltf::GltfExtras, prelude::*};
use bevy_rapier3d::prelude::RigidBody;
use std::fs::File;
use std::io::Write;
use crate::core::physics::Collider;
use crate::game::{Pickable, Player};
use super::Saveable;
const NEW_SCENE_FILE_PATH: &str = "save.scn.ron";
#[derive(Component, Event)]
pub struct SaveRequest {
pub path: String,
}
pub fn should_save(
// keycode: Res<Input<KeyCode>>,
save_requested_events: EventReader<SaveRequest>,
) -> bool {
return save_requested_events.len() > 0;
// return keycode.just_pressed(KeyCode::S)
}
pub fn save_game(
world: &mut World,
// save_requested_events: EventReader<SaveRequest>,
) {
info!("saving");
// world.
/*for bli in save_requested_events.iter(){
println!("SAAAAVE TO THISSSSS {:?}", bli.path)
}*/
let saveable_entities: Vec<Entity> = world
.query_filtered::<Entity, With<Saveable>>()
.iter(world)
.collect();
/*let static_entities: Vec<Entity> = world
.query_filtered::<Entity, Without<Saveable>>()
.iter(world)
.collect();*/
println!("saveable entities {}", saveable_entities.len());
let mut scene_builder = DynamicSceneBuilder::from_world(world);
scene_builder
.deny::<Children>()
.deny::<Parent>()
.deny::<ComputedVisibility>()
.deny::<Visibility>()
.deny::<GltfExtras>()
.deny::<GlobalTransform>()
.deny::<Collider>()
.deny::<RigidBody>()
.deny::<Saveable>()
// camera stuff
.deny::<Camera>()
.deny::<CameraRenderGraph>()
.deny::<Camera3d>()
.deny::<Clusters>()
.deny::<VisibleEntities>()
.deny::<VisiblePointLights>()
//.deny::<HasGizmoMarker>()
.extract_entities(saveable_entities.into_iter());
let dyn_scene = scene_builder.build();
let serialized_scene = dyn_scene
.serialize_ron(world.resource::<AppTypeRegistry>())
.unwrap();
#[cfg(not(target_arch = "wasm32"))]
IoTaskPool::get()
.spawn(async move {
// Write the scene RON data to file
File::create(format!("assets/scenes/{NEW_SCENE_FILE_PATH}"))
.and_then(|mut file| file.write(serialized_scene.as_bytes()))
.expect("Error while writing scene to file");
})
.detach();
}

View File

@ -67,13 +67,12 @@ pub fn spawn_test(
.spawn((
BluePrintBundle {
blueprint: BlueprintName("Health_Pickup".to_string()),
transform: TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
..Default::default()
},
bevy::prelude::Name::from(format!("test{}", name_index)),
// BlueprintName("Health_Pickup".to_string()),
// SpawnHere,
// TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
Velocity {
linvel: Vec3::new(vel_x, vel_y, vel_z),
angvel: Vec3::new(0.0, 0.0, 0.0),

View File

@ -0,0 +1,17 @@
[package]
name = "bevy_gltf_save_load_basic_example"
version = "0.3.0"
edition = "2021"
license = "MIT OR Apache-2.0"
[dependencies]
bevy="0.12"
bevy_gltf_blueprints = "0.6"
bevy_gltf_save_load = { path = "../../../crates/bevy_gltf_save_load" }
bevy_rapier3d = { version = "0.23.0", features = [ "serde-serialize", "debug-render-3d", "enhanced-determinism"] }
bevy_asset_loader = { version = "0.18", features = ["standard_dynamic_assets" ]}
bevy_editor_pls = { version = "0.6" }
rand = "0.8.5"
serde_json="1.0.108"
serde="1.0.193"

View File

@ -0,0 +1,16 @@
# Basic save_load example/demo
This example showcases how to use ```bevy_save_load``` crate to save & load your bevy game
## Notes Workflow with blender / demo information
- the gltf files for this demo where generated using the **Export dynamic and static objects seperatly** feature of the auto_export addon:
so the static & dynamic level files where generated automatically, based on the entities that have a **Dynamic** component/custom property
## Running this example
```
cargo run --features bevy/dynamic_linking
```

View File

@ -0,0 +1 @@
({})

View File

@ -0,0 +1,8 @@
({
"world":File (path: "models/World.glb"),
"world_dynamic":File (path: "models/World_dynamic.glb"),
"models": Folder (
path: "models/library",
),
})

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
use bevy::prelude::*;
use bevy_asset_loader::prelude::*;
#[derive(AssetCollection, Resource)]
pub struct CoreAssets {}

View File

@ -0,0 +1,16 @@
use bevy::gltf::Gltf;
use bevy::prelude::*;
use bevy::utils::HashMap;
use bevy_asset_loader::prelude::*;
#[derive(AssetCollection, Resource)]
pub struct GameAssets {
#[asset(key = "world")]
pub world: Handle<Gltf>,
#[asset(key = "world_dynamic")]
pub world_dynamic: Handle<Gltf>,
#[asset(key = "models", collection(typed, mapped))]
pub models: HashMap<String, Handle<Gltf>>,
}

View File

@ -0,0 +1,35 @@
pub mod assets_core;
pub use assets_core::*;
pub mod assets_game;
pub use assets_game::*;
use bevy::prelude::*;
use bevy_asset_loader::prelude::*;
use crate::state::AppState;
pub struct AssetsPlugin;
impl Plugin for AssetsPlugin {
fn build(&self, app: &mut App) {
app
// load core assets (ie assets needed in the main menu, and everywhere else before loading more assets in game)
.add_loading_state(
LoadingState::new(AppState::CoreLoading).continue_to_state(AppState::MenuRunning),
)
.add_dynamic_collection_to_loading_state::<_, StandardDynamicAssetCollection>(
AppState::CoreLoading,
"assets_core.assets.ron",
)
.add_collection_to_loading_state::<_, CoreAssets>(AppState::CoreLoading)
// load game assets
.add_loading_state(
LoadingState::new(AppState::AppLoading).continue_to_state(AppState::AppRunning),
)
.add_dynamic_collection_to_loading_state::<_, StandardDynamicAssetCollection>(
AppState::AppLoading,
"assets_game.assets.ron",
)
.add_collection_to_loading_state::<_, GameAssets>(AppState::AppLoading);
}
}

View File

@ -0,0 +1,24 @@
use bevy::core_pipeline::bloom::{BloomCompositeMode, BloomSettings};
use bevy::core_pipeline::tonemapping::{DebandDither, Tonemapping};
use bevy::prelude::*;
use super::CameraTrackingOffset;
pub fn camera_replace_proxies(
mut commands: Commands,
mut added_cameras: Query<(Entity, &mut Camera), (Added<Camera>, With<CameraTrackingOffset>)>,
) {
for (entity, mut camera) in added_cameras.iter_mut() {
info!("detected added camera, updating proxy");
camera.hdr = true;
commands
.entity(entity)
.insert(DebandDither::Enabled)
.insert(Tonemapping::BlenderFilmic)
.insert(BloomSettings {
intensity: 0.01,
composite_mode: BloomCompositeMode::Additive,
..default()
});
}
}

View File

@ -0,0 +1,58 @@
use bevy::prelude::*;
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
/// Component for cameras, with an offset from the Trackable target
///
pub struct CameraTracking {
pub offset: Vec3,
}
impl Default for CameraTracking {
fn default() -> Self {
CameraTracking {
offset: Vec3::new(0.0, 6.0, 8.0),
}
}
}
#[derive(Component, Reflect, Debug, Deref, DerefMut)]
#[reflect(Component)]
/// Component for cameras, with an offset from the Trackable target
pub struct CameraTrackingOffset(Vec3);
impl Default for CameraTrackingOffset {
fn default() -> Self {
CameraTrackingOffset(Vec3::new(0.0, 6.0, 8.0))
}
}
impl CameraTrackingOffset {
fn new(input: Vec3) -> Self {
CameraTrackingOffset(input)
}
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// Add this component to an entity if you want it to be tracked by a Camera
pub struct CameraTrackable;
pub fn camera_track(
mut tracking_cameras: Query<
(&mut Transform, &CameraTrackingOffset),
(
With<Camera>,
With<CameraTrackingOffset>,
Without<CameraTrackable>,
),
>,
camera_tracked: Query<&Transform, With<CameraTrackable>>,
) {
for (mut camera_transform, tracking_offset) in tracking_cameras.iter_mut() {
for tracked_transform in camera_tracked.iter() {
let target_position = tracked_transform.translation + tracking_offset.0;
let eased_position = camera_transform.translation.lerp(target_position, 0.1);
camera_transform.translation = eased_position; // + tracking.offset;// tracked_transform.translation + tracking.offset;
*camera_transform = camera_transform.looking_at(tracked_transform.translation, Vec3::Y);
}
}
}

View File

@ -0,0 +1,24 @@
pub mod camera_tracking;
pub use camera_tracking::*;
pub mod camera_replace_proxies;
pub use camera_replace_proxies::*;
use bevy::prelude::*;
use bevy_gltf_blueprints::GltfBlueprintsSet;
pub struct CameraPlugin;
impl Plugin for CameraPlugin {
fn build(&self, app: &mut App) {
app.register_type::<CameraTrackable>()
.register_type::<CameraTracking>()
.register_type::<CameraTrackingOffset>()
.add_systems(
Update,
(
camera_replace_proxies.after(GltfBlueprintsSet::AfterSpawn),
camera_track,
),
);
}
}

View File

@ -0,0 +1,25 @@
use bevy::prelude::*;
use bevy::pbr::{CascadeShadowConfig, CascadeShadowConfigBuilder};
// fixme might be too specific to might needs, should it be moved out ? also these are all for lights, not models
pub fn lighting_replace_proxies(
mut added_dirights: Query<(Entity, &mut DirectionalLight), Added<DirectionalLight>>,
mut added_spotlights: Query<&mut SpotLight, Added<SpotLight>>,
mut commands: Commands,
) {
for (entity, mut light) in added_dirights.iter_mut() {
light.illuminance *= 5.0;
light.shadows_enabled = true;
let shadow_config: CascadeShadowConfig = CascadeShadowConfigBuilder {
first_cascade_far_bound: 15.0,
maximum_distance: 135.0,
..default()
}
.into();
commands.entity(entity).insert(shadow_config);
}
for mut light in added_spotlights.iter_mut() {
light.shadows_enabled = true;
}
}

View File

@ -0,0 +1,18 @@
mod lighting_replace_proxies;
use lighting_replace_proxies::*;
use bevy::pbr::{DirectionalLightShadowMap, NotShadowCaster};
use bevy::prelude::*;
pub struct LightingPlugin;
impl Plugin for LightingPlugin {
fn build(&self, app: &mut App) {
app
.insert_resource(DirectionalLightShadowMap { size: 4096 })
// FIXME: adding these since they are missing
.register_type::<NotShadowCaster>()
.add_systems(PreUpdate, lighting_replace_proxies) // FIXME: you should actually run this in a specific state most likely
;
}
}

View File

@ -0,0 +1,62 @@
pub mod camera;
pub use camera::*;
pub mod lighting;
pub use lighting::*;
pub mod relationships;
pub use relationships::*;
pub mod physics;
pub use physics::*;
use bevy::{
core_pipeline::tonemapping::Tonemapping,
prelude::*,
render::{camera::CameraRenderGraph, primitives::Frustum, view::VisibleEntities},
utils::HashSet,
};
use bevy_rapier3d::dynamics::Velocity;
use std::any::TypeId;
use bevy_gltf_blueprints::*;
use bevy_gltf_save_load::*;
use crate::game::Pickable;
pub struct CorePlugin;
impl Plugin for CorePlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
LightingPlugin,
CameraPlugin,
PhysicsPlugin,
SaveLoadPlugin {
save_path: "scenes".into(),
component_filter: SceneFilter::Allowlist(HashSet::from([
TypeId::of::<Name>(),
TypeId::of::<Transform>(),
TypeId::of::<Velocity>(),
TypeId::of::<InheritedVisibility>(),
TypeId::of::<Camera>(),
TypeId::of::<Camera3d>(),
TypeId::of::<Tonemapping>(),
TypeId::of::<CameraTrackingOffset>(),
TypeId::of::<Projection>(),
TypeId::of::<CameraRenderGraph>(),
TypeId::of::<Frustum>(),
TypeId::of::<GlobalTransform>(),
TypeId::of::<VisibleEntities>(),
TypeId::of::<Pickable>(),
])),
..Default::default()
},
BlueprintsPlugin {
library_folder: "models/library".into(),
format: GltfFormat::GLB,
aabbs: true,
..Default::default()
},
));
}
}

View File

@ -0,0 +1,26 @@
use bevy::{
ecs::system::Res,
input::{keyboard::KeyCode, Input},
log::info,
prelude::ResMut,
};
use bevy_rapier3d::{prelude::RapierConfiguration, render::DebugRenderContext};
pub fn pause_physics(mut physics_config: ResMut<RapierConfiguration>) {
info!("pausing physics");
physics_config.physics_pipeline_active = false;
}
pub fn resume_physics(mut physics_config: ResMut<RapierConfiguration>) {
info!("unpausing physics");
physics_config.physics_pipeline_active = true;
}
pub fn toggle_physics_debug(
mut debug_config: ResMut<DebugRenderContext>,
keycode: Res<Input<KeyCode>>,
) {
if keycode.just_pressed(KeyCode::D) {
debug_config.enabled = !debug_config.enabled;
}
}

View File

@ -0,0 +1,38 @@
pub mod physics_replace_proxies;
use bevy_rapier3d::{
prelude::{NoUserData, RapierPhysicsPlugin},
render::RapierDebugRenderPlugin,
};
pub use physics_replace_proxies::*;
pub mod utils;
pub mod controls;
pub use controls::*;
use crate::state::GameState;
use bevy::prelude::*;
// use super::blueprints::GltfBlueprintsSet;
use bevy_gltf_blueprints::GltfBlueprintsSet;
// use crate::Collider;
pub struct PhysicsPlugin;
impl Plugin for PhysicsPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
RapierPhysicsPlugin::<NoUserData>::default(),
RapierDebugRenderPlugin::default(),
))
.register_type::<AutoAABBCollider>()
.register_type::<physics_replace_proxies::Collider>()
// find a way to make serde's stuff serializable
// .register_type::<bevy_rapier3d::dynamics::CoefficientCombineRule>()
//bevy_rapier3d::dynamics::CoefficientCombineRule
.add_systems(
Update,
physics_replace_proxies.after(GltfBlueprintsSet::AfterSpawn),
)
.add_systems(OnEnter(GameState::InGame), resume_physics)
.add_systems(OnExit(GameState::InGame), pause_physics)
.add_systems(Update, toggle_physics_debug);
}
}

View File

@ -0,0 +1,101 @@
use bevy::prelude::*;
// use bevy::render::primitives::Aabb;
use bevy_rapier3d::geometry::Collider as RapierCollider;
use bevy_rapier3d::prelude::{ActiveCollisionTypes, ActiveEvents, ComputedColliderShape};
use super::utils::*;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub enum Collider {
Ball(f32),
Cuboid(Vec3),
Capsule(Vec3, Vec3, f32),
#[default]
Mesh,
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub enum AutoAABBCollider {
#[default]
Cuboid,
Ball,
Capsule,
}
// replaces all physics stand-ins with the actual rapier types
pub fn physics_replace_proxies(
meshes: Res<Assets<Mesh>>,
mesh_handles: Query<&Handle<Mesh>>,
mut proxy_colliders: Query<
(Entity, &Collider, &Name, &mut Visibility),
(Without<RapierCollider>, Added<Collider>),
>,
// needed for tri meshes
children: Query<&Children>,
mut commands: Commands,
) {
for proxy_colider in proxy_colliders.iter_mut() {
let (entity, collider_proxy, name, mut visibility) = proxy_colider;
// we hide the collider meshes: perhaps they should be removed altogether once processed ?
if name.ends_with("_collider") || name.ends_with("_sensor") {
*visibility = Visibility::Hidden;
}
let mut rapier_collider: RapierCollider;
match collider_proxy {
Collider::Ball(radius) => {
info!("generating collider from proxy: ball");
rapier_collider = RapierCollider::ball(*radius);
commands.entity(entity)
.insert(rapier_collider)
.insert(ActiveEvents::COLLISION_EVENTS) // FIXME: this is just for demo purposes !!!
;
}
Collider::Cuboid(size) => {
info!("generating collider from proxy: cuboid");
rapier_collider = RapierCollider::cuboid(size.x, size.y, size.z);
commands.entity(entity)
.insert(rapier_collider)
.insert(ActiveEvents::COLLISION_EVENTS) // FIXME: this is just for demo purposes !!!
;
}
Collider::Capsule(a, b, radius) => {
info!("generating collider from proxy: capsule");
rapier_collider = RapierCollider::capsule(*a, *b, *radius);
commands.entity(entity)
.insert(rapier_collider)
.insert(ActiveEvents::COLLISION_EVENTS) // FIXME: this is just for demo purposes !!!
;
}
Collider::Mesh => {
info!("generating collider from proxy: mesh");
for (_, collider_mesh) in
Mesh::search_in_children(entity, &children, &meshes, &mesh_handles)
{
rapier_collider = RapierCollider::from_bevy_mesh(
collider_mesh,
&ComputedColliderShape::TriMesh,
)
.unwrap();
commands
.entity(entity)
.insert(rapier_collider)
// FIXME: this is just for demo purposes !!!
.insert(
ActiveCollisionTypes::default()
| ActiveCollisionTypes::KINEMATIC_STATIC
| ActiveCollisionTypes::STATIC_STATIC
| ActiveCollisionTypes::DYNAMIC_STATIC,
)
.insert(ActiveEvents::COLLISION_EVENTS);
// .insert(ActiveEvents::COLLISION_EVENTS)
// break;
// RapierCollider::convex_hull(points)
}
}
}
}
}

View File

@ -0,0 +1,175 @@
use bevy::prelude::*;
use bevy::render::mesh::{MeshVertexAttributeId, PrimitiveTopology, VertexAttributeValues};
// TAKEN VERBATIB FROM https://github.com/janhohenheim/foxtrot/blob/src/util/trait_extension.rs
pub(crate) trait Vec3Ext: Copy {
fn is_approx_zero(self) -> bool;
fn split(self, up: Vec3) -> SplitVec3;
}
impl Vec3Ext for Vec3 {
#[inline]
fn is_approx_zero(self) -> bool {
self.length_squared() < 1e-5
}
#[inline]
fn split(self, up: Vec3) -> SplitVec3 {
let vertical = up * self.dot(up);
let horizontal = self - vertical;
SplitVec3 {
vertical,
horizontal,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct SplitVec3 {
pub(crate) vertical: Vec3,
pub(crate) horizontal: Vec3,
}
pub(crate) trait Vec2Ext: Copy {
fn is_approx_zero(self) -> bool;
fn x0y(self) -> Vec3;
}
impl Vec2Ext for Vec2 {
#[inline]
fn is_approx_zero(self) -> bool {
self.length_squared() < 1e-5
}
#[inline]
fn x0y(self) -> Vec3 {
Vec3::new(self.x, 0., self.y)
}
}
pub(crate) trait MeshExt {
fn transform(&mut self, transform: Transform);
fn transformed(&self, transform: Transform) -> Mesh;
fn read_coords_mut(&mut self, id: impl Into<MeshVertexAttributeId>) -> &mut Vec<[f32; 3]>;
fn search_in_children<'a>(
parent: Entity,
children: &'a Query<&Children>,
meshes: &'a Assets<Mesh>,
mesh_handles: &'a Query<&Handle<Mesh>>,
) -> Vec<(Entity, &'a Mesh)>;
}
impl MeshExt for Mesh {
fn transform(&mut self, transform: Transform) {
for coords in self.read_coords_mut(Mesh::ATTRIBUTE_POSITION.clone()) {
let vec3 = (*coords).into();
let transformed = transform.transform_point(vec3);
*coords = transformed.into();
}
for normal in self.read_coords_mut(Mesh::ATTRIBUTE_NORMAL.clone()) {
let vec3 = (*normal).into();
let transformed = transform.rotation.mul_vec3(vec3);
*normal = transformed.into();
}
}
fn transformed(&self, transform: Transform) -> Mesh {
let mut mesh = self.clone();
mesh.transform(transform);
mesh
}
fn read_coords_mut(&mut self, id: impl Into<MeshVertexAttributeId>) -> &mut Vec<[f32; 3]> {
// Guaranteed by Bevy for the current usage
match self
.attribute_mut(id)
.expect("Failed to read unknown mesh attribute")
{
VertexAttributeValues::Float32x3(values) => values,
// Guaranteed by Bevy for the current usage
_ => unreachable!(),
}
}
fn search_in_children<'a>(
parent: Entity,
children_query: &'a Query<&Children>,
meshes: &'a Assets<Mesh>,
mesh_handles: &'a Query<&Handle<Mesh>>,
) -> Vec<(Entity, &'a Mesh)> {
if let Ok(children) = children_query.get(parent) {
let mut result: Vec<_> = children
.iter()
.filter_map(|entity| mesh_handles.get(*entity).ok().map(|mesh| (*entity, mesh)))
.map(|(entity, mesh_handle)| {
(
entity,
meshes
.get(mesh_handle)
.expect("Failed to get mesh from handle"),
)
})
.map(|(entity, mesh)| {
assert_eq!(mesh.primitive_topology(), PrimitiveTopology::TriangleList);
(entity, mesh)
})
.collect();
let mut inner_result = children
.iter()
.flat_map(|entity| {
Self::search_in_children(*entity, children_query, meshes, mesh_handles)
})
.collect();
result.append(&mut inner_result);
result
} else {
Vec::new()
}
}
}
pub(crate) trait F32Ext: Copy {
fn is_approx_zero(self) -> bool;
fn squared(self) -> f32;
fn lerp(self, other: f32, ratio: f32) -> f32;
}
impl F32Ext for f32 {
#[inline]
fn is_approx_zero(self) -> bool {
self.abs() < 1e-5
}
#[inline]
fn squared(self) -> f32 {
self * self
}
#[inline]
fn lerp(self, other: f32, ratio: f32) -> f32 {
self.mul_add(1. - ratio, other * ratio)
}
}
pub(crate) trait TransformExt: Copy {
fn horizontally_looking_at(self, target: Vec3, up: Vec3) -> Transform;
fn lerp(self, other: Transform, ratio: f32) -> Transform;
}
impl TransformExt for Transform {
fn horizontally_looking_at(self, target: Vec3, up: Vec3) -> Transform {
let direction = target - self.translation;
let horizontal_direction = direction - up * direction.dot(up);
let look_target = self.translation + horizontal_direction;
self.looking_at(look_target, up)
}
fn lerp(self, other: Transform, ratio: f32) -> Transform {
let translation = self.translation.lerp(other.translation, ratio);
let rotation = self.rotation.slerp(other.rotation, ratio);
let scale = self.scale.lerp(other.scale, ratio);
Transform {
translation,
rotation,
scale,
}
}
}

View File

@ -0,0 +1,75 @@
use bevy::prelude::*;
use bevy::render::mesh::{MeshVertexAttributeId, PrimitiveTopology, VertexAttributeValues};
// TAKEN VERBATIB FROM https://github.com/janhohenheim/foxtrot/blob/6e31fc02652fc9d085a4adde0a73ab007dbbb0dc/src/util/trait_extension.rs
pub trait Vec3Ext {
#[allow(clippy::wrong_self_convention)] // Because [`Vec3`] is [`Copy`]
fn is_approx_zero(self) -> bool;
fn x0z(self) -> Vec3;
}
impl Vec3Ext for Vec3 {
fn is_approx_zero(self) -> bool {
[self.x, self.y, self.z].iter().all(|&x| x.abs() < 1e-5)
}
fn x0z(self) -> Vec3 {
Vec3::new(self.x, 0., self.z)
}
}
pub trait MeshExt {
fn transform(&mut self, transform: Transform);
fn transformed(&self, transform: Transform) -> Mesh;
fn read_coords_mut(&mut self, id: impl Into<MeshVertexAttributeId>) -> &mut Vec<[f32; 3]>;
fn search_in_children<'a>(
children: &'a Children,
meshes: &'a Assets<Mesh>,
mesh_handles: &'a Query<&Handle<Mesh>>,
) -> (Entity, &'a Mesh);
}
impl MeshExt for Mesh {
fn transform(&mut self, transform: Transform) {
for attribute in [Mesh::ATTRIBUTE_POSITION, Mesh::ATTRIBUTE_NORMAL] {
for coords in self.read_coords_mut(attribute.clone()) {
let vec3 = (*coords).into();
let transformed = transform.transform_point(vec3);
*coords = transformed.into();
}
}
}
fn transformed(&self, transform: Transform) -> Mesh {
let mut mesh = self.clone();
mesh.transform(transform);
mesh
}
fn read_coords_mut(&mut self, id: impl Into<MeshVertexAttributeId>) -> &mut Vec<[f32; 3]> {
match self.attribute_mut(id).unwrap() {
VertexAttributeValues::Float32x3(values) => values,
// Guaranteed by Bevy
_ => unreachable!(),
}
}
fn search_in_children<'a>(
children: &'a Children,
meshes: &'a Assets<Mesh>,
mesh_handles: &'a Query<&Handle<Mesh>>,
) -> (Entity, &'a Mesh) {
let entity_handles: Vec<_> = children
.iter()
.filter_map(|entity| mesh_handles.get(*entity).ok().map(|mesh| (*entity, mesh)))
.collect();
assert_eq!(
entity_handles.len(),
1,
"Collider must contain exactly one mesh, but found {}",
entity_handles.len()
);
let (entity, mesh_handle) = entity_handles.first().unwrap();
let mesh = meshes.get(mesh_handle).unwrap();
assert_eq!(mesh.primitive_topology(), PrimitiveTopology::TriangleList);
(*entity, mesh)
}
}

View File

@ -0,0 +1,11 @@
pub mod relationships_insert_dependant_components;
pub use relationships_insert_dependant_components::*;
use bevy::prelude::*;
pub struct EcsRelationshipsPlugin;
impl Plugin for EcsRelationshipsPlugin {
fn build(&self, app: &mut App) {
app;
}
}

View File

@ -0,0 +1,15 @@
use bevy::prelude::*;
pub fn insert_dependant_component<
Dependant: Component,
Dependency: Component + std::default::Default,
>(
mut commands: Commands,
entities_without_depency: Query<(Entity, &Name), (With<Dependant>, Without<Dependency>)>,
) {
for (entity, name) in entities_without_depency.iter() {
let name = name.clone().to_string();
commands.entity(entity).insert(Dependency::default());
warn!("found an entity called {} with a {} component but without an {}, please check your assets", name.clone(), std::any::type_name::<Dependant>(), std::any::type_name::<Dependency>());
}
}

View File

@ -0,0 +1,203 @@
use super::Player;
use crate::state::{GameState, InAppRunning};
use bevy::prelude::*;
use bevy_gltf_blueprints::{BluePrintBundle, BlueprintName, GameWorldTag, Library, NoInBlueprint};
use bevy_gltf_save_load::{Dynamic, DynamicEntitiesRoot, StaticEntitiesRoot};
use bevy_rapier3d::prelude::Velocity;
use rand::Rng;
pub fn setup_game(mut commands: Commands, mut next_game_state: ResMut<NextState<GameState>>) {
info!("setting up game world");
// here we actually spawn our game world/level
let world_root = commands
.spawn((
Name::from("world"),
GameWorldTag,
InAppRunning,
TransformBundle::default(),
InheritedVisibility::default(),
//StaticEntities("World"),
//DynamicEntities("World_dynamic")
))
.id();
// and we fill it with static entities
let static_data = commands
.spawn((
Name::from("static"),
BluePrintBundle {
blueprint: BlueprintName("World".to_string()),
..Default::default()
},
StaticEntitiesRoot,
Library("models".into()),
))
.id();
// and we fill it with dynamic entities
let dynamic_data = commands
.spawn((
Name::from("dynamic"),
BluePrintBundle {
blueprint: BlueprintName("World_dynamic".to_string()),
..Default::default()
},
DynamicEntitiesRoot,
NoInBlueprint, // we do not want the descendants of this entity to be filtered out when saving
Library("models".into()),
))
.id();
commands.entity(world_root).add_child(static_data);
commands.entity(world_root).add_child(dynamic_data);
next_game_state.set(GameState::InGame)
}
// TODO: Same as in load, reuse
pub fn unload_world(mut commands: Commands, gameworlds: Query<Entity, With<GameWorldTag>>) {
for e in gameworlds.iter() {
info!("--loading: despawn old world/level");
commands.entity(e).despawn_recursive();
}
}
pub fn should_reset(keycode: Res<Input<KeyCode>>) -> bool {
return keycode.just_pressed(KeyCode::N);
}
pub fn spawn_test(
keycode: Res<Input<KeyCode>>,
mut dynamic_entities_world: Query<Entity, With<DynamicEntitiesRoot>>,
mut commands: Commands,
) {
if keycode.just_pressed(KeyCode::T) {
let world = dynamic_entities_world.single_mut();
let mut rng = rand::thread_rng();
let range = 5.5;
let x: f32 = rng.gen_range(-range..range);
let y: f32 = rng.gen_range(-range..range);
let mut rng = rand::thread_rng();
let range = 0.8;
let vel_x: f32 = rng.gen_range(-range..range);
let vel_y: f32 = rng.gen_range(2.0..2.5);
let vel_z: f32 = rng.gen_range(-range..range);
let name_index: u64 = rng.gen();
let new_entity = commands
.spawn((
BluePrintBundle {
blueprint: BlueprintName("Health_Pickup".to_string()),
..Default::default()
},
// AddToGameWorld, // automatically added to entity (should be only one) that has the GameWorldTag
bevy::prelude::Name::from(format!("test{}", name_index)),
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
Velocity {
linvel: Vec3::new(vel_x, vel_y, vel_z),
angvel: Vec3::new(0.0, 0.0, 0.0),
},
))
.id();
commands.entity(world).add_child(new_entity);
}
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
struct UnregisteredComponent;
pub fn spawn_test_unregisted_components(
keycode: Res<Input<KeyCode>>,
mut commands: Commands,
mut dynamic_entities_world: Query<Entity, With<DynamicEntitiesRoot>>,
) {
if keycode.just_pressed(KeyCode::U) {
let world = dynamic_entities_world.single_mut();
let mut rng = rand::thread_rng();
let range = 5.5;
let x: f32 = rng.gen_range(-range..range);
let y: f32 = rng.gen_range(-range..range);
let mut rng = rand::thread_rng();
let range = 0.8;
let vel_x: f32 = rng.gen_range(-range..range);
let vel_y: f32 = rng.gen_range(2.0..2.5);
let vel_z: f32 = rng.gen_range(-range..range);
let name_index: u64 = rng.gen();
let new_entity = commands
.spawn((
BluePrintBundle {
blueprint: BlueprintName("Health_Pickup".to_string()),
..Default::default()
},
bevy::prelude::Name::from(format!("test{}", name_index)),
// BlueprintName("Health_Pickup".to_string()),
// SpawnHere,
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
Velocity {
linvel: Vec3::new(vel_x, vel_y, vel_z),
angvel: Vec3::new(0.0, 0.0, 0.0),
},
UnregisteredComponent,
))
.id();
commands.entity(world).add_child(new_entity);
}
}
pub fn spawn_test_parenting(
keycode: Res<Input<KeyCode>>,
players: Query<Entity, With<Player>>,
mut commands: Commands,
names: Query<(Entity, &Name)>,
) {
if keycode.just_pressed(KeyCode::P) {
let mut rng = rand::thread_rng();
let range = 5.5;
let x: f32 = rng.gen_range(-range..range);
let y: f32 = rng.gen_range(-range..range);
let child_test = commands
.spawn((
BluePrintBundle {
blueprint: BlueprintName("Sphero".to_string()),
..Default::default()
},
bevy::prelude::Name::from(format!("SubParentingTest")),
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
Dynamic(true),
))
.id();
let parenting_test_entity = commands
.spawn((
BluePrintBundle {
blueprint: BlueprintName("Container".into()),
..Default::default()
},
bevy::prelude::Name::from(format!("ParentingTest")),
Dynamic(true),
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
))
.id();
commands.entity(parenting_test_entity).add_child(child_test);
for player in players.iter() {
commands.entity(player).add_child(parenting_test_entity);
}
for (e, name) in names.iter() {
if name.to_string() == "Player" {
commands.entity(e).add_child(parenting_test_entity);
}
}
}
}

View File

@ -0,0 +1,47 @@
use bevy::{core_pipeline::clear_color::ClearColorConfig, prelude::*};
use crate::state::InGameLoading;
pub fn setup_loading_screen(mut commands: Commands) {
commands.spawn((
Camera2dBundle {
camera_2d: Camera2d {
clear_color: ClearColorConfig::Custom(Color::BLACK),
},
camera: Camera {
// renders after / on top of the main camera
order: 2,
..default()
},
..Default::default()
},
InGameLoading,
));
commands.spawn((
TextBundle::from_section(
"Loading...",
TextStyle {
font_size: 28.0,
color: Color::WHITE,
..Default::default()
},
)
.with_style(Style {
position_type: PositionType::Relative,
top: Val::Percent(45.0),
left: Val::Percent(45.0),
..default()
}),
InGameLoading,
));
}
pub fn teardown_loading_screen(
in_main_menu: Query<Entity, With<InGameLoading>>,
mut commands: Commands,
) {
for entity in in_main_menu.iter() {
commands.entity(entity).despawn_recursive();
}
}

View File

@ -0,0 +1,32 @@
use bevy::prelude::*;
use crate::state::InGameSaving;
pub fn setup_saving_screen(mut commands: Commands) {
commands.spawn((
TextBundle::from_section(
"Saving...",
TextStyle {
font_size: 28.0,
color: Color::WHITE,
..Default::default()
},
)
.with_style(Style {
position_type: PositionType::Relative,
top: Val::Percent(90.0),
left: Val::Percent(80.0),
..default()
}),
InGameSaving,
));
}
pub fn teardown_saving_screen(
in_main_menu: Query<Entity, With<InGameSaving>>,
mut commands: Commands,
) {
for entity in in_main_menu.iter() {
commands.entity(entity).despawn_recursive();
}
}

View File

@ -0,0 +1,116 @@
use bevy::prelude::*;
use crate::state::{AppState, InMainMenu};
pub fn setup_main_menu(mut commands: Commands) {
commands.spawn((Camera2dBundle::default(), InMainMenu));
commands.spawn((
TextBundle::from_section(
"SOME GAME TITLE !!",
TextStyle {
font_size: 18.0,
color: Color::WHITE,
..Default::default()
},
)
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(100.0),
left: Val::Px(200.0),
..default()
}),
InMainMenu,
));
commands.spawn((
TextBundle::from_section(
"New Game (press Enter to start)
- press N to restart (once the game is started)
- press S to save (once the game is started)
- press L to load (once the game is started)
- press T for demo spawning (once the game is started)
- press U to spawn entities with unregistered components (once the game is started)
- press P to spawn entities attached to the player (once the game is started)
",
TextStyle {
font_size: 18.0,
color: Color::WHITE,
..Default::default()
},
)
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(200.0),
left: Val::Px(200.0),
..default()
}),
InMainMenu,
));
/*
commands.spawn((
TextBundle::from_section(
"Load Game",
TextStyle {
font_size: 18.0,
color: Color::WHITE,
..Default::default()
},
)
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(250.0),
left: Val::Px(200.0),
..default()
}),
InMainMenu
));
commands.spawn((
TextBundle::from_section(
"Exit Game",
TextStyle {
font_size: 18.0,
color: Color::WHITE,
..Default::default()
},
)
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(300.0),
left: Val::Px(200.0),
..default()
}),
InMainMenu
));*/
}
pub fn teardown_main_menu(in_main_menu: Query<Entity, With<InMainMenu>>, mut commands: Commands) {
for entity in in_main_menu.iter() {
commands.entity(entity).despawn_recursive();
}
}
pub fn main_menu(
keycode: Res<Input<KeyCode>>,
mut next_app_state: ResMut<NextState<AppState>>,
// mut next_game_state: ResMut<NextState<GameState>>,
// mut save_requested_events: EventWriter<SaveRequest>,
// mut load_requested_events: EventWriter<LoadRequest>,
) {
if keycode.just_pressed(KeyCode::Return) {
next_app_state.set(AppState::AppLoading);
// next_game_state.set(GameState::None);
}
if keycode.just_pressed(KeyCode::L) {
next_app_state.set(AppState::AppLoading);
// load_requested_events.send(LoadRequest { path: "toto".into() })
}
if keycode.just_pressed(KeyCode::S) {
// save_requested_events.send(SaveRequest { path: "toto".into() })
}
}

View File

@ -0,0 +1,171 @@
pub mod in_game;
pub use in_game::*;
pub mod in_main_menu;
pub use in_main_menu::*;
pub mod in_game_loading;
pub use in_game_loading::*;
pub mod in_game_saving;
pub use in_game_saving::*;
pub mod picking;
pub use picking::*;
use crate::state::{AppState, GameState};
use bevy::prelude::*;
use bevy_gltf_save_load::{LoadRequest, LoadingFinished, SaveRequest, SavingFinished};
// this file is just for demo purposes, contains various types of components, systems etc
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub enum SoundMaterial {
Metal,
Wood,
Rock,
Cloth,
Squishy,
#[default]
None,
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// Demo marker component
pub struct Player;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// Demo component showing auto injection of components
pub struct ShouldBeWithPlayer;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
/// Demo marker component
pub struct Interactible;
fn player_move_demo(
keycode: Res<Input<KeyCode>>,
mut players: Query<&mut Transform, With<Player>>,
) {
let speed = 0.2;
if let Ok(mut player) = players.get_single_mut() {
if keycode.pressed(KeyCode::Left) {
player.translation.x += speed;
}
if keycode.pressed(KeyCode::Right) {
player.translation.x -= speed;
}
if keycode.pressed(KeyCode::Up) {
player.translation.z += speed;
}
if keycode.pressed(KeyCode::Down) {
player.translation.z -= speed;
}
}
}
pub fn request_save(
mut save_requests: EventWriter<SaveRequest>,
keycode: Res<Input<KeyCode>>,
current_state: Res<State<GameState>>,
mut next_game_state: ResMut<NextState<GameState>>,
) {
if keycode.just_pressed(KeyCode::S)
&& (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(),
})
}
}
pub fn on_saving_finished(
mut saving_finished: EventReader<SavingFinished>,
mut next_game_state: ResMut<NextState<GameState>>,
) {
for _ in saving_finished.read() {
next_game_state.set(GameState::InGame);
}
}
pub fn request_load(
mut load_requests: EventWriter<LoadRequest>,
keycode: Res<Input<KeyCode>>,
current_state: Res<State<GameState>>,
mut next_game_state: ResMut<NextState<GameState>>,
) {
if keycode.just_pressed(KeyCode::L)
&& (current_state.get() != &GameState::InLoading)
&& (current_state.get() != &GameState::InSaving)
{
next_game_state.set(GameState::InLoading);
load_requests.send(LoadRequest {
path: "save.scn.ron".into(),
})
}
}
pub fn on_loading_finished(
mut loading_finished: EventReader<LoadingFinished>,
mut next_game_state: ResMut<NextState<GameState>>,
) {
for _ in loading_finished.read() {
next_game_state.set(GameState::InGame);
}
}
pub struct GamePlugin;
impl Plugin for GamePlugin {
fn build(&self, app: &mut App) {
app.add_plugins(PickingPlugin)
.register_type::<Interactible>()
.register_type::<SoundMaterial>()
.register_type::<Player>()
.add_systems(
Update,
(
// little helper utility, to automatically inject components that are dependant on an other component
// ie, here an Entity with a Player component should also always have a ShouldBeWithPlayer component
// you get a warning if you use this, as I consider this to be stop-gap solution (usually you should have either a bundle, or directly define all needed components)
// insert_dependant_component::<Player, ShouldBeWithPlayer>,
player_move_demo, //.run_if(in_state(AppState::Running)),
spawn_test,
spawn_test_unregisted_components,
spawn_test_parenting,
)
.run_if(in_state(GameState::InGame)),
)
.add_systems(
Update,
(unload_world, apply_deferred, setup_game)
.chain()
.run_if(should_reset)
.run_if(in_state(AppState::AppRunning)),
)
.add_systems(
Update,
(
request_save,
request_load,
on_saving_finished,
on_loading_finished,
),
)
.add_systems(OnEnter(AppState::MenuRunning), setup_main_menu)
.add_systems(OnExit(AppState::MenuRunning), teardown_main_menu)
.add_systems(Update, main_menu.run_if(in_state(AppState::MenuRunning)))
.add_systems(OnEnter(GameState::InLoading), setup_loading_screen)
.add_systems(OnExit(GameState::InLoading), teardown_loading_screen)
.add_systems(OnEnter(GameState::InSaving), setup_saving_screen)
.add_systems(OnExit(GameState::InSaving), teardown_saving_screen)
.add_systems(OnEnter(AppState::AppRunning), setup_game);
}
}

View File

@ -0,0 +1,34 @@
use super::Player;
use bevy::prelude::*;
use bevy_gltf_blueprints::GltfBlueprintsSet;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct Pickable;
// very simple, crude picking (as in picking up objects) implementation
pub fn picking(
players: Query<&GlobalTransform, With<Player>>,
pickables: Query<(Entity, &GlobalTransform), With<Pickable>>,
mut commands: Commands,
) {
for player_transforms in players.iter() {
for (pickable, pickable_transforms) in pickables.iter() {
let distance = player_transforms
.translation()
.distance(pickable_transforms.translation());
if distance < 2.5 {
commands.entity(pickable).despawn_recursive();
}
}
}
}
pub struct PickingPlugin;
impl Plugin for PickingPlugin {
fn build(&self, app: &mut App) {
app.register_type::<Pickable>()
.add_systems(Update, (picking.after(GltfBlueprintsSet::AfterSpawn),));
}
}

View File

@ -0,0 +1,33 @@
use bevy::prelude::*;
use bevy_editor_pls::prelude::*;
mod core;
use crate::core::*;
pub mod assets;
use assets::*;
pub mod state;
use state::*;
mod game;
use game::*;
mod test_components;
use test_components::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins.set(AssetPlugin::default()),
// editor
EditorPlugin::default(),
// our custom plugins
StatePlugin,
AssetsPlugin,
CorePlugin, // reusable plugins
GamePlugin, // specific to our game
ComponentsTestPlugin, // Showcases different type of components /structs
))
.run();
}

View File

@ -0,0 +1,57 @@
use bevy::prelude::*;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default, States)]
pub enum AppState {
#[default]
CoreLoading,
MenuRunning,
AppLoading,
AppRunning,
AppEnding,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default, States)]
pub enum GameState {
#[default]
None,
InMenu,
InGame,
InGameOver,
InSaving,
InLoading,
}
// tag components for all entities within a certain state (for despawning them if needed) , FIXME: seems kinda hack-ish
#[derive(Component)]
pub struct InCoreLoading;
#[derive(Component, Default)]
pub struct InMenuRunning;
#[derive(Component)]
pub struct InAppRunning;
// components for tagging in game vs in game menu stuff
#[derive(Component, Default)]
pub struct InMainMenu;
#[derive(Component, Default)]
pub struct InMenu;
#[derive(Component, Default)]
pub struct InGame;
#[derive(Component, Default)]
pub struct InGameSaving;
#[derive(Component, Default)]
pub struct InGameLoading;
pub struct StatePlugin;
impl Plugin for StatePlugin {
fn build(&self, app: &mut App) {
app.add_state::<AppState>().add_state::<GameState>();
}
}

Some files were not shown because too many files have changed in this diff Show More