feat(Animation): added code & example for animation support (#33)
* feat(animation): added example & boilerplate * moved animations specific code to a different module * added multiple robots & foxes * added example of controlling animation based on distance from the player * removed obsolete files * added information about animation to READMEs * updated dependencies closes #26
This commit is contained in:
parent
ea1d6cd78f
commit
4afa0f5d7d
File diff suppressed because it is too large
Load Diff
|
@ -14,7 +14,7 @@ members = [
|
|||
[dev-dependencies]
|
||||
bevy="0.11"
|
||||
bevy_rapier3d = { version = "0.22.0", features = [ "serde-serialize", "debug-render-3d", "enhanced-determinism"] }
|
||||
bevy_editor_pls = { git="https://github.com/jakobhellermann/bevy_editor_pls.git" }
|
||||
bevy_editor_pls = { version = "0.5.0" }
|
||||
bevy_asset_loader = { version = "0.17.0", features = ["standard_dynamic_assets" ]} #version = "0.16",
|
||||
rand = "0.8.5"
|
||||
|
||||
|
@ -30,6 +30,10 @@ ron="*"
|
|||
name = "basic"
|
||||
path = "examples/basic/main.rs"
|
||||
|
||||
[[example]]
|
||||
name = "animation"
|
||||
path = "examples/animation/main.rs"
|
||||
|
||||
[[example]]
|
||||
name = "advanced"
|
||||
path = "examples/advanced/main.rs"
|
||||
|
|
|
@ -50,6 +50,7 @@ Please read the [README]((./tools/gltf_auto_export/README.md)) of the add-on for
|
|||
|
||||
- [basic](./examples/basic/) use of ```bevy_gltf_components``` only, to spawn entities with components defined inside gltf files
|
||||
- [advanced](./examples/advanced/) more advanced example : use of ```bevy_gltf_blueprints``` to spawn a level and then populate it with entities coming from different gltf files, live (at runtime) spawning of entities etc
|
||||
- [animation](./examples/animation/) how to use and trigger animations from gltf files (a feature of ```bevy_gltf_blueprints```)
|
||||
|
||||
## Workflow
|
||||
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
({})
|
|
@ -0,0 +1,6 @@
|
|||
({
|
||||
"world":File (path: "animation/models/Level1.glb#Scene0"),
|
||||
"models": Folder (
|
||||
path: "animation/models/library",
|
||||
),
|
||||
})
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "bevy_gltf_blueprints"
|
||||
version = "0.1.4"
|
||||
version = "0.2.1"
|
||||
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"
|
||||
|
@ -11,8 +11,8 @@ edition = "2021"
|
|||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
bevy = { version = "0.11", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf", "bevy_animation"] }
|
||||
bevy = { version = "0.11", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf", "bevy_animation", "animation"] }
|
||||
|
||||
[dependencies]
|
||||
bevy_gltf_components = "0.1"
|
||||
bevy = { version = "0.11", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf", "bevy_animation"] }
|
||||
bevy = { version = "0.11", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf", "bevy_animation", "animation"] }
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
|
||||
Built upon [bevy_gltf_components](https://crates.io/crates/bevy_gltf_components) this crate adds the ability to define Blueprints/Prefabs for [Bevy](https://bevyengine.org/) inside gltf files and spawn them in Bevy.
|
||||
|
||||
* Allows you to create lightweight levels, where all assets are different gltf files and loaded after the main level is loaded
|
||||
* Allows you to spawn different entities from gtlf files at runtime in a clean manner, including simplified animation support !
|
||||
|
||||
A blueprint is a set of **overrideable** components + a hierarchy: ie
|
||||
|
||||
* just a Gltf file with Gltf_extras specifying components
|
||||
|
@ -23,7 +26,7 @@ Here's a minimal usage example:
|
|||
# Cargo.toml
|
||||
[dependencies]
|
||||
bevy="0.11"
|
||||
bevy_gltf_blueprints = { version = "0.1"}
|
||||
bevy_gltf_blueprints = { version = "0.2"}
|
||||
|
||||
```
|
||||
|
||||
|
@ -61,7 +64,7 @@ fn spawn_blueprint(
|
|||
Add the following to your `[dependencies]` section in `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
bevy_gltf_blueprints = "0.1"
|
||||
bevy_gltf_blueprints = "0.2"
|
||||
```
|
||||
|
||||
Or use `cargo add`:
|
||||
|
@ -179,9 +182,69 @@ Typically , the order of systems should be
|
|||
|
||||
see https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/advanced for how to set it up correctly
|
||||
|
||||
|
||||
|
||||
## Animation
|
||||
|
||||
```bevy_gltf_blueprints``` provides some lightweight helpers to deal with animations stored in gltf files
|
||||
|
||||
* an ```Animations``` component that gets inserted into spawned (root) entities that contains a hashmap of all animations contained inside that entity/gltf file .
|
||||
(this is a copy of the ```named_animations``` inside Bevy's gltf structures )
|
||||
* an ```AnimationPlayerLink``` component that gets inserted into spawned (root) entities, to make it easier to trigger/ control animations than it usually is inside Bevy + Gltf files
|
||||
|
||||
The workflow for animations is as follows:
|
||||
* create a gltf file with animations (using Blender & co) as you would normally do
|
||||
* inside Bevy, use the ```bevy_gltf_blueprints``` boilerplate (see sections above), no specific setup beyond that is required
|
||||
* to control the animation of an entity, you need to query for entities that have both ```AnimationPlayerLink``` and ```Animations``` components (added by ```bevy_gltf_blueprints```) AND entities with the ```AnimationPlayer``` component
|
||||
|
||||
For example:
|
||||
|
||||
```rust no_run
|
||||
// example of changing animation of entities based on proximity to the player, for "fox" entities (Tag component)
|
||||
pub fn animation_change_on_proximity_foxes(
|
||||
players: Query<&GlobalTransform, With<Player>>,
|
||||
animated_foxes: Query<(&GlobalTransform, &AnimationPlayerLink, &Animations ), With<Fox>>,
|
||||
|
||||
mut animation_players: Query<&mut AnimationPlayer>,
|
||||
|
||||
){
|
||||
for player_transforms in players.iter() {
|
||||
for (fox_tranforms, link, animations) in animated_foxes.iter() {
|
||||
let distance = player_transforms
|
||||
.translation()
|
||||
.distance(fox_tranforms.translation());
|
||||
let mut anim_name = "Walk";
|
||||
if distance < 8.5 {
|
||||
anim_name = "Run";
|
||||
}
|
||||
else if distance >= 8.5 && distance < 10.0{
|
||||
anim_name = "Walk";
|
||||
}
|
||||
else if distance >= 10.0 && distance < 15.0{
|
||||
anim_name = "Survey";
|
||||
}
|
||||
// now play the animation based on the chosen animation name
|
||||
let mut animation_player = animation_players.get_mut(link.0).unwrap();
|
||||
animation_player.play_with_transition(
|
||||
animations.named_animations.get(anim_name).expect("animation name should be in the list").clone(),
|
||||
Duration::from_secs(3)
|
||||
).repeat();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
see https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/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
|
||||
onward
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/advanced
|
||||
https://github.com/kaosat-dev/Blender_bevy_components_workflow/tree/main/examples/animation
|
||||
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy::utils::HashMap;
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug)]
|
||||
#[reflect(Component)]
|
||||
/// storage for animations for a given entity (hierarchy), essentially a clone of gltf's named_animations
|
||||
pub struct Animations {
|
||||
pub named_animations: HashMap<String, Handle<AnimationClip>>,
|
||||
}
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
/// Stop gap helper component : this is inserted into a "root" entity (an entity representing a whole gltf file)
|
||||
/// so that the root entity knows which of its children contains an actualy AnimationPlayer component
|
||||
/// this is for convenience, because currently , Bevy's gltf parsing inserts AnimationPlayers "one level down"
|
||||
/// ie armature/root for animated models, which means more complex queries to trigger animations that we want to avoid
|
||||
pub struct AnimationPlayerLink(pub Entity);
|
|
@ -2,7 +2,10 @@ pub mod spawn_from_blueprints;
|
|||
pub use spawn_from_blueprints::*;
|
||||
|
||||
pub mod spawn_post_process;
|
||||
pub use spawn_post_process::*;
|
||||
pub(crate) use spawn_post_process::*;
|
||||
|
||||
pub mod animation;
|
||||
pub use animation::*;
|
||||
|
||||
pub mod clone_entity;
|
||||
pub use clone_entity::*;
|
||||
|
@ -59,6 +62,7 @@ impl Plugin for BlueprintsPlugin {
|
|||
app.add_plugins(ComponentsFromGltfPlugin)
|
||||
.register_type::<BlueprintName>()
|
||||
.register_type::<SpawnHere>()
|
||||
.register_type::<Animations>()
|
||||
.insert_resource(BluePrintsConfig {
|
||||
library_folder: self.library_folder.clone(),
|
||||
})
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::path::Path;
|
|||
|
||||
use bevy::{gltf::Gltf, prelude::*};
|
||||
|
||||
use crate::BluePrintsConfig;
|
||||
use crate::{Animations, BluePrintsConfig};
|
||||
|
||||
/// this is a flag component for our levels/game world
|
||||
#[derive(Component)]
|
||||
|
@ -85,6 +85,9 @@ pub(crate) fn spawn_from_blueprints(
|
|||
// Parent(world) // FIXME/ would be good if this worked directly
|
||||
SpawnedRoot,
|
||||
Original(entity),
|
||||
Animations {
|
||||
named_animations: gltf.named_animations.clone(),
|
||||
},
|
||||
))
|
||||
.id();
|
||||
commands.entity(world).add_child(child_scene);
|
||||
|
|
|
@ -1,16 +1,9 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy::utils::HashMap;
|
||||
|
||||
use super::{AnimationPlayerLink, Animations};
|
||||
use super::{CloneEntity, SpawnHere};
|
||||
use super::{Original, SpawnedRoot};
|
||||
|
||||
// FIXME: move to more relevant module
|
||||
#[derive(Component, Reflect, Default, Debug)]
|
||||
#[reflect(Component)]
|
||||
pub struct AnimationHelper {
|
||||
pub named_animations: HashMap<String, Handle<AnimationClip>>,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
/// FlagComponent for dynamically spawned scenes
|
||||
pub(crate) struct SpawnedRootProcessed;
|
||||
|
@ -22,16 +15,16 @@ pub(crate) struct SpawnedRootProcessed;
|
|||
// - 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(
|
||||
// all_children: Query<(Entity, &Children)>,
|
||||
//
|
||||
unprocessed_entities: Query<
|
||||
(Entity, &Children, &Name, &Parent, &Original),
|
||||
(With<SpawnedRoot>, Without<SpawnedRootProcessed>),
|
||||
>,
|
||||
mut commands: Commands,
|
||||
|
||||
// FIXME: should be done at a more generic gltf level
|
||||
animation_helpers: Query<&AnimationHelper>,
|
||||
added_animation_helpers: Query<(Entity, &AnimationPlayer), Added<AnimationPlayer>>,
|
||||
// FIXME: not sure , but might be better if done at a more generic gltf level
|
||||
animations: Query<&Animations>,
|
||||
added_animation_players: Query<(Entity, &Parent), Added<AnimationPlayer>>,
|
||||
) {
|
||||
/*
|
||||
currently we have
|
||||
|
@ -85,13 +78,23 @@ pub(crate) fn update_spawned_root_first_child(
|
|||
// parent is either the world or an entity with a marker (BlueprintName)
|
||||
commands.entity(parent.get()).add_child(*root_entity);
|
||||
|
||||
let matching_animation_helper = animation_helpers.get(scene_instance);
|
||||
let matching_animations = animations.get(scene_instance);
|
||||
|
||||
if let Ok(anim_helper) = matching_animation_helper {
|
||||
for (added, _) in added_animation_helpers.iter() {
|
||||
commands.entity(added).insert(AnimationHelper {
|
||||
named_animations: anim_helper.named_animations.clone(),
|
||||
});
|
||||
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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
# Animation demo
|
||||
|
||||
Example on how to control animations & how to use the provided helpers
|
||||
Two animated blueprints are provided: Bevy's Fox, and a Robot I created.
|
||||
|
||||
There is almost no boilerplate, other than setting up ```Bevy_gltf_blueprints```
|
||||
|
||||
If you want to jump straight to controlling the imported animations, jump to :
|
||||
[]('./game/in_game.rs#86')
|
||||
|
||||
## Running this example
|
||||
|
||||
```
|
||||
cargo run --example animation --features bevy/dynamic_linking
|
||||
```
|
|
@ -0,0 +1,5 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_asset_loader::prelude::*;
|
||||
|
||||
#[derive(AssetCollection, Resource)]
|
||||
pub struct CoreAssets {}
|
|
@ -0,0 +1,13 @@
|
|||
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<Scene>,
|
||||
|
||||
#[asset(key = "models", collection(typed, mapped))]
|
||||
pub models: HashMap<String, Handle<Gltf>>,
|
||||
}
|
|
@ -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,
|
||||
"animation/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,
|
||||
"animation/assets_game.assets.ron",
|
||||
)
|
||||
.add_collection_to_loading_state::<_, GameAssets>(AppState::AppLoading);
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
pub mod camera;
|
||||
use bevy_rapier3d::prelude::Velocity;
|
||||
pub use camera::*;
|
||||
|
||||
pub mod lighting;
|
||||
pub use lighting::*;
|
||||
|
||||
pub mod relationships;
|
||||
pub use relationships::*;
|
||||
|
||||
pub mod physics;
|
||||
pub use physics::*;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_gltf_blueprints::*;
|
||||
|
||||
pub struct CorePlugin;
|
||||
impl Plugin for CorePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugins((
|
||||
LightingPlugin,
|
||||
CameraPlugin,
|
||||
PhysicsPlugin,
|
||||
BlueprintsPlugin {
|
||||
library_folder: "animation/models/library".into(),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
use bevy::prelude::{info, ResMut};
|
||||
use bevy_rapier3d::prelude::RapierConfiguration;
|
||||
|
||||
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;
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,302 @@
|
|||
use bevy_rapier3d::prelude::Velocity;
|
||||
use rand::Rng;
|
||||
use std::time::Duration;
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::{
|
||||
assets::GameAssets,
|
||||
state::{GameState, InAppRunning},
|
||||
};
|
||||
use bevy_gltf_blueprints::{
|
||||
AnimationPlayerLink, Animations, BluePrintBundle, BlueprintName, GameWorldTag,
|
||||
};
|
||||
|
||||
use super::{Fox, Player, Robot};
|
||||
|
||||
pub fn setup_game(
|
||||
mut commands: Commands,
|
||||
game_assets: Res<GameAssets>,
|
||||
mut next_game_state: ResMut<NextState<GameState>>,
|
||||
) {
|
||||
println!("setting up all stuff");
|
||||
commands.insert_resource(AmbientLight {
|
||||
color: Color::WHITE,
|
||||
brightness: 0.2,
|
||||
});
|
||||
// here we actually spawn our game world/level
|
||||
|
||||
commands.spawn((
|
||||
SceneBundle {
|
||||
scene: game_assets.world.clone(),
|
||||
..default()
|
||||
},
|
||||
bevy::prelude::Name::from("world"),
|
||||
GameWorldTag,
|
||||
InAppRunning,
|
||||
));
|
||||
|
||||
next_game_state.set(GameState::InGame)
|
||||
}
|
||||
|
||||
pub fn spawn_test(
|
||||
keycode: Res<Input<KeyCode>>,
|
||||
mut commands: Commands,
|
||||
|
||||
mut game_world: Query<(Entity, &Children), With<GameWorldTag>>,
|
||||
) {
|
||||
if keycode.just_pressed(KeyCode::T) {
|
||||
let world = game_world.single_mut();
|
||||
let world = world.1[0];
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let range = 8.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("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)),
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// example of changing animation of entities based on proximity to the player, for "fox" entities (Tag component)
|
||||
pub fn animation_change_on_proximity_foxes(
|
||||
players: Query<&GlobalTransform, With<Player>>,
|
||||
animated_foxes: Query<(&GlobalTransform, &AnimationPlayerLink, &Animations), With<Fox>>,
|
||||
|
||||
mut animation_players: Query<&mut AnimationPlayer>,
|
||||
) {
|
||||
for player_transforms in players.iter() {
|
||||
for (fox_tranforms, link, animations) in animated_foxes.iter() {
|
||||
let distance = player_transforms
|
||||
.translation()
|
||||
.distance(fox_tranforms.translation());
|
||||
let mut anim_name = "Walk";
|
||||
if distance < 8.5 {
|
||||
anim_name = "Run";
|
||||
} else if distance >= 8.5 && distance < 10.0 {
|
||||
anim_name = "Walk";
|
||||
} else if distance >= 10.0 && distance < 15.0 {
|
||||
anim_name = "Survey";
|
||||
}
|
||||
// now play the animation based on the chosen animation name
|
||||
let mut animation_player = animation_players.get_mut(link.0).unwrap();
|
||||
animation_player
|
||||
.play_with_transition(
|
||||
animations
|
||||
.named_animations
|
||||
.get(anim_name)
|
||||
.expect("animation name should be in the list")
|
||||
.clone(),
|
||||
Duration::from_secs(3),
|
||||
)
|
||||
.repeat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// example of changing animation of entities based on proximity to the player, this time for the "robot" entities (Tag component)
|
||||
pub fn animation_change_on_proximity_robots(
|
||||
players: Query<&GlobalTransform, With<Player>>,
|
||||
animated_robots: Query<(&GlobalTransform, &AnimationPlayerLink, &Animations), With<Robot>>,
|
||||
|
||||
mut animation_players: Query<&mut AnimationPlayer>,
|
||||
) {
|
||||
for player_transforms in players.iter() {
|
||||
for (robot_tranforms, link, animations) in animated_robots.iter() {
|
||||
let distance = player_transforms
|
||||
.translation()
|
||||
.distance(robot_tranforms.translation());
|
||||
|
||||
let mut anim_name = "Idle";
|
||||
if distance < 8.5 {
|
||||
anim_name = "Jump";
|
||||
} else if distance >= 8.5 && distance < 10.0 {
|
||||
anim_name = "Scan";
|
||||
} else if distance >= 10.0 && distance < 15.0 {
|
||||
anim_name = "Idle";
|
||||
}
|
||||
|
||||
// now play the animation based on the chosen animation name
|
||||
let mut animation_player = animation_players.get_mut(link.0).unwrap();
|
||||
animation_player
|
||||
.play_with_transition(
|
||||
animations
|
||||
.named_animations
|
||||
.get(anim_name)
|
||||
.expect("animation name should be in the list")
|
||||
.clone(),
|
||||
Duration::from_secs(3),
|
||||
)
|
||||
.repeat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn animation_control(
|
||||
animated_enemies: Query<(&AnimationPlayerLink, &Animations), With<Robot>>,
|
||||
animated_foxes: Query<(&AnimationPlayerLink, &Animations), With<Fox>>,
|
||||
|
||||
mut animation_players: Query<&mut AnimationPlayer>,
|
||||
|
||||
keycode: Res<Input<KeyCode>>,
|
||||
// mut entities_with_animations : Query<(&mut AnimationPlayer, &mut Animations)>,
|
||||
) {
|
||||
// robots
|
||||
if keycode.just_pressed(KeyCode::B) {
|
||||
for (link, animations) in animated_enemies.iter() {
|
||||
let mut animation_player = animation_players.get_mut(link.0).unwrap();
|
||||
let anim_name = "Scan";
|
||||
animation_player
|
||||
.play_with_transition(
|
||||
animations
|
||||
.named_animations
|
||||
.get(anim_name)
|
||||
.expect("animation name should be in the list")
|
||||
.clone(),
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.repeat();
|
||||
}
|
||||
}
|
||||
|
||||
// foxes
|
||||
if keycode.just_pressed(KeyCode::W) {
|
||||
for (link, animations) in animated_foxes.iter() {
|
||||
let mut animation_player = animation_players.get_mut(link.0).unwrap();
|
||||
let anim_name = "Walk";
|
||||
animation_player
|
||||
.play_with_transition(
|
||||
animations
|
||||
.named_animations
|
||||
.get(anim_name)
|
||||
.expect("animation name should be in the list")
|
||||
.clone(),
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.repeat();
|
||||
}
|
||||
}
|
||||
|
||||
if keycode.just_pressed(KeyCode::X) {
|
||||
for (link, animations) in animated_foxes.iter() {
|
||||
let mut animation_player = animation_players.get_mut(link.0).unwrap();
|
||||
let anim_name = "Run";
|
||||
animation_player
|
||||
.play_with_transition(
|
||||
animations
|
||||
.named_animations
|
||||
.get(anim_name)
|
||||
.expect("animation name should be in the list")
|
||||
.clone(),
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.repeat();
|
||||
}
|
||||
}
|
||||
|
||||
if keycode.just_pressed(KeyCode::C) {
|
||||
for (link, animations) in animated_foxes.iter() {
|
||||
let mut animation_player = animation_players.get_mut(link.0).unwrap();
|
||||
let anim_name = "Survey";
|
||||
animation_player
|
||||
.play_with_transition(
|
||||
animations
|
||||
.named_animations
|
||||
.get(anim_name)
|
||||
.expect("animation name should be in the list")
|
||||
.clone(),
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.repeat();
|
||||
}
|
||||
}
|
||||
|
||||
/* Improveement ideas for the future
|
||||
// a bit more ideal API
|
||||
if keycode.just_pressed(KeyCode::B) {
|
||||
for (animation_player, animations) in animated_enemies.iter() {
|
||||
let anim_name = "Scan";
|
||||
if animations.named_animations.contains_key(anim_name) {
|
||||
let clip = animations.named_animations.get(anim_name).unwrap();
|
||||
animation_player.play_with_transition(clip.clone(), Duration::from_secs(5)).repeat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// even better API
|
||||
if keycode.just_pressed(KeyCode::B) {
|
||||
for (animation_player, animations) in animated_enemies.iter() {
|
||||
animation_player.play_with_transition("Scan", Duration::from_secs(5)).repeat(); // with a merged animationPlayer + animations storage
|
||||
// alternative, perhaps more realistic, and better seperation of concerns
|
||||
animation_player.play_with_transition(animations, "Scan", Duration::from_secs(5)).repeat();
|
||||
|
||||
}
|
||||
}*/
|
||||
|
||||
/*for (mut anim_player, animations) in entities_with_animations.iter_mut(){
|
||||
|
||||
if keycode.just_pressed(KeyCode::W) {
|
||||
let anim_name = "Walk";
|
||||
if animations.named_animations.contains_key(anim_name) {
|
||||
let clip = animations.named_animations.get(anim_name).unwrap();
|
||||
anim_player.play_with_transition(clip.clone(), Duration::from_secs(5)).repeat();
|
||||
}
|
||||
}
|
||||
if keycode.just_pressed(KeyCode::X) {
|
||||
let anim_name = "Run";
|
||||
if animations.named_animations.contains_key(anim_name) {
|
||||
let clip = animations.named_animations.get(anim_name).unwrap();
|
||||
anim_player.play_with_transition(clip.clone(), Duration::from_secs(5)).repeat();
|
||||
}
|
||||
}
|
||||
if keycode.just_pressed(KeyCode::C) {
|
||||
let anim_name = "Survey";
|
||||
if animations.named_animations.contains_key(anim_name) {
|
||||
let clip = animations.named_animations.get(anim_name).unwrap();
|
||||
anim_player.play_with_transition(clip.clone(), Duration::from_secs(5)).repeat();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if keycode.just_pressed(KeyCode::S) {
|
||||
let anim_name = "Scan";
|
||||
if animations.named_animations.contains_key(anim_name) {
|
||||
let clip = animations.named_animations.get(anim_name).unwrap();
|
||||
anim_player.play_with_transition(clip.clone(), Duration::from_secs(5)).repeat();
|
||||
}
|
||||
}
|
||||
if keycode.just_pressed(KeyCode::I) {
|
||||
let anim_name = "Idle";
|
||||
if animations.named_animations.contains_key(anim_name) {
|
||||
let clip = animations.named_animations.get(anim_name).unwrap();
|
||||
anim_player.play_with_transition(clip.clone(), Duration::from_secs(5)).repeat();
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
use bevy::prelude::*;
|
||||
|
||||
use crate::state::{AppState, GameState, InMainMenu};
|
||||
|
||||
pub fn setup_main_menu(mut commands: Commands) {
|
||||
commands.spawn((Camera2dBundle::default(), InMainMenu));
|
||||
|
||||
commands.spawn((
|
||||
TextBundle::from_section(
|
||||
"SOME GAME TITLE !!",
|
||||
TextStyle {
|
||||
//font: asset_server.load("fonts/FiraMono-Medium.ttf"),
|
||||
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 T once the game is started for demo spawning)",
|
||||
TextStyle {
|
||||
//font: asset_server.load("fonts/FiraMono-Medium.ttf"),
|
||||
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: asset_server.load("fonts/FiraMono-Medium.ttf"),
|
||||
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: asset_server.load("fonts/FiraMono-Medium.ttf"),
|
||||
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(bla: Query<Entity, With<InMainMenu>>, mut commands: Commands) {
|
||||
for bli in bla.iter() {
|
||||
commands.entity(bli).despawn_recursive();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main_menu(
|
||||
keycode: Res<Input<KeyCode>>,
|
||||
|
||||
mut next_app_state: ResMut<NextState<AppState>>,
|
||||
// mut next_game_state: ResMut<NextState<GameState>>,
|
||||
) {
|
||||
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() })
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
pub mod in_game;
|
||||
pub use in_game::*;
|
||||
|
||||
pub mod in_main_menu;
|
||||
pub use in_main_menu::*;
|
||||
|
||||
pub mod picking;
|
||||
pub use picking::*;
|
||||
|
||||
use crate::{
|
||||
insert_dependant_component,
|
||||
state::{AppState, GameState},
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
use bevy_rapier3d::prelude::*;
|
||||
|
||||
// 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;
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug)]
|
||||
#[reflect(Component)]
|
||||
/// Demo marker component
|
||||
pub struct Fox;
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug)]
|
||||
#[reflect(Component)]
|
||||
/// Demo marker component
|
||||
pub struct Robot;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collision tests/debug
|
||||
pub fn test_collision_events(
|
||||
mut collision_events: EventReader<CollisionEvent>,
|
||||
mut contact_force_events: EventReader<ContactForceEvent>,
|
||||
) {
|
||||
for collision_event in collision_events.iter() {
|
||||
println!("collision");
|
||||
match collision_event {
|
||||
CollisionEvent::Started(_entity1, _entity2, _) => {
|
||||
println!("collision started")
|
||||
}
|
||||
CollisionEvent::Stopped(_entity1, _entity2, _) => {
|
||||
println!("collision ended")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for contact_force_event in contact_force_events.iter() {
|
||||
println!("Received contact force event: {:?}", contact_force_event);
|
||||
}
|
||||
}
|
||||
|
||||
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>()
|
||||
.register_type::<Robot>()
|
||||
.register_type::<Fox>()
|
||||
// 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)
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
player_move_demo,
|
||||
spawn_test,
|
||||
animation_control,
|
||||
animation_change_on_proximity_foxes,
|
||||
animation_change_on_proximity_robots,
|
||||
)
|
||||
.run_if(in_state(GameState::InGame)),
|
||||
)
|
||||
.add_systems(OnEnter(AppState::MenuRunning), setup_main_menu)
|
||||
.add_systems(OnExit(AppState::MenuRunning), teardown_main_menu)
|
||||
.add_systems(Update, (main_menu))
|
||||
.add_systems(OnEnter(AppState::AppRunning), setup_game);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
use super::Player;
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[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, //.run_if(in_state(AppState::Running)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
use bevy::{asset::ChangeWatcher, 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 {
|
||||
// This tells the AssetServer to watch for changes to assets.
|
||||
// It enables our scenes to automatically reload in game when we modify their files.
|
||||
// practical in our case to be able to edit the shaders without needing to recompile
|
||||
// watch_for_changes: ChangeWatcher::with_delay(Duration::from_millis(50)), : FIXME: breaks scene save/loading
|
||||
..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();
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
use bevy::app::AppExit;
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default, States)]
|
||||
pub enum AppState {
|
||||
#[default]
|
||||
CoreLoading,
|
||||
MenuRunning,
|
||||
AppLoading,
|
||||
AppRunning,
|
||||
AppEnding,
|
||||
|
||||
// FIXME: not sure
|
||||
LoadingGame,
|
||||
}
|
||||
|
||||
#[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 InAppLoading;
|
||||
#[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;
|
||||
|
||||
pub struct StatePlugin;
|
||||
impl Plugin for StatePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_state::<AppState>().add_state::<GameState>();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug)]
|
||||
#[reflect(Component)]
|
||||
struct UnitTest;
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug, Deref, DerefMut)]
|
||||
#[reflect(Component)]
|
||||
struct TuppleTestF32(f32);
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug, Deref, DerefMut)]
|
||||
#[reflect(Component)]
|
||||
struct TuppleTestU64(u64);
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug, Deref, DerefMut)]
|
||||
#[reflect(Component)]
|
||||
pub struct TuppleTestStr(String);
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug)]
|
||||
#[reflect(Component)]
|
||||
struct TuppleTest2(f32, u64, String);
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug)]
|
||||
#[reflect(Component)]
|
||||
struct TuppleTestBool(bool);
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug)]
|
||||
#[reflect(Component)]
|
||||
struct TuppleVec2(Vec2);
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug)]
|
||||
#[reflect(Component)]
|
||||
struct TuppleVec3(Vec3);
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug)]
|
||||
#[reflect(Component)]
|
||||
struct TuppleVec(Vec<String>);
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug)]
|
||||
#[reflect(Component)]
|
||||
struct TuppleTestColor(Color);
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug)]
|
||||
#[reflect(Component)]
|
||||
struct BasicTest {
|
||||
a: f32,
|
||||
b: u64,
|
||||
c: String,
|
||||
}
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug)]
|
||||
#[reflect(Component)]
|
||||
pub enum EnumTest {
|
||||
Metal,
|
||||
Wood,
|
||||
Rock,
|
||||
Cloth,
|
||||
Squishy,
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
pub struct ComponentsTestPlugin;
|
||||
impl Plugin for ComponentsTestPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.register_type::<BasicTest>()
|
||||
.register_type::<UnitTest>()
|
||||
.register_type::<TuppleTestF32>()
|
||||
.register_type::<TuppleTestU64>()
|
||||
.register_type::<TuppleTestStr>()
|
||||
.register_type::<TuppleTestBool>()
|
||||
.register_type::<TuppleTest2>()
|
||||
.register_type::<TuppleVec2>()
|
||||
.register_type::<TuppleVec3>()
|
||||
.register_type::<EnumTest>()
|
||||
.register_type::<TuppleTestColor>()
|
||||
.register_type::<TuppleVec>()
|
||||
.register_type::<Vec<String>>();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue