Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/bevy_dev_tools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
20 changes: 16 additions & 4 deletions crates/bevy_dev_tools/src/ci_testing/config.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use bevy_ecs::prelude::*;
use bevy_math::{Quat, Vec3};
use serde::Deserialize;

/// A configuration struct for automated CI testing.
///
/// 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)]
Expand All @@ -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.
///
Expand All @@ -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`.
Expand All @@ -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),
}
Expand Down
58 changes: 43 additions & 15 deletions crates/bevy_dev_tools/src/ci_testing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand All @@ -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::<CiTestingConfig>() {
// 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::<CiTestingConfig>().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::<EasyCameraMovementPlugin>() {
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::<EasyScreenRecordPlugin>()
&& 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(
Expand Down
25 changes: 25 additions & 0 deletions crates/bevy_dev_tools/src/ci_testing/systems.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -51,6 +54,28 @@ pub(crate) fn send_events(world: &mut World, mut current_frame: Local<u32>) {
*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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this log prints whether or not the camera actually moves, so should be a different string (if its meant to convey that the event happened) or moved into the if-let (if its meant to convey the component insertion happened)

if let Ok(camera) = world.query_filtered::<Entity, With<Camera>>().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));
Expand Down
55 changes: 54 additions & 1 deletion crates/bevy_dev_tools/src/easy_screenshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<Camera>>,
time: Res<Time>| {
{
{
let target = query.1;
query.0.translation.smooth_nudge(
&target.translation,
decay_rate,
time.delta_secs(),
);
query.0.rotation.smooth_nudge(
&target.rotation,
decay_rate,
time.delta_secs(),
);
}
}
})
.before(TransformSystems::Propagate),
);
}
}