diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index 9d1506356e215..5e2f340ca443f 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -30,6 +30,7 @@ bevy_render = { path = "../bevy_render", version = "0.18.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.18.0-dev" } bevy_time = { path = "../bevy_time", version = "0.18.0-dev" } bevy_text = { path = "../bevy_text", version = "0.18.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.18.0-dev" } bevy_shader = { path = "../bevy_shader", version = "0.18.0-dev" } bevy_ui = { path = "../bevy_ui", version = "0.18.0-dev" } bevy_ui_render = { path = "../bevy_ui_render", version = "0.18.0-dev" } diff --git a/crates/bevy_dev_tools/src/ci_testing/config.rs b/crates/bevy_dev_tools/src/ci_testing/config.rs index 4e3573c49c3c2..074efae0c8608 100644 --- a/crates/bevy_dev_tools/src/ci_testing/config.rs +++ b/crates/bevy_dev_tools/src/ci_testing/config.rs @@ -1,4 +1,5 @@ use bevy_ecs::prelude::*; +use bevy_math::{Quat, Vec3}; use serde::Deserialize; /// A configuration struct for automated CI testing. @@ -6,7 +7,7 @@ use serde::Deserialize; /// It gets used when the `bevy_ci_testing` feature is enabled to automatically /// exit a Bevy app when run through the CI. This is needed because otherwise /// Bevy apps would be stuck in the game loop and wouldn't allow the CI to progress. -#[derive(Deserialize, Resource, PartialEq, Debug, Default)] +#[derive(Deserialize, Resource, PartialEq, Debug, Default, Clone)] pub struct CiTestingConfig { /// The setup for this test. #[serde(default)] @@ -17,7 +18,7 @@ pub struct CiTestingConfig { } /// Setup for a test. -#[derive(Deserialize, Default, PartialEq, Debug)] +#[derive(Deserialize, Default, PartialEq, Debug, Clone)] pub struct CiTestingSetup { /// The amount of time in seconds between frame updates. /// @@ -28,11 +29,11 @@ pub struct CiTestingSetup { } /// An event to send at a given frame, used for CI testing. -#[derive(Deserialize, PartialEq, Debug)] +#[derive(Deserialize, PartialEq, Debug, Clone)] pub struct CiTestingEventOnFrame(pub u32, pub CiTestingEvent); /// An event to send, used for CI testing. -#[derive(Deserialize, PartialEq, Debug)] +#[derive(Deserialize, PartialEq, Debug, Clone)] pub enum CiTestingEvent { /// Takes a screenshot of the entire screen, and saves the results to /// `screenshot-{current_frame}.png`. @@ -47,6 +48,17 @@ pub enum CiTestingEvent { /// /// [`AppExit::Success`]: bevy_app::AppExit::Success AppExit, + /// Starts recording the screen. + StartScreenRecording, + /// Stops recording the screen. + StopScreenRecording, + /// Smoothly moves the camera to the given position. + MoveCamera { + /// Position to move the camera to. + translation: Vec3, + /// Rotation to move the camera to. + rotation: Quat, + }, /// Sends a [`CiTestingCustomEvent`] using the given [`String`]. Custom(String), } diff --git a/crates/bevy_dev_tools/src/ci_testing/mod.rs b/crates/bevy_dev_tools/src/ci_testing/mod.rs index f763db407ccb1..34b99fb4a81cf 100644 --- a/crates/bevy_dev_tools/src/ci_testing/mod.rs +++ b/crates/bevy_dev_tools/src/ci_testing/mod.rs @@ -3,6 +3,10 @@ mod config; mod systems; +use crate::EasyCameraMovementPlugin; +#[cfg(feature = "screenrecording")] +use crate::EasyScreenRecordPlugin; + pub use self::config::*; use bevy_app::prelude::*; @@ -26,24 +30,48 @@ pub struct CiTestingPlugin; impl Plugin for CiTestingPlugin { fn build(&self, app: &mut App) { - #[cfg(not(target_arch = "wasm32"))] - let config: CiTestingConfig = { - let filename = std::env::var("CI_TESTING_CONFIG") - .unwrap_or_else(|_| "ci_testing_config.ron".to_string()); - std::fs::read_to_string(filename) - .map(|content| { - ron::from_str(&content) - .expect("error deserializing CI testing configuration file") - }) - .unwrap_or_default() - }; + let config = if !app.world().is_resource_added::() { + // Load configuration from file if not already setup + #[cfg(not(target_arch = "wasm32"))] + let config: CiTestingConfig = { + let filename = std::env::var("CI_TESTING_CONFIG") + .unwrap_or_else(|_| "ci_testing_config.ron".to_string()); + std::fs::read_to_string(filename) + .map(|content| { + ron::from_str(&content) + .expect("error deserializing CI testing configuration file") + }) + .unwrap_or_default() + }; + + #[cfg(target_arch = "wasm32")] + let config: CiTestingConfig = { + let config = include_str!("../../../../ci_testing_config.ron"); + ron::from_str(config).expect("error deserializing CI testing configuration file") + }; - #[cfg(target_arch = "wasm32")] - let config: CiTestingConfig = { - let config = include_str!("../../../../ci_testing_config.ron"); - ron::from_str(config).expect("error deserializing CI testing configuration file") + config + } else { + app.world().resource::().clone() }; + // Add the `EasyCameraMovementPlugin` to the app if it's not already added. + // To configure the movement speed, add the plugin first. + if !app.is_plugin_added::() { + app.add_plugins(EasyCameraMovementPlugin::default()); + } + // Add the `EasyScreenRecordPlugin` to the app if it's not already added and one of the event is starting screenrecording. + // To configure the recording quality, add the plugin first. + #[cfg(feature = "screenrecording")] + if !app.is_plugin_added::() + && config + .events + .iter() + .any(|e| matches!(e.1, CiTestingEvent::StartScreenRecording)) + { + app.add_plugins(EasyScreenRecordPlugin::default()); + } + // Configure a fixed frame time if specified. if let Some(fixed_frame_time) = config.setup.fixed_frame_time { app.insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_secs_f32( diff --git a/crates/bevy_dev_tools/src/ci_testing/systems.rs b/crates/bevy_dev_tools/src/ci_testing/systems.rs index b9cdc3e2289fd..9df128c9ba1f3 100644 --- a/crates/bevy_dev_tools/src/ci_testing/systems.rs +++ b/crates/bevy_dev_tools/src/ci_testing/systems.rs @@ -1,5 +1,8 @@ +use crate::CameraMovement; + use super::config::*; use bevy_app::AppExit; +use bevy_camera::Camera; use bevy_ecs::prelude::*; use bevy_render::view::screenshot::{save_to_disk, Screenshot}; use tracing::{debug, info}; @@ -51,6 +54,28 @@ pub(crate) fn send_events(world: &mut World, mut current_frame: Local) { *current_frame, name ); } + CiTestingEvent::StartScreenRecording => { + info!("Started recording screen at frame {}.", *current_frame); + #[cfg(feature = "screenrecording")] + world.write_message(crate::RecordScreen::Start); + } + CiTestingEvent::StopScreenRecording => { + info!("Stopped recording screen at frame {}.", *current_frame); + #[cfg(feature = "screenrecording")] + world.write_message(crate::RecordScreen::Stop); + } + CiTestingEvent::MoveCamera { + translation, + rotation, + } => { + info!("Moved camera at frame {}.", *current_frame); + if let Ok(camera) = world.query_filtered::>().single(world) { + world.entity_mut(camera).insert(CameraMovement { + translation, + rotation, + }); + } + } // Custom events are forwarded to the world. CiTestingEvent::Custom(event_string) => { world.write_message(CiTestingCustomEvent(event_string)); diff --git a/crates/bevy_dev_tools/src/easy_screenshot.rs b/crates/bevy_dev_tools/src/easy_screenshot.rs index 84f93e9879820..28e166e525c4e 100644 --- a/crates/bevy_dev_tools/src/easy_screenshot.rs +++ b/crates/bevy_dev_tools/src/easy_screenshot.rs @@ -2,10 +2,14 @@ use core::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; -use bevy_app::{App, Plugin, Update}; +use bevy_app::{App, Plugin, PostUpdate, Update}; +use bevy_camera::Camera; use bevy_ecs::prelude::*; use bevy_input::{common_conditions::input_just_pressed, keyboard::KeyCode}; +use bevy_math::{Quat, StableInterpolate, Vec3}; use bevy_render::view::screenshot::{save_to_disk, Screenshot}; +use bevy_time::Time; +use bevy_transform::{components::Transform, TransformSystems}; use bevy_window::{PrimaryWindow, Window}; #[cfg(all(not(target_os = "windows"), feature = "screenrecording"))] pub use x264::{Preset, Tune}; @@ -311,3 +315,52 @@ impl Plugin for EasyScreenRecordPlugin { } } } + +/// Plugin to move the camera smoothly according to the current time +pub struct EasyCameraMovementPlugin { + /// Decay rate for the camera movement + pub decay_rate: f32, +} + +impl Default for EasyCameraMovementPlugin { + fn default() -> Self { + Self { decay_rate: 1.0 } + } +} + +/// Move the camera to the given position +#[derive(Component)] +pub struct CameraMovement { + /// Target position for the camera movement + pub translation: Vec3, + /// Target rotation for the camera movement + pub rotation: Quat, +} + +impl Plugin for EasyCameraMovementPlugin { + fn build(&self, app: &mut App) { + let decay_rate = self.decay_rate; + app.add_systems( + PostUpdate, + (move |mut query: Single<(&mut Transform, &CameraMovement), With>, + time: Res