feat(examples): added basics of multi-level example (#46)

* added basic assets & logic for scene switching upon reaching a trigger zone ! Works !
 * added toggling of physics debug
 * added handling of nested colliders for LevelTransitions
 * fix/ addition for point light intensity scaling & shadows
 * closes #40
This commit is contained in:
Mark Moissette 2023-11-14 13:24:59 +01:00 committed by GitHub
parent dd4a04f237
commit 76f6a45fc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 6922 additions and 2 deletions

12
Cargo.lock generated
View File

@ -797,6 +797,18 @@ dependencies = [
"rand",
]
[[package]]
name = "bevy_gltf_blueprints_multiple_levels"
version = "0.3.0"
dependencies = [
"bevy",
"bevy_asset_loader",
"bevy_editor_pls",
"bevy_gltf_blueprints",
"bevy_rapier3d",
"rand",
]
[[package]]
name = "bevy_gltf_components"
version = "0.2.0"

View File

@ -5,8 +5,8 @@ members = [
"examples/bevy_gltf_components/basic/",
"examples/bevy_gltf_blueprints/basic/",
"examples/bevy_gltf_blueprints/basic_xpbd_physics/",
"examples/bevy_gltf_blueprints/animation/"
"examples/bevy_gltf_blueprints/animation/",
"examples/bevy_gltf_blueprints/multiple_levels/"
]
resolver = "2"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,13 @@
[package]
name = "bevy_gltf_blueprints_multiple_levels"
version = "0.3.0"
edition = "2021"
license = "MIT OR Apache-2.0"
[dependencies]
bevy="0.12"
bevy_gltf_blueprints = { path = "../../../crates/bevy_gltf_blueprints" }
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"

View File

@ -0,0 +1,12 @@
# Multiple levels example/demo
This example showcases multiple levels, and using triggers to load another level.
It currently has NO state persistence between levels.
You can press "D" to toggle the physics debug view.
## Running this example
```
cargo run --features bevy/dynamic_linking
```

View File

@ -0,0 +1,9 @@
({
"world":File (path: "models/StartLevel.glb"),
"level1":File (path: "models/Level1.glb"),
"level2":File (path: "models/Level2.glb"),
"models": Folder (
path: "models/library",
),
})

View File

@ -0,0 +1,485 @@
(
resources: {},
entities: {
20: (
components: {
"bevy_render::camera::projection::Projection": Perspective((
fov: 0.3995965,
aspect_ratio: 1.7777778,
near: 0.1,
far: 100.0,
)),
"bevy_render::primitives::Frustum": (),
"bevy_transform::components::transform::Transform": (
translation: (
x: 34.821884,
y: 49.024857,
z: -36.79615,
),
rotation: (-0.1694689, 0.82838506, 0.40884802, 0.3433684),
scale: (
x: 1.0,
y: 1.0,
z: 1.0,
),
),
"bevy_core_pipeline::tonemapping::Tonemapping": BlenderFilmic,
"bevy_core_pipeline::tonemapping::DebandDither": Enabled,
"bevy_render::view::ColorGrading": (
exposure: 0.0,
gamma: 1.0,
pre_saturation: 1.0,
post_saturation: 1.0,
),
"bevy_core::name::Name": (
hash: 17702508670109176045,
name: "Camera",
),
"advanced::core::camera::camera_tracking::CameraTrackingOffset": ((
x: 26.0,
y: 48.0,
z: -26.0,
)),
"bevy_pbr::light::ClusterConfig": FixedZ(
total: 4096,
z_slices: 24,
z_config: (
first_slice_depth: 5.0,
far_z_mode: MaxLightRange,
),
dynamic_resizing: true,
),
"bevy_core_pipeline::bloom::settings::BloomSettings": (
intensity: 0.01,
low_frequency_boost: 0.7,
low_frequency_boost_curvature: 0.95,
high_pass_frequency: 1.0,
prefilter_settings: (
threshold: 0.0,
threshold_softness: 0.0,
),
composite_mode: Additive,
),
},
),
34: (
components: {
"bevy_transform::components::transform::Transform": (
translation: (
x: 4.697565,
y: 1.5983224,
z: 8.962274,
),
rotation: (0.000000000000000031724054, -0.00000000000000000000647681, -0.000013119204, 1.0),
scale: (
x: 1.0,
y: 1.0,
z: 1.0,
),
),
"bevy_core::name::Name": (
hash: 9837288155836662016,
name: "Health_Pickup.001",
),
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
"advanced::game::picking::Pickable": (),
},
),
54: (
components: {
"bevy_transform::components::transform::Transform": (
translation: (
x: 8.799996,
y: 1.02484,
z: -10.799994,
),
rotation: (0.0, 0.0, 0.0, 1.0),
scale: (
x: 1.0,
y: 1.0,
z: 1.0,
),
),
"bevy_core::name::Name": (
hash: 17978181434632022651,
name: "Player",
),
"advanced::core::camera::camera_tracking::CameraTrackable": (),
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Player"),
"advanced::game::Player": (),
"advanced::game::SoundMaterial": Wood,
},
),
60: (
components: {
"bevy_transform::components::transform::Transform": (
translation: (
x: 3.6351967,
y: 1.7298106,
z: -7.313273,
),
rotation: (0.0, 0.0, 0.0, 1.0),
scale: (
x: 1.0,
y: 1.0,
z: 1.0,
),
),
"bevy_core::name::Name": (
hash: 7225506896223411979,
name: "MagicTeapot.001",
),
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("MagicTeapot"),
},
),
64: (
components: {
"bevy_transform::components::transform::Transform": (
translation: (
x: -4.6068983,
y: 1.5983224,
z: -10.579347,
),
rotation: (0.000000000000000031724054, 0.00000000000000000000647681, 0.000013119204, 1.0),
scale: (
x: 1.0,
y: 1.0,
z: 1.0,
),
),
"bevy_core::name::Name": (
hash: 3089896164553476909,
name: "Health_Pickup.002",
),
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
"advanced::game::picking::Pickable": (),
},
),
72: (
components: {
"bevy_transform::components::transform::Transform": (
translation: (
x: -11.560788,
y: 0.0,
z: 7.6554174,
),
rotation: (0.0, 0.0, 0.0, 1.0),
scale: (
x: 1.0,
y: 1.0,
z: 1.0,
),
),
"bevy_core::name::Name": (
hash: 16961132108296874979,
name: "Container.001",
),
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Container"),
"advanced::game::picking::Pickable": (),
},
),
80: (
components: {
"bevy_transform::components::transform::Transform": (
translation: (
x: -21.397858,
y: 0.3833189,
z: -0.32418346,
),
rotation: (0.0, 0.0, 0.0, 1.0),
scale: (
x: 1.0,
y: 1.0,
z: 1.0,
),
),
"bevy_core::name::Name": (
hash: 5104740624378885265,
name: "Container.002",
),
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Container"),
"advanced::game::picking::Pickable": (),
},
),
82: (
components: {
"bevy_transform::components::transform::Transform": (
translation: (
x: 2.9156065,
y: 1.4984571,
z: 2.1909573,
),
rotation: (0.058853183, 0.0726243, 0.2048649, 0.97431636),
scale: (
x: 1.0,
y: 1.0,
z: 1.0,
),
),
"bevy_core::name::Name": (
hash: 107557640935939866,
name: "test5159735758431545549",
),
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
"advanced::game::picking::Pickable": (),
"bevy_rapier3d::dynamics::rigid_body::Velocity": (
linvel: (
x: -1.2580805,
y: -0.39687577,
z: 0.4816798,
),
angvel: (
x: 0.2979751,
y: 0.07926611,
z: 0.8434645,
),
),
},
),
86: (
components: {
"bevy_transform::components::transform::Transform": (
translation: (
x: 0.26087752,
y: 1.5525806,
z: 1.5980839,
),
rotation: (0.059497803, -0.0000018232388, 0.13145457, 0.9895351),
scale: (
x: 1.0,
y: 1.0,
z: 1.0,
),
),
"bevy_core::name::Name": (
hash: 3398656236303073559,
name: "test7470642598731063943",
),
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
"advanced::game::picking::Pickable": (),
"bevy_rapier3d::dynamics::rigid_body::Velocity": (
linvel: (
x: -0.9268077,
y: -0.19806683,
z: 0.41948256,
),
angvel: (
x: 0.26946256,
y: -0.000006710977,
z: 0.5953494,
),
),
},
),
90: (
components: {
"bevy_transform::components::transform::Transform": (
translation: (
x: 2.6515265,
y: 1.5944021,
z: -4.391837,
),
rotation: (-0.030030435, -0.0000006527225, 0.029748484, 0.9991062),
scale: (
x: 1.0,
y: 1.0,
z: 1.0,
),
),
"bevy_core::name::Name": (
hash: 12541900054595385134,
name: "test3938024405863834719",
),
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
"advanced::game::picking::Pickable": (),
"bevy_rapier3d::dynamics::rigid_body::Velocity": (
linvel: (
x: -0.28430828,
y: -0.022357654,
z: -0.2870027,
),
angvel: (
x: -0.17986917,
y: -0.0000035613396,
z: 0.17818078,
),
),
},
),
94: (
components: {
"bevy_transform::components::transform::Transform": (
translation: (
x: -4.2356462,
y: 1.596993,
z: 0.7254991,
),
rotation: (-0.0221751, -0.0000000001891749, 0.011065631, 0.99969286),
scale: (
x: 1.0,
y: 1.0,
z: 1.0,
),
),
"bevy_core::name::Name": (
hash: 6757906322211730861,
name: "test11007490954016878479",
),
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
"advanced::game::picking::Pickable": (),
"bevy_rapier3d::dynamics::rigid_body::Velocity": (
linvel: (
x: -0.21747473,
y: -0.014912919,
z: -0.43581253,
),
angvel: (
x: -0.2727097,
y: -0.0000000034594905,
z: 0.13608481,
),
),
},
),
98: (
components: {
"bevy_transform::components::transform::Transform": (
translation: (
x: 3.1525247,
y: 1.5518407,
z: -2.9611976,
),
rotation: (-0.09219627, 0.1602262, -0.11205085, 0.9763565),
scale: (
x: 1.0,
y: 1.0,
z: 1.0,
),
),
"bevy_core::name::Name": (
hash: 12588565107899185946,
name: "test5980867849331267699",
),
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
"advanced::game::picking::Pickable": (),
"bevy_rapier3d::dynamics::rigid_body::Velocity": (
linvel: (
x: 0.8323179,
y: -0.20597076,
z: -0.68975484,
),
angvel: (
x: -0.37971017,
y: 0.49603412,
z: -0.6079359,
),
),
},
),
4294967310: (
components: {
"bevy_transform::components::transform::Transform": (
translation: (
x: 4.826278,
y: 1.2710563,
z: -3.1997645,
),
rotation: (-0.303028, 0.00000087800436, -0.23889118, 0.9225535),
scale: (
x: 1.0,
y: 1.0,
z: 1.0,
),
),
"bevy_core::name::Name": (
hash: 15533546218717453536,
name: "test12380979123759326444",
),
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
"advanced::game::picking::Pickable": (),
"bevy_rapier3d::dynamics::rigid_body::Velocity": (
linvel: (
x: 1.2146912,
y: -1.1640646,
z: -1.5408095,
),
angvel: (
x: -1.1932359,
y: 0.000002945365,
z: -0.94068503,
),
),
},
),
4294967314: (
components: {
"bevy_transform::components::transform::Transform": (
translation: (
x: 3.9906094,
y: 1.4824095,
z: 2.4394412,
),
rotation: (0.06015042, 0.085218765, 0.2215642, 0.9695509),
scale: (
x: 1.0,
y: 1.0,
z: 1.0,
),
),
"bevy_core::name::Name": (
hash: 2466794778849297109,
name: "test12475628281920299197",
),
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
"advanced::game::picking::Pickable": (),
"bevy_rapier3d::dynamics::rigid_body::Velocity": (
linvel: (
x: -1.0818624,
y: -0.37798148,
z: 0.45334253,
),
angvel: (
x: 0.25961447,
y: 0.14854014,
z: 0.7426717,
),
),
},
),
4294967321: (
components: {
"bevy_transform::components::transform::Transform": (
translation: (
x: 2.2306876,
y: 0.989814,
z: -1.3596333,
),
rotation: (0.30614096, 0.002587511, -0.42789298, 0.8503991),
scale: (
x: 1.0,
y: 1.0,
z: 1.0,
),
),
"bevy_core::name::Name": (
hash: 1545925632270385398,
name: "test15780367212768138828",
),
"bevy_gltf_blueprints::spawn_from_blueprints::BlueprintName": ("Health_Pickup"),
"advanced::game::picking::Pickable": (),
"bevy_rapier3d::dynamics::rigid_body::Velocity": (
linvel: (
x: 1.3027526,
y: -1.8947054,
z: 1.6179247,
),
angvel: (
x: 1.4565696,
y: -0.16299045,
z: -1.3631926,
),
),
},
),
},
)

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 = "level1")]
pub level1: Handle<Gltf>,
#[asset(key = "level2")]
pub level2: 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,81 @@
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;
// this system ensures that the camera is at the correct position immediatly after spawning
pub fn init_camera_track(
mut tracking_cameras: Query<
(&mut Transform, &CameraTrackingOffset),
(
With<Camera>,
With<CameraTrackingOffset>,
Without<CameraTrackable>,
),
>,
camera_tracked: Query<&Transform, (With<CameraTrackable>, Added<CameraTrackable>)>,
) {
for (mut camera_transform, tracking_offset) in tracking_cameras.iter_mut() {
for tracked_transform in camera_tracked.iter() {
println!("ADDED tracking camera");
let target_position = tracked_transform.translation + tracking_offset.0;
camera_transform.translation = target_position;
*camera_transform = camera_transform.looking_at(tracked_transform.translation, Vec3::Y);
}
}
}
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,25 @@
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),
init_camera_track,
camera_track,
),
);
}
}

