Skip to content

Commit

Permalink
refactor around an event-based API
Browse files Browse the repository at this point in the history
  • Loading branch information
snendev committed Mar 1, 2024
1 parent d4960c8 commit 7a46436
Show file tree
Hide file tree
Showing 2 changed files with 230 additions and 21 deletions.
155 changes: 146 additions & 9 deletions src/input_capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,58 @@ impl Plugin for InputCapturePlugin {
.add_systems(First, frame_counter);
}

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_final_capture_frame
.run_if(resource_exists::<FinalCaptureFrame>),
serialize_captured_input_on_end_capture_event,
serialize_captured_input_on_exit,
)
.run_if(resource_exists::<InputModesCaptured>)
.chain(),
);
}
}

/// 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 @@ -91,6 +128,30 @@ 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(*frame_count + final_frame));
}
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 @@ -103,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 @@ -118,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 @@ -135,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 @@ -156,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 @@ -170,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
96 changes: 84 additions & 12 deletions src/input_playback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,88 @@ impl Plugin for InputPlaybackPlugin {
.add_systems(First, frame_counter);
}

app.init_resource::<TimestampedInputs>()
.init_resource::<PlaybackProgress>()
.init_resource::<PlaybackStrategy>()
.init_resource::<PlaybackFilePath>()
.add_systems(Startup, deserialize_timestamped_inputs)
.add_systems(First, playback_timestamped_input.after(frame_counter));
app.add_event::<BeginInputPlayback>()
.add_event::<EndInputPlayback>()
.add_systems(
First,
(handle_end_playback_event, initiate_input_playback).chain(),
)
.add_systems(
First,
playback_timestamped_input
.run_if(resource_exists::<PlaybackProgress>)
.after(frame_counter)
.after(initiate_input_playback),
);
}
}

/// An Event that users can send to initiate input capture.
///
/// Data is serialized to the provided `filepath` when either an [`EndCaptureEvent`] or an [`AppExit`] event is detected.
#[derive(Debug, Default, Event)]
pub struct BeginInputPlayback {
/// The filepath at which to serialize captured input data.
pub filepath: String,
/// Controls the approach used for playing back recorded inputs.
///
/// See [`PlaybackStrategy`] for more information.
pub playback_strategy: PlaybackStrategy,
/// A entity corresponding to the [`bevy::window::Window`] which will receive input events.
/// If unspecified, input events will target the serialized window entity, which may be fragile.
pub playback_window: Option<Entity>,
}

/// Initiates input playback when a [`BeginInputPlayback`] is detected.
pub fn initiate_input_playback(
mut commands: Commands,
mut begin_capture_events: EventReader<BeginInputPlayback>,
) {
if let Some(event) = begin_capture_events.read().next() {
commands.init_resource::<TimestampedInputs>();
commands.init_resource::<PlaybackProgress>();
commands.insert_resource(event.playback_strategy.clone());
let playback_path = PlaybackFilePath::new(&event.filepath);
if let Some(path) = playback_path.path() {
let file = File::open(path).unwrap();
let timestamped_inputs: TimestampedInputs = from_reader(file).unwrap();
commands.insert_resource(timestamped_inputs);
}
commands.insert_resource(playback_path);
if let Some(playback_window) = event.playback_window {
commands.insert_resource(PlaybackWindow(playback_window));
}
}
begin_capture_events.clear();
}

/// An Event that users can send to end input playback prematurely.
#[derive(Debug, Event)]
pub struct EndInputPlayback;

/// 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 handle_end_playback_event(
mut commands: Commands,
mut end_capture_events: EventReader<EndInputPlayback>,
) {
if !end_capture_events.is_empty() {
end_capture_events.clear();
commands.remove_resource::<PlaybackFilePath>();
commands.remove_resource::<TimestampedInputs>();
commands.remove_resource::<PlaybackProgress>();
commands.remove_resource::<PlaybackStrategy>();
commands.remove_resource::<PlaybackWindow>();
}
}

/// The `Window` entity for which inputs will be captured.
///
/// If this Resource is attached, input events will be forwarded to this window entity rather than the serialized window entity.
#[derive(Debug, Resource)]
pub struct PlaybackWindow(Entity);

/// Controls the approach used for playing back recorded inputs
///
/// [`PlaybackStrategy::Time`] is the default strategy.
Expand Down Expand Up @@ -219,13 +292,12 @@ fn send_playback_events(

/// Reads the stored file paths from the [`PlaybackFilePath`] location (if any)
pub fn deserialize_timestamped_inputs(
mut timestamped_inputs: ResMut<TimestampedInputs>,
playback_path: Res<PlaybackFilePath>,
) {
if let Some(file_path) = playback_path.path() {
playback_path: &PlaybackFilePath,
)-> Option<Result<TimestampedInputs, ron::de::SpannedError>> {
playback_path.path().as_ref().map(|file_path| {
let file = File::open(file_path).unwrap();
*timestamped_inputs = from_reader(file).unwrap();
}
from_reader(file)
})
}

/// How far through the current cycle of input playback we've gotten.
Expand Down

0 comments on commit 7a46436

Please sign in to comment.