Skip to content

Commit fa9fbf7

Browse files
authored
Easy demo recording (#21243)
# Objective - Followup on #21237 - It can be hard to share how to record a demo for a specific feature - Make it easy ## Solution - Add a dev plugin that can move the camera - Add plumbing to CI testing to be able to move the camera and control screen recording - Demo can be configured in code or in a configuration file ## Testing - Add to an example: ```rs let fps = 120; // FPS for the example on your computer [...] // Before `DefaultPlugins`: .insert_resource(CiTestingConfig { events: vec![ CiTestingEventOnFrame(fps * 1, ci_testing::CiTestingEvent::StartScreenRecording), { let transform = Transform::from_xyz(0.7, 0.7, -1.0) .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y); CiTestingEventOnFrame( fps * 6, ci_testing::CiTestingEvent::MoveCamera { translation: transform.translation, rotation: transform.rotation, }, ) }, { let transform = Transform::from_xyz(-0.7, 0.7, -1.0) .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y); CiTestingEventOnFrame( fps * 11, ci_testing::CiTestingEvent::MoveCamera { translation: transform.translation, rotation: transform.rotation, }, ) }, { let transform = Transform::from_xyz(-0.7, 0.7, 1.0) .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y); CiTestingEventOnFrame( fps * 16, ci_testing::CiTestingEvent::MoveCamera { translation: transform.translation, rotation: transform.rotation, }, ) }, { let transform = Transform::from_xyz(0.7, 0.7, 1.0) .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y); CiTestingEventOnFrame( fps * 21, ci_testing::CiTestingEvent::MoveCamera { translation: transform.translation, rotation: transform.rotation, }, ) }, CiTestingEventOnFrame(fps * 26, ci_testing::CiTestingEvent::StopScreenRecording), ], ..default() }) ``` - Run the example with `--features bevy_internal/screenrecording,bevy_ci_testing` - Lay back, enjoy your demo recording --- ## Showcase https://github.com/user-attachments/assets/bbd9e56d-58c1-41eb-b6b4-1f1db2f2dab5
1 parent c92867a commit fa9fbf7

File tree

5 files changed

+139
-20
lines changed

5 files changed

+139
-20
lines changed

crates/bevy_dev_tools/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ bevy_render = { path = "../bevy_render", version = "0.18.0-dev" }
3030
bevy_reflect = { path = "../bevy_reflect", version = "0.18.0-dev" }
3131
bevy_time = { path = "../bevy_time", version = "0.18.0-dev" }
3232
bevy_text = { path = "../bevy_text", version = "0.18.0-dev" }
33+
bevy_transform = { path = "../bevy_transform", version = "0.18.0-dev" }
3334
bevy_shader = { path = "../bevy_shader", version = "0.18.0-dev" }
3435
bevy_ui = { path = "../bevy_ui", version = "0.18.0-dev" }
3536
bevy_ui_render = { path = "../bevy_ui_render", version = "0.18.0-dev" }

crates/bevy_dev_tools/src/ci_testing/config.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
use bevy_ecs::prelude::*;
2+
use bevy_math::{Quat, Vec3};
23
use serde::Deserialize;
34

45
/// A configuration struct for automated CI testing.
56
///
67
/// It gets used when the `bevy_ci_testing` feature is enabled to automatically
78
/// exit a Bevy app when run through the CI. This is needed because otherwise
89
/// Bevy apps would be stuck in the game loop and wouldn't allow the CI to progress.
9-
#[derive(Deserialize, Resource, PartialEq, Debug, Default)]
10+
#[derive(Deserialize, Resource, PartialEq, Debug, Default, Clone)]
1011
pub struct CiTestingConfig {
1112
/// The setup for this test.
1213
#[serde(default)]
@@ -17,7 +18,7 @@ pub struct CiTestingConfig {
1718
}
1819

1920
/// Setup for a test.
20-
#[derive(Deserialize, Default, PartialEq, Debug)]
21+
#[derive(Deserialize, Default, PartialEq, Debug, Clone)]
2122
pub struct CiTestingSetup {
2223
/// The amount of time in seconds between frame updates.
2324
///
@@ -28,11 +29,11 @@ pub struct CiTestingSetup {
2829
}
2930

3031
/// An event to send at a given frame, used for CI testing.
31-
#[derive(Deserialize, PartialEq, Debug)]
32+
#[derive(Deserialize, PartialEq, Debug, Clone)]
3233
pub struct CiTestingEventOnFrame(pub u32, pub CiTestingEvent);
3334

3435
/// An event to send, used for CI testing.
35-
#[derive(Deserialize, PartialEq, Debug)]
36+
#[derive(Deserialize, PartialEq, Debug, Clone)]
3637
pub enum CiTestingEvent {
3738
/// Takes a screenshot of the entire screen, and saves the results to
3839
/// `screenshot-{current_frame}.png`.
@@ -47,6 +48,17 @@ pub enum CiTestingEvent {
4748
///
4849
/// [`AppExit::Success`]: bevy_app::AppExit::Success
4950
AppExit,
51+
/// Starts recording the screen.
52+
StartScreenRecording,
53+
/// Stops recording the screen.
54+
StopScreenRecording,
55+
/// Smoothly moves the camera to the given position.
56+
MoveCamera {
57+
/// Position to move the camera to.
58+
translation: Vec3,
59+
/// Rotation to move the camera to.
60+
rotation: Quat,
61+
},
5062
/// Sends a [`CiTestingCustomEvent`] using the given [`String`].
5163
Custom(String),
5264
}

crates/bevy_dev_tools/src/ci_testing/mod.rs

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
mod config;
44
mod systems;
55

6+
use crate::EasyCameraMovementPlugin;
7+
#[cfg(feature = "screenrecording")]
8+
use crate::EasyScreenRecordPlugin;
9+
610
pub use self::config::*;
711

812
use bevy_app::prelude::*;
@@ -26,24 +30,48 @@ pub struct CiTestingPlugin;
2630

2731
impl Plugin for CiTestingPlugin {
2832
fn build(&self, app: &mut App) {
29-
#[cfg(not(target_arch = "wasm32"))]
30-
let config: CiTestingConfig = {
31-
let filename = std::env::var("CI_TESTING_CONFIG")
32-
.unwrap_or_else(|_| "ci_testing_config.ron".to_string());
33-
std::fs::read_to_string(filename)
34-
.map(|content| {
35-
ron::from_str(&content)
36-
.expect("error deserializing CI testing configuration file")
37-
})
38-
.unwrap_or_default()
39-
};
33+
let config = if !app.world().is_resource_added::<CiTestingConfig>() {
34+
// Load configuration from file if not already setup
35+
#[cfg(not(target_arch = "wasm32"))]
36+
let config: CiTestingConfig = {
37+
let filename = std::env::var("CI_TESTING_CONFIG")
38+
.unwrap_or_else(|_| "ci_testing_config.ron".to_string());
39+
std::fs::read_to_string(filename)
40+
.map(|content| {
41+
ron::from_str(&content)
42+
.expect("error deserializing CI testing configuration file")
43+
})
44+
.unwrap_or_default()
45+
};
46+
47+
#[cfg(target_arch = "wasm32")]
48+
let config: CiTestingConfig = {
49+
let config = include_str!("../../../../ci_testing_config.ron");
50+
ron::from_str(config).expect("error deserializing CI testing configuration file")
51+
};
4052

41-
#[cfg(target_arch = "wasm32")]
42-
let config: CiTestingConfig = {
43-
let config = include_str!("../../../../ci_testing_config.ron");
44-
ron::from_str(config).expect("error deserializing CI testing configuration file")
53+
config
54+
} else {
55+
app.world().resource::<CiTestingConfig>().clone()
4556
};
4657

58+
// Add the `EasyCameraMovementPlugin` to the app if it's not already added.
59+
// To configure the movement speed, add the plugin first.
60+
if !app.is_plugin_added::<EasyCameraMovementPlugin>() {
61+
app.add_plugins(EasyCameraMovementPlugin::default());
62+
}
63+
// Add the `EasyScreenRecordPlugin` to the app if it's not already added and one of the event is starting screenrecording.
64+
// To configure the recording quality, add the plugin first.
65+
#[cfg(feature = "screenrecording")]
66+
if !app.is_plugin_added::<EasyScreenRecordPlugin>()
67+
&& config
68+
.events
69+
.iter()
70+
.any(|e| matches!(e.1, CiTestingEvent::StartScreenRecording))
71+
{
72+
app.add_plugins(EasyScreenRecordPlugin::default());
73+
}
74+
4775
// Configure a fixed frame time if specified.
4876
if let Some(fixed_frame_time) = config.setup.fixed_frame_time {
4977
app.insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_secs_f32(

crates/bevy_dev_tools/src/ci_testing/systems.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
use crate::CameraMovement;
2+
13
use super::config::*;
24
use bevy_app::AppExit;
5+
use bevy_camera::Camera;
36
use bevy_ecs::prelude::*;
47
use bevy_render::view::screenshot::{save_to_disk, Screenshot};
58
use tracing::{debug, info};
@@ -51,6 +54,28 @@ pub(crate) fn send_events(world: &mut World, mut current_frame: Local<u32>) {
5154
*current_frame, name
5255
);
5356
}
57+
CiTestingEvent::StartScreenRecording => {
58+
info!("Started recording screen at frame {}.", *current_frame);
59+
#[cfg(feature = "screenrecording")]
60+
world.write_message(crate::RecordScreen::Start);
61+
}
62+
CiTestingEvent::StopScreenRecording => {
63+
info!("Stopped recording screen at frame {}.", *current_frame);
64+
#[cfg(feature = "screenrecording")]
65+
world.write_message(crate::RecordScreen::Stop);
66+
}
67+
CiTestingEvent::MoveCamera {
68+
translation,
69+
rotation,
70+
} => {
71+
info!("Moved camera at frame {}.", *current_frame);
72+
if let Ok(camera) = world.query_filtered::<Entity, With<Camera>>().single(world) {
73+
world.entity_mut(camera).insert(CameraMovement {
74+
translation,
75+
rotation,
76+
});
77+
}
78+
}
5479
// Custom events are forwarded to the world.
5580
CiTestingEvent::Custom(event_string) => {
5681
world.write_message(CiTestingCustomEvent(event_string));

crates/bevy_dev_tools/src/easy_screenshot.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
use core::time::Duration;
33
use std::time::{SystemTime, UNIX_EPOCH};
44

5-
use bevy_app::{App, Plugin, Update};
5+
use bevy_app::{App, Plugin, PostUpdate, Update};
6+
use bevy_camera::Camera;
67
use bevy_ecs::prelude::*;
78
use bevy_input::{common_conditions::input_just_pressed, keyboard::KeyCode};
9+
use bevy_math::{Quat, StableInterpolate, Vec3};
810
use bevy_render::view::screenshot::{save_to_disk, Screenshot};
11+
use bevy_time::Time;
12+
use bevy_transform::{components::Transform, TransformSystems};
913
use bevy_window::{PrimaryWindow, Window};
1014
#[cfg(all(not(target_os = "windows"), feature = "screenrecording"))]
1115
pub use x264::{Preset, Tune};
@@ -311,3 +315,52 @@ impl Plugin for EasyScreenRecordPlugin {
311315
}
312316
}
313317
}
318+
319+
/// Plugin to move the camera smoothly according to the current time
320+
pub struct EasyCameraMovementPlugin {
321+
/// Decay rate for the camera movement
322+
pub decay_rate: f32,
323+
}
324+
325+
impl Default for EasyCameraMovementPlugin {
326+
fn default() -> Self {
327+
Self { decay_rate: 1.0 }
328+
}
329+
}
330+
331+
/// Move the camera to the given position
332+
#[derive(Component)]
333+
pub struct CameraMovement {
334+
/// Target position for the camera movement
335+
pub translation: Vec3,
336+
/// Target rotation for the camera movement
337+
pub rotation: Quat,
338+
}
339+
340+
impl Plugin for EasyCameraMovementPlugin {
341+
fn build(&self, app: &mut App) {
342+
let decay_rate = self.decay_rate;
343+
app.add_systems(
344+
PostUpdate,
345+
(move |mut query: Single<(&mut Transform, &CameraMovement), With<Camera>>,
346+
time: Res<Time>| {
347+
{
348+
{
349+
let target = query.1;
350+
query.0.translation.smooth_nudge(
351+
&target.translation,
352+
decay_rate,
353+
time.delta_secs(),
354+
);
355+
query.0.rotation.smooth_nudge(
356+
&target.rotation,
357+
decay_rate,
358+
time.delta_secs(),
359+
);
360+
}
361+
}
362+
})
363+
.before(TransformSystems::Propagate),
364+
);
365+
}
366+
}

0 commit comments

Comments
 (0)