Skip to content

Commit

Permalink
Powerups (#79)
Browse files Browse the repository at this point in the history
* general updates and improvements

* added powerups

* removed powerups when game state is reset

* improvements

* improvements & updates
  • Loading branch information
acheronfail authored and aochagavia committed Jan 10, 2018
1 parent 5135a19 commit 3b327ef
Show file tree
Hide file tree
Showing 21 changed files with 356 additions and 82 deletions.
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ rand = "0.3.14"

[dependencies.ggez]
git = "https://github.com/ggez/ggez.git"
rev = "b208c6c8b22e48ea2a7b4d7fe02bd88454330640"
rev = "82066aaf2bd3813cb6cff94d07452b8b03b082f8"

[features]
default = []
debug = []
Binary file added resources/audio/powerup.ogg
Binary file not shown.
Binary file added resources/images/powerup_shield.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/images/powerup_time_slow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/images/powerup_triple_shot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
95 changes: 79 additions & 16 deletions src/controllers/collisions.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
use std::time::Duration;
use ggez::{self, Context};

use ApplicationState;
use Resources;
use {ApplicationState, Resources};
use super::PLAYER_GRACE_AREA;
use game_state::GameState;
use geometry::{Collide, Position};
use geometry::{Collide, Position, Point};
use models::{Enemy, Particle, PowerupKind};
use util;

const SCORE_PER_ENEMY: u32 = 10;
const POWERUP_DURATION: u64 = 10;

pub struct CollisionsController;

impl CollisionsController {
pub fn handle_collisions(app: &mut ApplicationState, ctx: &mut Context) {
CollisionsController::handle_bullet_collisions(&mut app.game_state, &app.resources);

// Powerups only last 5 seconds, so set a timer to reset it
let got_powerup = CollisionsController::handle_powerup_collisions(&mut app.game_state, &app.resources);
if got_powerup {
let when = ggez::timer::get_time_since_start(ctx) + Duration::from_secs(POWERUP_DURATION);
app.scheduled_events.push(when, |app| app.game_state.world.player.powerup = None);
}

// If the player died then we set a timeout (3 seconds) after which a game over message
// will appear, and the user will be able to restart.
let player_died = CollisionsController::handle_player_collisions(&mut app.game_state, &app.resources);
Expand All @@ -37,7 +46,7 @@ impl CollisionsController {
let enemies = &mut state.world.enemies;
let particles = &mut state.world.particles;

// Note: this is O(n * m) where n = amount of bullets and n = amount of enemies
// Note: this is O(n * m) where n = amount of bullets and m = amount of enemies
// This is pretty bad, but we don't care because n and m are small
util::fast_retain(bullets, |bullet| {
// Remove the first enemy that collides with a bullet (if any)
Expand All @@ -50,6 +59,9 @@ impl CollisionsController {
enemies.remove(index);

// Play enemy_destroyed_sound sound
// TODO: these sounds (like all the others) are queued rather than played
// atop of one another - this is a current limitation of ggez
// See https://github.com/ggez/ggez/issues/208
let _ = resources.enemy_destroyed_sound.play();
false
} else {
Expand All @@ -62,20 +74,71 @@ impl CollisionsController {
state.score += SCORE_PER_ENEMY * killed_enemies;
}

/// Handles collisions between the player and powerups
fn handle_powerup_collisions(state: &mut GameState, resources: &Resources) -> bool {
let mut gained_powerup = false;
let player = &mut state.world.player;
let powerups = &mut state.world.powerups;

if !player.is_dead {
if let Some((index, kind)) = powerups.iter().enumerate()
.find(|&(_, powerup)| powerup.collides_with(player))
.map(|(index, powerup)| (index, powerup.kind)) {

gained_powerup = true;

// Set player's powerup kind to the powerup we just picked up
player.powerup = Some(kind);
powerups.remove(index);

// Play the powerup sound
let _ = resources.powerup_sound.play();
}
}

return gained_powerup;
}

/// Handles collisions between the player and the enemies
/// This function will return true if the player died
fn handle_player_collisions(state: &mut GameState, resources: &Resources) -> bool {
let player_alive = !state.world.player.is_dead;
if player_alive && state.world.enemies.iter().any(|enemy| state.world.player.collides_with(enemy)) {
// Make an explosion where the player was
let ppos = state.world.player.position();
util::make_explosion(&mut state.world.particles, &ppos, 16);
// Mark the player as dead (to stop drawing it on screen)
state.world.player.is_dead = true;
// Play player_destroyed sound
let _ = resources.player_destroyed_sound.play();
return true;
let mut player_died = false;
let player = &mut state.world.player;

if !player.is_dead && state.world.enemies.iter().any(|enemy| player.collides_with(enemy)) {
// Remove shield powerup from player, also killing any enemies within close range
if let Some(PowerupKind::Shield) = player.powerup {
player.powerup = None;

let enemies = &mut state.world.enemies;
let particles = &mut state.world.particles;
CollisionsController::remove_surrounding_enemies(enemies, particles, player.position());
// Play enemy destroyed sound
let _ = resources.enemy_destroyed_sound.play();
} else {
// Make an explosion where the player was
let ppos = player.position();
util::make_explosion(&mut state.world.particles, &ppos, 16);
// Mark the player as dead (to stop drawing it on screen)
player_died = true;
player.is_dead = true;
// Play player_destroyed sound
let _ = resources.player_destroyed_sound.play();
}
}

false

return player_died;
}

fn remove_surrounding_enemies(enemies: &mut Vec<Enemy>, particles: &mut Vec<Particle>, point: Point) {
util::fast_retain(enemies, |enemy| {
let enemy_pos = enemy.position();
if enemy_pos.intersect_circle(&point, PLAYER_GRACE_AREA) {
util::make_explosion(particles, &enemy_pos, 10);
false
} else {
true
}
});
}
}
2 changes: 1 addition & 1 deletion src/controllers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ mod time;

pub use self::collisions::CollisionsController;
pub use self::input::{Actions, InputController};
pub use self::time::TimeController;
pub use self::time::{TimeController, PLAYER_GRACE_AREA};
107 changes: 83 additions & 24 deletions src/controllers/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use Resources;
use super::Actions;
use game_state::GameState;
use geometry::{Advance, Position, Point};
use models::{Bullet, Enemy, Particle, Vector};
use models::{Bullet, Powerup, PowerupKind, Enemy, Particle, Vector};
use util;

// Constants related to time
Expand All @@ -18,6 +18,9 @@ const ENEMY_SPAWN_RATE: f64 = 1.0 / ENEMY_SPAWNS_PER_SECOND;
const TRAIL_PARTICLES_PER_SECOND: f64 = 20.0;
const TRAIL_PARTICLE_RATE: f64 = 1.0 / TRAIL_PARTICLES_PER_SECOND;

const POWERUP_SPAWNS_PER_SECOND: f64 = 1.0 / 30.0; // every ~30 seconds
const POWERUP_SPAWN_RATE: f64 = 1.0 / POWERUP_SPAWNS_PER_SECOND;

// Constants related to movement
// Speed is measured in pixels per second
// Rotation speed is measured in radians per second
Expand All @@ -27,7 +30,7 @@ const ENEMY_SPEED: f64 = 100.0;
const ROTATE_SPEED: f64 = 2.0 * f64::consts::PI;
const STAR_BASE_SPEED: f64 = 50.0;

const PLAYER_GRACE_AREA: f64 = 200.0;
pub const PLAYER_GRACE_AREA: f64 = 200.0;

/// Timers to handle creation of bullets, enemies and particles
pub struct TimeController {
Expand All @@ -36,7 +39,8 @@ pub struct TimeController {
current_time: f64,
last_tail_particle: f64,
last_shoot: f64,
last_spawned_enemy: f64
last_spawned_enemy: f64,
last_spawned_powerup: f64
}

impl TimeController {
Expand All @@ -46,49 +50,87 @@ impl TimeController {
current_time: 0.0,
last_tail_particle: 0.0,
last_shoot: 0.0,
last_spawned_enemy: 0.0
last_spawned_enemy: 0.0,
last_spawned_powerup: 0.0
}
}

// Called when the game is reset
pub fn reset(&mut self) {
self.last_shoot = 0.0;
self.current_time = 0.0;
self.last_tail_particle = 0.0;
self.last_spawned_enemy = 0.0;
self.last_spawned_powerup = 0.0;
}

/// Updates the game
///
/// `dt` is the amount of seconds that have passed since the last update
pub fn update_seconds(&mut self, dt: f64, actions: &Actions, state: &mut GameState, resources: &Resources) {
// You can run `cargo run --release --features "debug"` in order to run the game in
// slow motion (assists in debugging rendering)
#[cfg(feature = "debug")]
let dt = dt * 0.1;

self.current_time += dt;
state.difficulty += dt / 100.0;

self.update_player(dt, actions, state);
// Check if we have the "TimeSlow" powerup
let time_slow = state.world.player.powerup == Some(PowerupKind::TimeSlow);

// Only modify player/powerups if player is alive
if !state.world.player.is_dead {
self.update_player(dt, actions, state);
self.update_powerups(dt, state);
}

self.update_bullets(dt, actions, state, resources);
self.update_particles(dt, state);
self.update_enemies(dt, state, resources);
self.update_stars(dt, state);
self.update_enemies(dt, state, resources, time_slow);
self.update_stars(dt, state, time_slow);
}

// Updates the position and rotation of the player
fn update_player(&mut self, dt: f64, actions: &Actions, state: &mut GameState) {
if actions.rotate_left {
*state.world.player.direction_mut() += -ROTATE_SPEED * dt;
} else if actions.rotate_right {
*state.world.player.direction_mut() += ROTATE_SPEED * dt;
}
if !state.world.player.is_dead {
if actions.rotate_left {
*state.world.player.direction_mut() += -ROTATE_SPEED * dt;
} else if actions.rotate_right {
*state.world.player.direction_mut() += ROTATE_SPEED * dt;
}

// Set speed and advance the player with wrap around
let speed = if actions.boost { 2.0 * ADVANCE_SPEED } else { ADVANCE_SPEED };
state.world.player.advance_wrapping(dt * speed, state.world.size);
// Set speed and advance the player with wrap around
let speed = if actions.boost { 2.0 * ADVANCE_SPEED } else { ADVANCE_SPEED };
state.world.player.advance_wrapping(dt * speed, state.world.size);
}
}

// Adds, removes and updates the positions of bullets on screen
fn update_bullets(&mut self, dt: f64, actions: &Actions, state: &mut GameState, resources: &Resources) {
// Add bullets
if !state.world.player.is_dead && actions.shoot && self.current_time - self.last_shoot > BULLET_RATE {
self.last_shoot = self.current_time;
let vector = Vector::new(state.world.player.front(), state.world.player.direction());
state.world.bullets.push(Bullet::new(vector));

// TODO: get bullets working in a pleasant way
// if resources.shot_sound.playing() { resources.shot_sound.stop(); }
match state.world.player.powerup {
// If the player has the TripleShot powerup, apply that here
Some(PowerupKind::TripleShot) => {
let pos = state.world.player.front();
let dir = state.world.player.direction();
state.world.bullets.extend_from_slice(&[
Bullet::new(Vector::new(pos, dir - f64::consts::PI / 6.0)),
Bullet::new(Vector::new(pos, dir)),
Bullet::new(Vector::new(pos, dir + f64::consts::PI / 6.0)),
]);
}
// If there was no powerup, shoot normally
_ => {
let vector = Vector::new(state.world.player.front(), state.world.player.direction());
state.world.bullets.push(Bullet::new(vector));
}
}

let _ = resources.shot_sound.play();

}

// Advance bullets
Expand All @@ -101,6 +143,21 @@ impl TimeController {
util::fast_retain(&mut state.world.bullets, |b| size.contains(b.position()));
}

fn update_powerups(&mut self, dt: f64, state: &mut GameState) {
for powerup in &mut state.world.powerups {
powerup.update(dt);
}

// Remove any expired powerups
util::fast_retain(&mut state.world.powerups, |p| p.ttl > 0.0);

// Add new powerups
if self.current_time - self.last_spawned_powerup > POWERUP_SPAWN_RATE {
self.last_spawned_powerup = self.current_time;
state.world.powerups.push(Powerup::random(&mut self.rng, state.world.size));
}
}

// Updates or removes particles on screen, adds particles behind player
fn update_particles(&mut self, dt: f64, state: &mut GameState) {
for particle in &mut state.world.particles {
Expand All @@ -118,7 +175,7 @@ impl TimeController {
}

// Updates positions of enemies, and spawns new ones when necessary
fn update_enemies(&mut self, dt: f64, state: &mut GameState, resources: &Resources) {
fn update_enemies(&mut self, dt: f64, state: &mut GameState, resources: &Resources, time_slow: bool) {
// Spawn enemies at random locations
if self.current_time - self.last_spawned_enemy > ENEMY_SPAWN_RATE {
self.last_spawned_enemy = self.current_time;
Expand Down Expand Up @@ -160,18 +217,20 @@ impl TimeController {
// the direction they're facing
for enemy in &mut state.world.enemies {
if !state.world.player.is_dead {
enemy.update(dt * ENEMY_SPEED + state.difficulty, state.world.player.position());
let base_speed = if time_slow { ENEMY_SPEED - 75.0 } else { ENEMY_SPEED };
enemy.update(dt * base_speed + state.difficulty, state.world.player.position());
} else {
enemy.advance(dt * ENEMY_SPEED);
}
}
}

// Advance stars, wrapping them around the view
fn update_stars(&mut self, dt: f64, state: &mut GameState) {
fn update_stars(&mut self, dt: f64, state: &mut GameState, time_slow: bool) {
for star in &mut state.world.stars {
let speed = star.speed;
star.advance_wrapping(dt * STAR_BASE_SPEED * speed, state.world.size);
let base_speed = if time_slow { 20.0 } else { STAR_BASE_SPEED };
star.advance_wrapping(dt * base_speed * speed, state.world.size);
}
}
}
Loading

0 comments on commit 3b327ef

Please sign in to comment.