Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: Make event types control the lifecycle of capture and playback #36

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
164 changes: 153 additions & 11 deletions src/input_capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! These are unified into a single [`TimestampedInputs`](crate::timestamped_input::TimestampedInputs) resource, which can be played back.

use bevy::app::{App, AppExit, Last, Plugin};
use bevy::app::{App, AppExit, First, Last, Plugin};
use bevy::core::{update_frame_count, FrameCount};
use bevy::ecs::prelude::*;
use bevy::input::gamepad::GamepadEvent;
Expand All @@ -27,22 +27,62 @@ pub struct InputCapturePlugin;

impl Plugin for InputCapturePlugin {
fn build(&self, app: &mut App) {
app.init_resource::<TimestampedInputs>()
.init_resource::<InputModesCaptured>()
.init_resource::<PlaybackFilePath>()
app.add_event::<BeginInputCapture>()
.add_event::<EndInputCapture>()
.add_systems(First, initiate_input_capture)
.add_systems(
Last,
(
// Capture any mocked input as well
capture_input,
serialize_captured_input_on_exit,
(
serialize_captured_input_on_final_capture_frame
.run_if(resource_exists::<FinalCaptureFrame>),
serialize_captured_input_on_end_capture_event,
serialize_captured_input_on_exit,
)
.run_if(resource_exists::<PlaybackFilePath>),
)
.run_if(resource_exists::<InputModesCaptured>)
.chain()
.before(update_frame_count),
);
}
}

/// An Event that users can send to initiate input capture.
///
/// Data is serialized to the provided `filepath` when either an [`EndInputCapture`] or an [`AppExit`] event is detected.
#[derive(Debug, Default, Event)]
pub struct BeginInputCapture {
/// The input mechanisms that will be captured, see [`InputModesCaptured`].
pub input_modes_captured: InputModesCaptured,
/// The filepath at which to serialize captured input data.
pub filepath: Option<String>,
/// The number of frames for which inputs should be captured.
/// If None, inputs will be captured until an [`EndInputCapture`] or [`AppExit`] event is detected.
pub frames_to_capture: Option<FrameCount>,
/// A `Window` entity which acts as a filter for which inputs will be captured.
/// This data will not be serialized, so that a target window can be selected on playback.
pub window_to_capture: Option<Entity>,
}

/// An Event that users can send to end input capture and serialize data to disk.
#[derive(Debug, Event)]
pub struct EndInputCapture;

/// The final [`FrameCount`] at which inputs will stop being captured.
///
/// If this Resource is attached, [`TimestampedInputs`] will be serialized and input capture will stop once `FrameCount` reaches this value.
#[derive(Debug, Resource)]
pub struct FinalCaptureFrame(FrameCount);

/// The `Window` entity for which inputs will be captured.
///
/// If this Resource is attached, only input events on the window corresponding to this entity will be captured.
#[derive(Debug, Resource)]
pub struct InputCaptureWindow(Entity);

/// The input mechanisms captured via the [`InputCapturePlugin`], configured as a resource.
///
/// By default, all supported input modes will be captured.
Expand Down Expand Up @@ -86,6 +126,32 @@ impl Default for InputModesCaptured {
}
}

/// Initiates input capture when a [`BeginInputCapture`] is detected.
pub fn initiate_input_capture(
mut commands: Commands,
mut begin_capture_events: EventReader<BeginInputCapture>,
frame_count: Res<FrameCount>,
) {
if let Some(event) = begin_capture_events.read().next() {
commands.init_resource::<TimestampedInputs>();
commands.insert_resource(event.input_modes_captured.clone());
if let Some(path) = &event.filepath {
commands.insert_resource(PlaybackFilePath::new(path));
} else {
commands.init_resource::<PlaybackFilePath>();
}
if let Some(final_frame) = event.frames_to_capture {
commands.insert_resource(FinalCaptureFrame(FrameCount(
frame_count.0.wrapping_add(final_frame.0),
)));
}
if let Some(window_entity) = &event.window_to_capture {
commands.insert_resource(InputCaptureWindow(*window_entity));
}
}
begin_capture_events.clear();
}