View File

@ -0,0 +1,31 @@
use bevy::prelude::*;
use bevy::pbr::{CascadeShadowConfig, CascadeShadowConfigBuilder};
pub fn lighting_replace_proxies(
mut added_dirights: Query<(Entity, &mut DirectionalLight), Added<DirectionalLight>>,
mut added_spotlights: Query<&mut SpotLight, Added<SpotLight>>,
mut added_pointlights: Query<&mut PointLight, Added<PointLight>>,
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;
}
for mut light in added_pointlights.iter_mut() {
light.intensity *= 0.001; // arbitrary/ eyeballed to match the levels of Blender
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,32 @@
pub mod camera;
pub use camera::*;
pub mod lighting;
pub use lighting::*;
pub mod relationships;
pub use relationships::*;
pub mod physics;
pub use physics::*;
// pub mod save_load;
// pub use save_load::*;
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,
// SaveLoadPlugin,
BlueprintsPlugin {
library_folder: "models/library".into(),
},
));
}
}

View File

@ -0,0 +1,21 @@
use bevy::prelude::{info, Input, KeyCode, Res, 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,36 @@
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>()
.add_systems(
Update,
physics_replace_proxies.after(GltfBlueprintsSet::AfterSpawn),
)
// physics controls
.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,102 @@
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;
}
// also entities marked with collider names are actually children colliders
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,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,218 @@
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

