Skip to content

Commit

Permalink
Fix delta time in the invaders example (#252)
Browse files Browse the repository at this point in the history
- Minor rewrite of the winit integration using the `game-loop` crate for
  fixed time-step updates.
- Updates are now handled at 240 fps, regardless of frame rate.
- Frame rate is capped at 240 fps.
- Adds a pause key.
- Closes #11
  • Loading branch information
parasyte committed Jan 8, 2022
1 parent afd1543 commit 3968c97
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 123 deletions.
1 change: 1 addition & 0 deletions examples/invaders/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ default = ["optimize"]
[dependencies]
byteorder = "1.3"
env_logger = "0.9"
game-loop = { version = "0.8", features = ["window"] }
getrandom = "0.2"
gilrs = "0.8"
log = "0.4"
Expand Down
16 changes: 13 additions & 3 deletions examples/invaders/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,21 @@ The pixels have invaded!
cargo run --release --package invaders
```

## Controls
## Keyboard Controls

<kbd>Left</kbd> <kbd>Right</kbd> Move tank
<kbd>🡰</kbd> <kbd>🡲</kbd>: Move tank

<kbd>space</kbd> Fire cannon
<kbd>Space</kbd>: Fire cannon

<kbd>Pause</kbd> <kbd>P</kbd>: Pause

## GamePad Controls

`D-Pad 🡰` `D-Pad 🡲`: Move tank

`XBox 🅐` `PS 🅧` `Switch 🅑`: Fire cannon

`XBox/PS ≡` `Switch ⊕︀`: Pause

## Goal

Expand Down
4 changes: 2 additions & 2 deletions examples/invaders/simple-invaders/src/collision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ impl Collision {
];

for (x, y) in corners.iter() {
let col = (x - left) / GRID.x + invaders.bounds.left_col;
let row = (y - top) / GRID.y + invaders.bounds.top_row;
let col = x.saturating_sub(left) / GRID.x + invaders.bounds.left_col;
let row = y.saturating_sub(top) / GRID.y + invaders.bounds.top_row;

if col < COLS && row < ROWS && invaders.grid[row][col].is_some() {
let detail = BulletDetail::Invader(col, row);
Expand Down
87 changes: 56 additions & 31 deletions examples/invaders/simple-invaders/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@
#![deny(clippy::all)]
#![forbid(unsafe_code)]

use std::time::Duration;

use crate::collision::Collision;
pub use crate::controls::{Controls, Direction};
use crate::geo::Point;
use crate::loader::{load_assets, Assets};
use crate::sprites::{blit, Animation, Drawable, Frame, Sprite, SpriteRef};
use randomize::PCG32;
use std::time::Duration;

mod collision;
mod controls;
Expand All @@ -28,6 +27,12 @@ pub const WIDTH: usize = 224;
/// The screen height is constant (units are in pixels)
pub const HEIGHT: usize = 256;

// Fixed time step (240 fps)
pub const FPS: usize = 240;
pub const TIME_STEP: Duration = Duration::from_nanos(1_000_000_000 / FPS as u64);
// Internally, the game advances at 60 fps
const ONE_FRAME: Duration = Duration::from_nanos(1_000_000_000 / 60);

// Invader positioning
const START: Point = Point::new(24, 64);
const GRID: Point = Point::new(16, 16);
Expand Down Expand Up @@ -92,7 +97,7 @@ struct Bounds {
struct Player {
sprite: SpriteRef,
pos: Point,
dt: usize,
dt: Duration,
}

/// The shield entity.
Expand All @@ -108,15 +113,45 @@ struct Shield {
struct Laser {
sprite: SpriteRef,
pos: Point,
dt: usize,
dt: Duration,
}

/// The cannon entity.
#[derive(Debug)]
struct Bullet {
sprite: SpriteRef,
pos: Point,
dt: usize,
dt: Duration,
}

trait DeltaTime {
fn update(&mut self) -> usize;

fn update_dt(dest_dt: &mut Duration, step: Duration) -> usize {
*dest_dt += TIME_STEP;
let frames = dest_dt.as_nanos() / step.as_nanos();
*dest_dt -= Duration::from_nanos((frames * step.as_nanos()) as u64);

frames as usize
}
}

impl DeltaTime for Player {
fn update(&mut self) -> usize {
Self::update_dt(&mut self.dt, ONE_FRAME)
}
}

impl DeltaTime for Laser {
fn update(&mut self) -> usize {
Self::update_dt(&mut self.dt, ONE_FRAME)
}
}

impl DeltaTime for Bullet {
fn update(&mut self) -> usize {
Self::update_dt(&mut self.dt, TIME_STEP)
}
}

impl World {
Expand Down Expand Up @@ -168,7 +203,7 @@ impl World {
let player = Player {
sprite: SpriteRef::new(&assets, Player1, Duration::from_millis(100)),
pos: PLAYER_START,
dt: 0,
dt: Duration::default(),
};
let bullet = None;
let collision = Collision::default();
Expand Down Expand Up @@ -200,36 +235,34 @@ impl World {
///
/// * `dt`: The time delta since last update.
/// * `controls`: The player inputs.
pub fn update(&mut self, dt: &Duration, controls: &Controls) {
pub fn update(&mut self, controls: &Controls) {
if self.gameover {
// TODO: Add a game over screen
return;
}

let one_frame = Duration::new(0, 16_666_667);

// Advance the timer by the delta time
self.dt += *dt;
self.dt += TIME_STEP;

// Clear the collision details
self.collision.clear();

// Step the invaders one by one
while self.dt >= one_frame {
self.dt -= one_frame;
while self.dt >= ONE_FRAME {
self.dt -= ONE_FRAME;
self.step_invaders();
}

// Handle player movement and animation
self.step_player(controls, dt);
self.step_player(controls);

if let Some(bullet) = &mut self.bullet {
// Handle bullet movement
let velocity = update_dt(&mut bullet.dt, dt) * 4;
let velocity = bullet.update();

if bullet.pos.y > velocity {
bullet.pos.y -= velocity;
bullet.sprite.animate(&self.assets, dt);
bullet.sprite.animate(&self.assets);

// Handle collisions
if self
Expand All @@ -250,11 +283,11 @@ impl World {
// Handle laser movement
let mut destroy = Vec::new();
for (i, laser) in self.lasers.iter_mut().enumerate() {
let velocity = update_dt(&mut laser.dt, dt) * 2;
let velocity = laser.update() * 2;

if laser.pos.y < self.player.pos.y {
laser.pos.y += velocity;
laser.sprite.animate(&self.assets, dt);
laser.sprite.animate(&self.assets);

// Handle collisions
if self.collision.laser_to_player(laser, &self.player) {
Expand Down Expand Up @@ -387,28 +420,28 @@ impl World {
let laser = Laser {
sprite: SpriteRef::new(&self.assets, Frame::Laser1, Duration::from_millis(16)),
pos: invader.pos + LASER_OFFSET,
dt: 0,
dt: Duration::default(),
};
self.lasers.push(laser);
}
}

fn step_player(&mut self, controls: &Controls, dt: &Duration) {
let frames = update_dt(&mut self.player.dt, dt);
fn step_player(&mut self, controls: &Controls) {
let frames = self.player.update();
let width = self.player.sprite.width();

match controls.direction {
Direction::Left => {
if self.player.pos.x > width {
self.player.pos.x -= frames;
self.player.sprite.animate(&self.assets, dt);
self.player.sprite.animate(&self.assets);
}
}

Direction::Right => {
if self.player.pos.x < WIDTH - width * 2 {
self.player.pos.x += frames;
self.player.sprite.animate(&self.assets, dt);
self.player.sprite.animate(&self.assets);
}
}
_ => (),
Expand All @@ -418,7 +451,7 @@ impl World {
self.bullet = Some(Bullet {
sprite: SpriteRef::new(&self.assets, Frame::Bullet1, Duration::from_millis(32)),
pos: self.player.pos + BULLET_OFFSET,
dt: 0,
dt: Duration::default(),
});
}
}
Expand Down Expand Up @@ -612,11 +645,3 @@ fn next_invader<'a>(
}
}
}

fn update_dt(dest_dt: &mut usize, dt: &Duration) -> usize {
*dest_dt += dt.subsec_nanos() as usize;
let frames = *dest_dt / 16_666_667;
*dest_dt -= frames * 16_666_667;

frames
}
14 changes: 7 additions & 7 deletions examples/invaders/simple-invaders/src/sprites.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use std::cmp::min;
use std::rc::Rc;
use std::time::Duration;

use crate::loader::Assets;
use crate::TIME_STEP;
use crate::{Point, HEIGHT, WIDTH};
use line_drawing::Bresenham;
use std::cmp::min;
use std::rc::Rc;
use std::time::Duration;

// This is the type stored in the `Assets` hash map
pub(crate) type CachedSprite = (usize, usize, Rc<[u8]>);
Expand Down Expand Up @@ -74,7 +74,7 @@ pub(crate) trait Drawable {
}

pub(crate) trait Animation {
fn animate(&mut self, assets: &Assets, dt: &Duration);
fn animate(&mut self, assets: &Assets);
}

impl Sprite {
Expand Down Expand Up @@ -172,11 +172,11 @@ impl Drawable for SpriteRef {
}

impl Animation for SpriteRef {
fn animate(&mut self, assets: &Assets, dt: &Duration) {
fn animate(&mut self, assets: &Assets) {
if self.duration.subsec_nanos() == 0 {
self.step_frame(assets);
} else {
self.dt += *dt;
self.dt += TIME_STEP;

while self.dt >= self.duration {
self.dt -= self.duration;
Expand Down
Loading

0 comments on commit 3968c97

Please sign in to comment.