//! Runtime game crate: FPS player, rendering, and starter world. #![allow(clippy::type_complexity)] #![cfg_attr( feature = "hot-reload", allow( clippy::too_many_arguments, reason = "hot-lib-reloader wrappers preserve Bevy system signatures and macro spans" ) )] pub mod animation; pub mod audio; pub mod editor_ext; pub mod launch; pub mod navigation; mod scene_bootstrap; mod schema_world_loader; pub use scene_bootstrap::ProjectDefaultLevelRoot; pub mod player { //! Player types re-exported from the stable sim crate. pub use sim::{CameraSensitivity, PlayerCamera}; } pub mod rendering { //! Rendering helpers re-exported from the hot-reloadable implementation. pub use game_hot::{ apply_camera_render_profile, apply_project_camera_fx, camera_auto_exposure_active, clear_viewport_camera_stack, effective_gi_path_for_camera, effective_viewport_gi_path, effective_viewport_render_stack, has_local_shadow_lights, hdr_enabled_profile, patch_camera_effects, resolve_viewport_camera_owner, strip_project_camera_fx, sync_optional_rendering_fx, sync_project_atmosphere, sync_viewport_camera_stack, world_has_local_shadow_lights, FullscreenEffectsPlugin, ProjectAtmosphere, SolariRaytracingSceneStats, SolariRenderingPlugin, ViewportCameraOwner, ViewportFxSnapshot, ViewportStackApply, HDR_ENV_VAR, }; } use bevy::prelude::*; use settings::ProjectSettingsPlugin; use sim::SimPlugin; pub use sim::{GameInputEnabled, GameInputFocused, SimEnabled}; /// When set, the in-process editor owns [`apply_project_camera_fx`] on a single camera. #[derive(Resource, Debug, Clone, Copy, Default)] pub struct GameRenderBootstrap { pub defer_player_camera_fx: bool, } /// Scene-startup controls for a host that embeds [`GamePlugin`]. #[derive(Resource, Debug, Clone, Copy, Default)] pub struct GameSceneBootstrap { /// The host owns initial scene materialization instead of the standalone runtime. pub defer_default_level: bool, } #[cfg(not(feature = "hot-reload"))] mod systems { pub use game_hot::{ apply_ambient_light, apply_player_intent, grab_cursor, move_and_slide, setup_project_camera_effects, spawn_level, spawn_player, spawn_sun, sync_player_physics_position, sync_project_sun_visibility, sync_scene_directional_shadow_cascades, tag_player_camera, toggle_cursor_grab, update_camera_pitch, update_eye_height, update_local_player_intent, }; } #[cfg(feature = "hot-reload")] mod systems { pub use crate::hot::{ apply_ambient_light, apply_player_intent, grab_cursor, move_and_slide, setup_project_camera_effects, spawn_level, spawn_player, spawn_sun, sync_player_physics_position, sync_project_sun_visibility, sync_scene_directional_shadow_cascades, tag_player_camera, toggle_cursor_grab, update_camera_pitch, update_eye_height, update_local_player_intent, }; } #[cfg(feature = "hot-reload")] #[hot_lib_reloader::hot_module(dylib = "game_hot", lib_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/debug"))] mod hot { use avian3d::prelude::*; use bevy::input::mouse::AccumulatedMouseMotion; use bevy::light::atmosphere::ScatteringMedium; use bevy::light::CascadeShadowConfig; use bevy::prelude::*; use bevy::time::Fixed; use bevy::window::{CursorOptions, PrimaryWindow}; use game_hot::{ProjectAtmosphere, SolariRaytracingSceneStats}; use protocol::PlayerInputIntent; use settings::{ ActiveCameraRenderProfile, ProjectRenderCamera, ProjectSettings, RenderingCapabilities, SimTuning, }; use shared::{AuthoringComponentStates, InspectorOrder, LevelObject, LightDesc, ProjectSun}; use sim::{ CameraSensitivity, Crouching, GameInputEnabled, GameInputFocused, Grounded, JumpState, Player, PlayerCamera, PlayerVelocity, PlayerYawSensitivity, }; hot_functions_from_file!("crates/game_hot/src/sim_systems.rs"); // move_and_slide, sync_player_physics_position, apply_player_intent hot_functions_from_file!("crates/game_hot/src/player/spawn.rs"); hot_functions_from_file!("crates/game_hot/src/player/input.rs"); hot_functions_from_file!("crates/game_hot/src/player/camera.rs"); hot_functions_from_file!("crates/game_hot/src/rendering/camera_fx.rs"); hot_functions_from_file!("crates/game_hot/src/world/mod.rs"); hot_functions_from_file!("crates/game_hot/src/world/level.rs"); hot_functions_from_file!("crates/game_hot/src/world/lighting.rs"); #[lib_change_subscription] pub fn subscribe() -> hot_lib_reloader::LibReloadObserver {} } #[cfg(feature = "hot-reload")] pub fn hot_reload_subscribe() -> hot_lib_reloader::LibReloadObserver { hot::subscribe() } fn sim_enabled(enabled: Res) -> bool { enabled.0 } fn game_input_enabled(enabled: Res) -> bool { enabled.0 } fn register_level_bootstrap(app: &mut App, defer_default_level: bool) { if defer_default_level { // SceneIo owns editor startup and retains the generated arena as its // failure fallback until the authored document is materialized. app.add_systems(Startup, systems::spawn_level); } else { app.add_systems(Startup, scene_bootstrap::spawn_project_default_level); } } fn register_game_systems( app: &mut App, render_bootstrap: GameRenderBootstrap, scene_bootstrap: GameSceneBootstrap, ) { app.add_systems( FixedUpdate, ( systems::apply_player_intent, systems::move_and_slide, systems::sync_player_physics_position, ) .chain() .run_if(sim_enabled), ) .add_systems(Startup, systems::spawn_player) .add_systems( Startup, systems::grab_cursor .after(systems::spawn_player) .run_if(game_input_enabled), ) .add_systems( Update, ( systems::toggle_cursor_grab, systems::update_local_player_intent, systems::update_camera_pitch, systems::update_eye_height, ) .chain() .run_if(game_input_enabled), ) .add_systems( Startup, (systems::apply_ambient_light, systems::spawn_sun).chain(), ); register_level_bootstrap(app, scene_bootstrap.defer_default_level); app.add_systems( Update, ( systems::sync_project_sun_visibility, systems::sync_scene_directional_shadow_cascades, ), ); if !render_bootstrap.defer_player_camera_fx { app.add_systems( PostStartup, ( systems::tag_player_camera, systems::setup_project_camera_effects, ) .chain(), ); } } /// Runtime game systems and content, without Bevy's platform/window plugins. pub struct GamePlugin; impl Plugin for GamePlugin { fn build(&self, app: &mut App) { let render_bootstrap = app .world() .get_resource::() .copied() .unwrap_or_default(); let scene_bootstrap = app .world() .get_resource::() .copied() .unwrap_or_default(); app.register_type::() .init_resource::() .init_resource::() .add_plugins(schema_world_loader::SchemaWorldAssetPlugin) .add_plugins((ProjectSettingsPlugin, SimPlugin, bevy_ufbx::FbxPlugin)) .add_plugins((audio::GameAudioPlugin, animation::GameAnimationPlugin)) .add_plugins(game_hot::SolariRenderingPlugin) .add_plugins(game_hot::AutoExposureRenderingPlugin) .add_plugins(game_hot::FullscreenEffectsPlugin) .insert_resource(ClearColor(Color::srgb(0.0, 0.0, 0.0))); register_game_systems(app, render_bootstrap, scene_bootstrap); } } /// Called by the editor after swapping the hot dylib. pub fn on_hot_reload(world: &mut World) { game_hot::on_hot_reload(world); } #[cfg(test)] mod startup_tests { use super::*; use bevy::asset::AssetPlugin; use bevy::world_serialization::WorldSerializationPlugin; #[test] fn deferred_bootstrap_leaves_default_level_loading_to_the_host() { let mut app = App::new(); app.add_plugins(( MinimalPlugins, AssetPlugin::default(), WorldSerializationPlugin, )); app.insert_resource(settings::ProjectSettings::default()); register_level_bootstrap(&mut app, true); app.update(); let world = app.world_mut(); let mut runtime_roots = world.query_filtered::>(); assert_eq!(runtime_roots.iter(world).count(), 0); let mut fallback = world.query_filtered::>(); assert!( fallback.iter(world).next().is_some(), "the editor fallback arena remains available until SceneIo loads" ); } }