@ -0,0 +1,70 @@
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

@ -0,0 +1,137 @@
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

@ -0,0 +1,14 @@
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

@ -0,0 +1,87 @@
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

@ -0,0 +1,85 @@
use bevy::prelude::*;
use crate::{
assets::GameAssets,
state::{GameState, InAppRunning},
};
use bevy_gltf_blueprints::{BluePrintBundle, BlueprintName, GameWorldTag};
use bevy_rapier3d::prelude::Velocity;
use rand::Rng;
pub fn setup_game(
mut commands: Commands,
game_assets: Res<GameAssets>,
models: Res<Assets<bevy::gltf::Gltf>>,
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 {
// note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene
scene: models
.get(game_assets.world.id())
.expect("main level should have been loaded")
.scenes[0]
.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 = 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()),
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)),
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);
}
}

View File

@ -0,0 +1,113 @@
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>>,
// 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,86 @@
use crate::{assets::GameAssets, state::InAppRunning};
use bevy::prelude::*;
use bevy_gltf_blueprints::GameWorldTag;
use bevy_rapier3d::prelude::*;
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct LevelTransition {
pub target: String,
}
// very barebones example of triggering level transitions
pub fn trigger_level_transition(
mut collision_events: EventReader<CollisionEvent>,
level_transition_triggers: Query<&LevelTransition>,
parents: Query<&Parent>,
mut commands: Commands,
game_assets: Res<GameAssets>,
models: Res<Assets<bevy::gltf::Gltf>>,
game_world: Query<(Entity, &GameWorldTag)>,
) {
for collision_event in collision_events.read() {
match collision_event {
CollisionEvent::Started(entity1, entity2, _) => {
// 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()
|| level_transition_triggers.get(entity1_parent.get()).is_ok()
|| level_transition_triggers.get(entity2_parent.get()).is_ok()
{
println!("collision started, we can transition to level");
let transition_trigger;
if level_transition_triggers.get(*entity1).is_ok() {
transition_trigger = level_transition_triggers.get(*entity1).unwrap();
} else if level_transition_triggers.get(*entity2).is_ok() {
transition_trigger = level_transition_triggers.get(*entity2).unwrap();
} else if level_transition_triggers.get(entity1_parent.get()).is_ok() {
transition_trigger =
level_transition_triggers.get(entity1_parent.get()).unwrap();
} else {
transition_trigger =
level_transition_triggers.get(entity2_parent.get()).unwrap();
}
let current_game_world = game_world.single();
// remove current level/world
info!("despawning current level");
commands.entity(current_game_world.0).despawn_recursive();
let target_level = &transition_trigger.target;
let level;
println!("target level {}", target_level);
if target_level == "Level1" {
level = &game_assets.level1;
} else if (target_level == "Level2") {
level = &game_assets.level2;
} else {
level = &game_assets.world;
}
info!("spawning new level");
commands.spawn((
SceneBundle {
// note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene
scene: models
.get(level.id())
.expect("main level should have been loaded")
.scenes[0]
.clone(),
..default()
},
bevy::prelude::Name::from("world"),
GameWorldTag,
InAppRunning,
));
}
}
CollisionEvent::Stopped(_entity1, _entity2, _) => {
// println!("collision ended")
}
}
}
}

View File

@ -0,0 +1,121 @@
pub mod in_game;
pub use in_game::*;
pub mod in_main_menu;
pub use in_main_menu::*;
pub mod picking;
pub use picking::*;
pub mod level_transitions;
pub use level_transitions::*;
use crate::{
assets::GameAssets,
insert_dependant_component,
state::{AppState, GameState, InAppRunning},
};
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;
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.read() {
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.read() {
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::<LevelTransition>()
// 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,
(
// insert_dependant_component::<Player, ShouldBeWithPlayer>,
player_move_demo, //.run_if(in_state(AppState::Running)),
// test_collision_events,
spawn_test,
trigger_level_transition,
)
.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);
}
}

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,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>();
}
}

View File

@ -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>>();
}
}