/// Captures input from the [`bevy::window`] and [`bevy::input`] event streams.
///
/// The input modes can be controlled via the [`InputModesCaptured`] resource.
Expand All @@ -98,6 +164,7 @@ pub fn capture_input(
mut gamepad_events: EventReader<GamepadEvent>,
mut app_exit_events: EventReader<AppExit>,
mut timestamped_input: ResMut<TimestampedInputs>,
window_to_capture: Option<Res<InputCaptureWindow>>,
input_modes_captured: Res<InputModesCaptured>,
frame_count: Res<FrameCount>,
time: Res<Time>,
Expand All @@ -113,13 +180,29 @@ pub fn capture_input(
timestamped_input.send_multiple(
frame,
time_since_startup,
mouse_button_events.read().cloned(),
mouse_button_events
.read()
.filter(|event| {
window_to_capture
.as_deref()
.map(|window| window.0 == event.window)
.unwrap_or(true)
})
.cloned(),
);

timestamped_input.send_multiple(
frame,
time_since_startup,
mouse_wheel_events.read().cloned(),
mouse_wheel_events
.read()
.filter(|event| {
window_to_capture
.as_deref()
.map(|window| window.0 == event.window)
.unwrap_or(true)
})
.cloned(),
);
} else {
mouse_button_events.clear();
Expand All @@ -130,14 +213,34 @@ pub fn capture_input(
timestamped_input.send_multiple(
frame,
time_since_startup,
cursor_moved_events.read().cloned(),
cursor_moved_events
.read()
.filter(|event| {
window_to_capture
.as_deref()
.map(|window| window.0 == event.window)
.unwrap_or(true)
})
.cloned(),
);
} else {
cursor_moved_events.clear();
}

if input_modes_captured.keyboard {
timestamped_input.send_multiple(frame, time_since_startup, keyboard_events.read().cloned());
timestamped_input.send_multiple(
frame,
time_since_startup,
keyboard_events
.read()
.filter(|event| {
window_to_capture
.as_deref()
.map(|window| window.0 == event.window)
.unwrap_or(true)
})
.cloned(),
);
} else {
keyboard_events.clear()
}
Expand All @@ -151,9 +254,8 @@ pub fn capture_input(
timestamped_input.send_multiple(frame, time_since_startup, app_exit_events.read().cloned())
}

/// Serializes captured input to the path given in the [`PlaybackFilePath`] resource.
/// Serializes captured input to the path given in the [`PlaybackFilePath`] resource once [`AppExit`] is sent.
///
/// This data is only serialized once when [`AppExit`] is sent.
/// Use the [`serialized_timestamped_inputs`] function directly if you want to implement custom checkpointing strategies.
pub fn serialize_captured_input_on_exit(
app_exit_events: EventReader<AppExit>,
Expand All @@ -165,6 +267,46 @@ pub fn serialize_captured_input_on_exit(
}
}

/// Serializes captured input to the path given in the [`PlaybackFilePath`] resource once the provided number of frames have elapsed.
///
/// Use the [`serialized_timestamped_inputs`] function directly if you want to implement custom checkpointing strategies.
pub fn serialize_captured_input_on_final_capture_frame(
mut commands: Commands,
frame_count: Res<FrameCount>,
final_frame: Res<FinalCaptureFrame>,
playback_file: Res<PlaybackFilePath>,
captured_inputs: Res<TimestampedInputs>,
) {
if *frame_count == final_frame.0 {
serialize_timestamped_inputs(&captured_inputs, &playback_file);
commands.remove_resource::<PlaybackFilePath>();
commands.remove_resource::<TimestampedInputs>();
commands.remove_resource::<InputModesCaptured>();
commands.remove_resource::<FinalCaptureFrame>();
commands.remove_resource::<InputCaptureWindow>();
}
}

/// Serializes captured input to the path given in the [`PlaybackFilePath`] resource when an [`EndInputCapture`] is detected.
///
/// Use the [`serialized_timestamped_inputs`] function directly if you want to implement custom checkpointing strategies.
pub fn serialize_captured_input_on_end_capture_event(
mut commands: Commands,
mut end_capture_events: EventReader<EndInputCapture>,
playback_file: Res<PlaybackFilePath>,
captured_inputs: Res<TimestampedInputs>,
) {
if !end_capture_events.is_empty() {
serialize_timestamped_inputs(&captured_inputs, &playback_file);
end_capture_events.clear();
commands.remove_resource::<PlaybackFilePath>();
commands.remove_resource::<TimestampedInputs>();
commands.remove_resource::<InputModesCaptured>();
commands.remove_resource::<FinalCaptureFrame>();
commands.remove_resource::<InputCaptureWindow>();
}
}

/// Writes the `timestamped_inputs` to the provided `path` (which should store [`Some(PathBuf)`]).
pub fn serialize_timestamped_inputs(
timestamped_inputs: &TimestampedInputs,
Expand Down
Loading
Loading