Add dedicated skinned rendering, pose restoration, shared Material and Material Instance slots, registry-driven components, Surface/Solari integration, transactional schema upgrades, navigation authoring, documentation, and evaluation evidence.
264 lines
9.0 KiB
Rust
264 lines
9.0 KiB
Rust
//! 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::{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<SimEnabled>) -> bool {
|
|
enabled.0
|
|
}
|
|
|
|
fn game_input_enabled(enabled: Res<GameInputEnabled>) -> 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::<GameRenderBootstrap>()
|
|
.copied()
|
|
.unwrap_or_default();
|
|
let scene_bootstrap = app
|
|
.world()
|
|
.get_resource::<GameSceneBootstrap>()
|
|
.copied()
|
|
.unwrap_or_default();
|
|
|
|
app.register_type::<GameInputEnabled>()
|
|
.init_resource::<GameInputEnabled>()
|
|
.init_resource::<GameInputFocused>()
|
|
.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::<Entity, With<scene_bootstrap::ProjectDefaultLevelRoot>>();
|
|
assert_eq!(runtime_roots.iter(world).count(), 0);
|
|
let mut fallback = world.query_filtered::<Entity, With<shared::LevelObject>>();
|
|
assert!(
|
|
fallback.iter(world).next().is_some(),
|
|
"the editor fallback arena remains available until SceneIo loads"
|
|
);
|
|
}
|
|
}
|