diff --git a/Cargo.toml b/Cargo.toml index 51f90ed..a39949b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,9 @@ include = ["/src", "LICENSE", "/examples"] [dependencies] [dev-dependencies] -winit = "0.25.0" \ No newline at end of file +env_logger = "0.9" +log = "0.4" +pixels = "0.5.0" +winit = "0.25" +winit_input_helper = "0.10" +rand = "0.8.4" \ No newline at end of file diff --git a/examples/basics.rs b/examples/basics.rs index 2166293..b03a7ed 100644 --- a/examples/basics.rs +++ b/examples/basics.rs @@ -1,8 +1,9 @@ use hypoloop::core::{State, Loop}; fn main() { - // create sim with default configuration + // create a new sim loop let mut sim = Loop::new(); + sim.set_update_interval(20); // test variable let mut x: f32 = 0.0; @@ -10,7 +11,7 @@ fn main() { // create a closure containing your update logic let mut update_logic = move |state: &mut State| { // access loop metadata via the State object - x += state.get_timescale(); + x += state.get_timestep(); print!("x: {} | ", x); // print information about the current tick's timings @@ -18,7 +19,7 @@ fn main() { }; // create a closure containing your display logic - let display_logic = move |state: &State| { + let mut display_logic = move |state: &mut State| { // }; @@ -27,6 +28,6 @@ fn main() { sim.init(); loop { // "step" the sim forward - sim.step(&mut update_logic, &display_logic); + sim.step(&mut update_logic, &mut display_logic); } } \ No newline at end of file diff --git a/examples/buffer.rs b/examples/buffer.rs new file mode 100644 index 0000000..1e8d7c9 --- /dev/null +++ b/examples/buffer.rs @@ -0,0 +1,137 @@ +#![deny(clippy::all)] +#![forbid(unsafe_code)] + +use hypoloop::core::{State, Loop}; +use rand::Rng; +use log::error; +use pixels::{Error, Pixels, SurfaceTexture}; +use winit::dpi::LogicalSize; +use winit::event::{Event, VirtualKeyCode}; +use winit::event_loop::{ControlFlow, EventLoop}; +use winit::window::WindowBuilder; +use winit_input_helper::WinitInputHelper; + +const WIDTH: u32 = 500; +const HEIGHT: u32 = 500; +const BOX_SIZE: i16 = 64; + +/// Representation of the application state. In this example, a box will bounce around the screen. +struct World { + target: [i16; 2] +} + +fn main() -> Result<(), Error> { + env_logger::init(); + let event_loop = EventLoop::new(); + let mut input = WinitInputHelper::new(); + let window = { + let size = LogicalSize::new(WIDTH as f64, HEIGHT as f64); + WindowBuilder::new() + .with_title("Hello Pixels") + .with_inner_size(size) + .with_min_inner_size(size) + .build(&event_loop) + .unwrap() + }; + + let mut pixels = { + let window_size = window.inner_size(); + let surface_texture = SurfaceTexture::new(window_size.width, window_size.height, &window); + Pixels::new(WIDTH, HEIGHT, surface_texture)? + }; + let mut world = World::new(); + + // create sim with default configuration + let mut sim = Loop::new(); + //sim.set_update_interval(10); + + let mut update_logic = move |state: &mut State| { + // print information about the current tick's timings + state.debug_time(); + world.update(state.get_timestep()); + world.draw(pixels.get_frame()); + if pixels + .render() + .map_err(|e| error!("pixels.render() failed: {}", e)) + .is_err() + { + state.pause(); + return; + } + }; + + // create a closure containing your display logic + let mut display_logic = move |state: &mut State| { + // Draw the current frame + window.request_redraw(); + }; + + sim.init(); + event_loop.run(move |event, _, control_flow| { + // Handle input events + if input.update(&event) { + // Close events + if input.key_pressed(VirtualKeyCode::Escape) || input.quit() { + *control_flow = ControlFlow::Exit; + return; + } + } + + // step the sim forward + sim.step(&mut update_logic, &mut display_logic); + }); +} + +impl World { + /// Create a new `World` instance that can draw a moving box. + fn new() -> Self { + Self { + target: [0, 0] + } + } + + /// Update the `World` internal state; bounce the box around the screen. + fn update(&mut self, timestep: f32) { + let speed: f32 = 500.0; + + let mut new_target = self.target; + + // update the target + if new_target[0] < WIDTH as i16 { + new_target[0] += (speed * timestep) as i16; + } else { + new_target[0] = 0; + } + + self.target = new_target; + } + + /// Draw the `World` state to the frame buffer. + /// + /// Assumes the default texture format: `wgpu::TextureFormat::Rgba8UnormSrgb` + fn draw(&self, frame: &mut [u8]) { + for (i, pixel) in frame.chunks_exact_mut(4).enumerate() { + let x = (i % WIDTH as usize) as i16; + let y = (i / WIDTH as usize) as i16; + + let mut old_pixel = [0u8; 4]; + for j in 0..4 { + // get the old pixel and decay it + old_pixel[j] = pixel[j]; + if old_pixel[j] > 0 { + old_pixel[j] -= 1; + } + } + + let condition = x <= self.target[0]; + + let rgba = if condition { + [0xff, 0x00, 0x00, 0xff] + } else { + old_pixel + }; + + pixel.copy_from_slice(&rgba); + } + } +} \ No newline at end of file diff --git a/examples/pixelstest.rs b/examples/pixelstest.rs new file mode 100644 index 0000000..ab58c33 --- /dev/null +++ b/examples/pixelstest.rs @@ -0,0 +1,135 @@ +#![deny(clippy::all)] +#![forbid(unsafe_code)] + +use hypoloop::core::{State, Loop}; +use log::error; +use pixels::{Error, Pixels, SurfaceTexture}; +use winit::dpi::LogicalSize; +use winit::event::{Event, VirtualKeyCode}; +use winit::event_loop::{ControlFlow, EventLoop}; +use winit::window::WindowBuilder; +use winit_input_helper::WinitInputHelper; + +const WIDTH: u32 = 320; +const HEIGHT: u32 = 240; +const BOX_SIZE: i16 = 64; + +/// Representation of the application state. In this example, a box will bounce around the screen. +struct World { + box_x: i16, + box_y: i16, + velocity_x: i16, + velocity_y: i16, +} + +fn main() -> Result<(), Error> { + env_logger::init(); + let event_loop = EventLoop::new(); + let mut input = WinitInputHelper::new(); + let window = { + let size = LogicalSize::new(WIDTH as f64, HEIGHT as f64); + WindowBuilder::new() + .with_title("Hello Pixels") + .with_inner_size(size) + .with_min_inner_size(size) + .build(&event_loop) + .unwrap() + }; + + let mut pixels = { + let window_size = window.inner_size(); + let surface_texture = SurfaceTexture::new(window_size.width, window_size.height, &window); + Pixels::new(WIDTH, HEIGHT, surface_texture)? + }; + let mut world = World::new(); + + // create sim with default configuration + let mut sim = Loop::new(); + sim.set_update_interval(10); + + let mut update_logic = move |state: &mut State| { + // print information about the current tick's timings + state.debug_time(); + world.update(state.get_delta_time(), state.get_timescale()); + world.draw(pixels.get_frame()); + if pixels + .render() + .map_err(|e| error!("pixels.render() failed: {}", e)) + .is_err() + { + state.pause(); + return; + } + }; + + // create a closure containing your display logic + let mut display_logic = move |state: &mut State| { + // Draw the current frame + window.request_redraw(); + }; + + sim.init(); + event_loop.run(move |event, _, control_flow| { + // Handle input events + if input.update(&event) { + // Close events + if input.key_pressed(VirtualKeyCode::Escape) || input.quit() { + *control_flow = ControlFlow::Exit; + return; + } + } + + // step the sim forward + sim.step(&mut update_logic, &mut display_logic); + }); +} + +impl World { + /// Create a new `World` instance that can draw a moving box. + fn new() -> Self { + Self { + box_x: 24, + box_y: 16, + velocity_x: 100, + velocity_y: 100, + } + } + + /// Update the `World` internal state; bounce the box around the screen. + fn update(&mut self, delta_time: u32, timescale: f32) { + let timestep: f32 = delta_time as f32 / 1000.0 * timescale; + + if self.box_x <= 0 || self.box_x + BOX_SIZE > WIDTH as i16 { + self.velocity_x *= -1; + } + if self.box_y <= 0 || self.box_y + BOX_SIZE > HEIGHT as i16 { + self.velocity_y *= -1; + } + + self.box_x += (self.velocity_x as f32 * timestep) as i16; + self.box_y += (self.velocity_y as f32 * timestep) as i16; + } + + /// Draw the `World` state to the frame buffer. + /// + /// Assumes the default texture format: `wgpu::TextureFormat::Rgba8UnormSrgb` + fn draw(&self, frame: &mut [u8]) { + for (i, pixel) in frame.chunks_exact_mut(4).enumerate() { + let x = (i % WIDTH as usize) as i16; + let y = (i / WIDTH as usize) as i16; + + let inside_the_box = x >= self.box_x + && x < self.box_x + BOX_SIZE + && y >= self.box_y + && y < self.box_y + BOX_SIZE; + + let rgba = if inside_the_box { + [0x5e, 0x48, 0xe8, 0xff] + } else { + [0x48, 0xb2, 0xe8, 0xff] + }; + + pixel.copy_from_slice(&rgba); + } + } +} \ No newline at end of file diff --git a/examples/windowing.rs b/examples/windowing.rs index 36b98b9..edf7a4c 100644 --- a/examples/windowing.rs +++ b/examples/windowing.rs @@ -30,7 +30,7 @@ fn main() { }; // create a closure containing your display logic - let display_logic = move |state: &State| { + let display_logic = move |state: &mut State| { // redraw the winit window window.request_redraw(); }; @@ -41,6 +41,6 @@ fn main() { // run the winit event loop with embedded hypoloop sim event_loop.run(move |event, _, control_flow| { // "step" the sim forward - sim.step(&mut update_logic, &display_logic); + sim.step(&mut update_logic, &mut display_logic); }); } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 7e13e94..fefb179 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,12 +4,12 @@ pub mod core { /// Contains mutable simulation state which can be changed via callback functions #[derive(Copy, Clone)] pub struct State { - update_interval: u32, timescale: f32, simulate: bool, clock_start: Instant, last_tick: Instant, delta_time: u32, + timestep: f32, irl_time: Duration, sim_time: Duration } @@ -18,35 +18,32 @@ pub mod core { /// Creates a default State object pub fn new() -> State { // Create default state object - let mut new_state = State { - update_interval: 40, + let new_state = State { timescale: 1.0, simulate: true, clock_start: Instant::now(), last_tick: Instant::now(), delta_time: 0, + timestep: 0.0, irl_time: Duration::new(0,0), sim_time: Duration::new(0,0) }; - // Make sure that delta_time always starts the same as update_interval - new_state.delta_time = new_state.update_interval; - // Return this default state new_state } - /// Returns the "update interval", the minimum time (in ms) which will elapse between update ticks - pub fn get_update_interval(self) -> u32 { - self.update_interval - } - - /// Returns the current "delta time", the time (in ms) elapsed since the last update tick + /// Returns the current "delta time", the real time (in ms) elapsed since the last update tick pub fn get_delta_time(self) -> u32 { self.delta_time } - /// Returns the current IRL time elapsed since the start of the simulation + /// Returns the current "timestep", the virtual time (in s) elapsed since the last update tick (necessary for scaling physics simulations, etc.) + pub fn get_timestep(self) -> f32 { + self.timestep + } + + /// Returns the current real time elapsed since the start of the simulation pub fn get_irl_time(self) -> Duration { self.irl_time } @@ -76,42 +73,56 @@ pub mod core { self.simulate = true; } - /// Changes the simulation update interval - pub fn set_update_interval(&mut self, update_interval: u32) { - self.update_interval = update_interval; - } - /// Changes the simulation timescale pub fn set_timescale(&mut self, timescale: f32) { self.timescale = timescale; } - /// Prints a string of information about the current time (IRL time, Sim time, Delta time (tick), Delta time (step)) - /// IRL time: Real time (in ms) elapsed since the start of the loop - /// Sim time: Virtual time (in ms) elapsed since the start of the loop - /// Delta time (tick): Real time (in ms) elapsed between the last tick and the previous tick - /// Delta time (step): Real time (in ms with nanosecond accuracy) elapsed since the last update tick + /// Prints a string of information about the current step's timings + /// + /// # Example: + /// `IRL time: 4443ms | Sim time: 4443ms | Delta time (tick): 40ms | Delta time (step): 40.0638ms | Timestep: 0.04s` + /// # Terminology: + /// - *IRL time:* Real time (in ms) elapsed since the start of the simulation + /// - *Sim time:* Virtual time (in ms) elapsed since the start of the simulation + /// - *Delta time (tick):* Real time (in ms) elapsed between the last tick and the previous tick + /// - *Delta time (step):* Real time (in ms with ns accuracy) elapsed since the last tick + /// - *Timestep:* Virtual time (in s with ms accuracy) elapsed since the last tick pub fn debug_time(self) { let elapsed_time = Instant::now().duration_since(self.last_tick); let loop_delay_ms = elapsed_time.as_nanos() as f32 / 1_000_000.0; - println!("IRL time: {}ms | Sim time: {}ms | Delta time (tick): {}ms | Delta time (step): {}ms", self.irl_time.as_millis(), self.sim_time.as_millis(), self.delta_time, loop_delay_ms); + println!("IRL time: {}ms | Sim time: {}ms | Delta time (tick): {}ms | Delta time (step): {}ms | Timestep: {}s", self.irl_time.as_millis(), self.sim_time.as_millis(), self.delta_time, loop_delay_ms, self.timestep); } } /// The simulation loop itself pub struct Loop { state: State, - realtime: bool + realtime: bool, + update_interval: u32 } impl Loop { /// Creates a new simulation with default values pub fn new() -> Loop { - // Return a Loop object with a default State - Loop { - state: State::new(), - realtime: true - } + // Create a new State object + let mut new_state = State::new(); + + // Create a Loop object with a default State + let mut new_loop = Loop { + state: new_state, + realtime: true, + update_interval: 40 + }; + + // Initialize the delta time to be the same as the update interval (to prevent division by zero) + new_loop.state.delta_time = new_loop.update_interval; + + // Initialize the timestep based on the new delta time + new_loop.state.timestep = timestep(new_loop.state.delta_time, new_loop.state.timescale); + + // Return the now-initialized Loop + new_loop } /// Initializes or re-initializes the simulation @@ -126,11 +137,11 @@ pub mod core { } /// Executes the per-loop logic (can be triggered manually so that hypoloop can be tied into external event loops) - pub fn step(&mut self, mut update_callback: impl FnMut(&mut State), display_callback: impl Fn(&State)) { + pub fn step(&mut self, mut update_callback: impl FnMut(&mut State), mut display_callback: impl FnMut(&mut State)) { // don't run if the simulation is paused if self.state.simulate { // TODO - support frameskips - if !self.realtime || delta_time(self.state.last_tick) >= self.state.update_interval { + if !self.realtime || delta_time(self.state.last_tick) >= self.update_interval { // mutable delta time and timescale for flexibility let elapsed_time = Instant::now().duration_since(self.state.last_tick); @@ -140,10 +151,11 @@ pub mod core { self.state.sim_time += elapsed_time.mul_f32(self.state.timescale); self.state.irl_time += elapsed_time; } else { - self.state.delta_time = self.state.update_interval; - self.state.sim_time += Duration::from_millis(self.state.update_interval as u64); + self.state.delta_time = self.update_interval; + self.state.sim_time += Duration::from_millis(self.update_interval as u64); self.state.irl_time = Instant::now().duration_since(self.state.clock_start); } + self.state.timestep = timestep(self.state.delta_time, self.state.timescale); // update update_callback(&mut self.state); @@ -154,7 +166,7 @@ pub mod core { // display if self.realtime { - display_callback(&self.state); + display_callback(&mut self.state); } } } @@ -163,10 +175,25 @@ pub mod core { pub fn set_realtime(&mut self, realtime: bool) { self.realtime = realtime; } + + /// Returns the "update interval", the minimum time (in ms) which will elapse between update ticks + pub fn get_update_interval(self) -> u32 { + self.update_interval + } + + /// Changes the update interval + pub fn set_update_interval(&mut self, update_interval: u32) { + self.update_interval = update_interval; + } } - // gets the time in milliseconds that's elapsed since the earlier Instant + // gets the real time (in ms) that's elapsed since the earlier Instant fn delta_time(earlier: Instant) -> u32 { Instant::now().duration_since(earlier).as_millis() as u32 } + + // returns the fractional timestep (in s) based on delta time and timescale + fn timestep(delta_time: u32, timescale: f32) -> f32 { + delta_time as f32 / 1000.0 * timescale + } } \ No newline at end of file