Repository: aochagavia/rocket_wasm Branch: master Commit: 95c902703d72 Files: 26 Total size: 33.4 KB Directory structure: gitextract_okp2mtnu/ ├── .gitattributes ├── .gitignore ├── Cargo.toml ├── LICENSE.md ├── html/ │ ├── index.html │ └── program.wasm ├── post_build.py ├── readme.md └── src/ ├── controllers/ │ ├── collisions.rs │ ├── input.rs │ ├── mod.rs │ └── time.rs ├── game_state.rs ├── geometry/ │ ├── mod.rs │ ├── point.rs │ ├── size.rs │ └── traits.rs ├── lib.rs ├── models/ │ ├── bullet.rs │ ├── enemy.rs │ ├── mod.rs │ ├── particle.rs │ ├── player.rs │ ├── vector.rs │ └── world.rs └── util.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto ================================================ FILE: .gitignore ================================================ target .vscode .idea html/rocket.wasm ================================================ FILE: Cargo.toml ================================================ [package] name = "rocket" version = "1.0.0" authors = ["Adolfo Ochagavía "] [lib] crate-type = ["cdylib"] [dependencies] clippy = { version = "0.0.118", optional = true } itertools-num = "0.1.1" lazy_static = "1.0" rand = "0.3.18" pcg_rand = "0.7.1" [patch.crates-io] rand = { git = "https://github.com/aochagavia/rand.git", branch = "wasm" } [features] default = [] ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2015 Adolfo Ochagavía Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: html/index.html ================================================ ================================================ FILE: post_build.py ================================================ from shutil import copyfile from subprocess import call copyfile('target/wasm32-unknown-unknown/release/rocket.wasm', 'html/rocket.wasm') call(['wasm-gc', 'html/rocket.wasm', 'html/program.wasm']) ================================================ FILE: readme.md ================================================ Rocket on WASM ============== An adapted version of the [Rocket](https://github.com/aochagavia/rocket) game, running on WASM! [Read the blog post](https://ochagavia.nl/blog/rocket-a-rust-game-running-on-wasm/) about the development of this WASM version (includes an embedded canvas where you can play the game). ## Screenshot ![Screenshot](screenshots/gameplay1.png) ## How to play As you can see in the screenshot, you are the red rocket and have to save the world from the yellow invaders. To do so, you can use the following controls: Keyboard | Action ----------------------- | ------------ | Boost | Rotate left | Rotate right Space | Shoot ## Compiling and running Follow the steps on the [hellorust website](https://www.hellorust.com/setup/wasm-target/) in order to set up everything. Besides the `wasm32-unknown-unknown` target, the `post_build.py` script requires python 2.7 and `wasm-gc`. After setting things up, you should be able to compile the code using the commands below: ``` cargo build --release --target wasm32-unknown-unknown python post_build.py ``` The generated wasm will be copied to the `html` directory and `wasm-gc`ed. ``` python -m SimpleHTTPServer ``` Try opening http://localhost:8000/ on your browser to check whether it works. ## Related projects * [Running Rocket in a Python environment through WebAssembly](https://almarklein.org/python_and_webassembly.html) ================================================ FILE: src/controllers/collisions.rs ================================================ use game_state::GameState; use geometry::{Collide, Position}; use util; const SCORE_PER_ENEMY: u32 = 10; pub struct CollisionsController; impl CollisionsController { pub fn handle_collisions(state: &mut GameState) { CollisionsController::handle_bullet_collisions(state); CollisionsController::handle_player_collisions(state); } /// Handles collisions between the bullets and the enemies /// /// When an enemy is reached by a bullet, both the enemy and the bullet /// will be removed. Additionally, the score of the player will be increased. fn handle_bullet_collisions(state: &mut GameState) { let old_enemy_count = state.world.enemies.len(); // We introduce a scope to shorten the lifetime of the borrows below { let bullets = &mut state.world.bullets; 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 // 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) // Add an explosion on its place if let Some((index, position)) = enemies.iter().enumerate() .find(|&(_, enemy)| enemy.collides_with(bullet)) .map(|(index, enemy)| (index, enemy.position())) { util::make_explosion(particles, &position, 10); enemies.remove(index); false } else { true } }); } let killed_enemies = (old_enemy_count - state.world.enemies.len()) as u32; state.score += SCORE_PER_ENEMY * killed_enemies; } /// Handles collisions between the player and the enemies fn handle_player_collisions(state: &mut GameState) { if 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, 8); state.reset(); } } } ================================================ FILE: src/controllers/input.rs ================================================ /// Active actions (toggled by user input) #[derive(Default)] pub struct Actions { pub rotate_left: bool, pub rotate_right: bool, pub boost: bool, pub shoot: bool } ================================================ FILE: src/controllers/mod.rs ================================================ //! This module contains the game logic //! //! There are three main controllers: collisions, input and time mod collisions; mod input; mod time; pub use self::collisions::CollisionsController; pub use self::input::Actions; pub use self::time::TimeController; ================================================ FILE: src/controllers/time.rs ================================================ use std::f64; use rand::Rng; use super::Actions; use game_state::GameState; use geometry::{Advance, Position, Point}; use models::{Bullet, Enemy, Particle, Vector}; use util; // Constants related to time const BULLETS_PER_SECOND: f64 = 100.0; const BULLET_RATE: f64 = 1.0 / BULLETS_PER_SECOND; const ENEMY_SPAWNS_PER_SECOND: f64 = 1.0; 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; // Constants related to movement // Speed is measured in pixels per second // Rotation speed is measured in radians per second const ADVANCE_SPEED: f64 = 200.0; const BULLET_SPEED: f64 = 500.0; const ENEMY_SPEED: f64 = 100.0; const ROTATE_SPEED: f64 = 2.0 * f64::consts::PI; const PLAYER_GRACE_AREA: f64 = 200.0; /// Timers to handle creation of bullets, enemies and particles pub struct TimeController { /// A random number generator rng: T, current_time: f64, last_tail_particle: f64, last_shoot: f64, last_spawned_enemy: f64 } impl TimeController { pub fn new(rng: T) -> TimeController { TimeController { rng, current_time: 0.0, last_tail_particle: 0.0, last_shoot: 0.0, last_spawned_enemy: 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) { self.current_time += dt; // Update rocket rotation if actions.rotate_left { *state.world.player.direction_mut() += -ROTATE_SPEED * dt; } 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); // Update particles for particle in &mut state.world.particles { particle.update(dt); } // Remove old particles util::fast_retain(&mut state.world.particles, |p| p.ttl > 0.0); // Add new particles at the player's position, to leave a trail if self.current_time - self.last_tail_particle > TRAIL_PARTICLE_RATE { self.last_tail_particle = self.current_time; state.world.particles.push(Particle::new(state.world.player.vector.clone().invert(), 0.5)); } // Add bullets if actions.shoot && self.current_time - self.last_shoot > BULLET_RATE { self.last_shoot = self.current_time; state.world.bullets.push(Bullet::new(Vector::new(state.world.player.front(), state.world.player.direction()))); } // Advance bullets for bullet in &mut state.world.bullets { bullet.update(dt * BULLET_SPEED); } // Remove bullets outside the viewport { // Shorten the lifetime of size let size = &state.world.size; util::fast_retain(&mut state.world.bullets, |b| size.contains(b.position())); } // Spawn enemies at random locations if self.current_time - self.last_spawned_enemy > ENEMY_SPAWN_RATE { self.last_spawned_enemy = self.current_time; let player_pos: &Vector = &state.world.player.vector; let mut enemy_pos; // We loop here, just in case the new enemy random position is exactly equal // to the players current position, this would break our calculations below loop { enemy_pos = Vector::random(&mut self.rng, state.world.size); if enemy_pos.position != player_pos.position { break; } } // Check if the newly spawned enemy is inside the player's grace area, // if so, we push its spawn point to the edge of the area if enemy_pos.position.intersect_circle(&player_pos.position, PLAYER_GRACE_AREA) { let length: f64 = enemy_pos.position.squared_distance_to(&player_pos.position).sqrt(); let dp: Point = enemy_pos.position - player_pos.position; enemy_pos.position = player_pos.position + dp / length * PLAYER_GRACE_AREA; } let new_enemy = Enemy::new(enemy_pos); state.world.enemies.push(new_enemy); } // Move enemies in the player's direction for enemy in &mut state.world.enemies { enemy.update(dt * ENEMY_SPEED, state.world.player.position()); } } } ================================================ FILE: src/game_state.rs ================================================ use pcg_rand::Pcg32Basic; use rand::SeedableRng; use geometry::{Position, Size}; use models::World; /// The data structure that contains the state of the game pub struct GameState { /// The world contains everything that needs to be drawn pub world: World, /// The current score of the player pub score: u32 } impl GameState { /// Returns a new `GameState` containing a `World` of the given `Size` pub fn new(size: Size) -> GameState { let mut rng = Pcg32Basic::from_seed([42, 42]); GameState { world: World::new(&mut rng, size), score: 0 } } /// Reset our game-state pub fn reset(&mut self) { let mut rng = Pcg32Basic::from_seed([42, 42]); // Reset player position *self.world.player.x_mut() = self.world.size.random_x(&mut rng); *self.world.player.y_mut() = self.world.size.random_y(&mut rng); // Reset score self.score = 0; // Remove all enemies and bullets self.world.bullets.clear(); self.world.enemies.clear(); } } ================================================ FILE: src/geometry/mod.rs ================================================ mod point; mod size; mod traits; pub use self::point::Point; pub use self::size::Size; pub use self::traits::{Position, Advance, Collide}; ================================================ FILE: src/geometry/point.rs ================================================ use rand::Rng; use super::Size; use std::ops::{Add, Sub, Mul, Div}; /// A `Point` represents a position in space #[derive(Clone, Default, Copy)] pub struct Point { pub x: f64, pub y: f64 } impl Point { /// Returns a new `Point` with the given coordinates pub fn new(x: f64, y: f64) -> Point { Point { x: x, y: y } } /// Returns a random `Point` within the given bounds (exclusive) pub fn random(rng: &mut R, bounds: Size) -> Point { Point { x: rng.gen_range(0.0, bounds.width), y: rng.gen_range(0.0, bounds.height) } } /// Returns the squared distance from this point to the given one pub fn squared_distance_to(&self, target: &Point) -> f64 { (self.x - target.x) * (self.x - target.x) + (self.y - target.y) * (self.y - target.y) } /// Rotates the point through the origin in the given angle (radians) pub fn rotate(mut self, radians: f64) -> Point { let radius = (self.x * self.x + self.y * self.y).sqrt(); let point_angle = (self.y / self.x).atan(); let final_angle = point_angle + radians; self.x = final_angle.cos() * radius; self.y = final_angle.sin() * radius; self } /// Translates the point by another point pub fn translate(mut self, other: &Point) -> Point { self.x += other.x; self.y += other.y; self } /// Checks if this point is contained in a circle pub fn intersect_circle(self, center: &Point, radius: f64) -> bool { (self.x - center.x).powi(2) + (self.y - center.y).powi(2) < radius.powi(2) } } /// Implements '==' for Point, as well as its inverse '!=' impl PartialEq for Point { fn eq (&self, _rhs: &Self) -> bool { (self.x == _rhs.x) && (self.y == _rhs.y) } } /// Implements the '+' operator for Point + Point impl Add for Point { type Output = Point; fn add(self, _rhs: Point) -> Point { Point { x: self.x + _rhs.x, y: self.y + _rhs.y, } } } /// Implements the '+' operator for Point + f64 impl Add for Point { type Output = Point; fn add(self, _rhs: f64) -> Point { Point { x: self.x + _rhs, y: self.y + _rhs, } } } /// Implements the '-' operator for Point - Point impl Sub for Point { type Output = Point; fn sub(self, _rhs: Point) -> Point { Point { x: self.x - _rhs.x, y: self.y - _rhs.y, } } } /// Implements the '-' operator for Point - f64 impl Sub for Point { type Output = Point; fn sub(self, _rhs: f64) -> Point { Point { x: self.x - _rhs, y: self.y - _rhs, } } } /// Implements the '*' operator for Point * Point impl Mul for Point { type Output = Point; fn mul(self, _rhs: Point) -> Point { Point { x: self.x * _rhs.x, y: self.y * _rhs.y, } } } /// Implements the '*' operator for Point * f64 impl Mul for Point { type Output = Point; fn mul(self, _rhs: f64) -> Point { Point { x: self.x * _rhs, y: self.x * _rhs, } } } /// Implements the '/' operator for Point / Point impl Div for Point { type Output = Point; fn div(self, _rhs: Point) -> Point { assert!(_rhs.x != 0f64); assert!(_rhs.y != 0f64); Point { x: self.x / _rhs.x, y: self.y / _rhs.y, } } } /// Implements the '/' operator for Point / f64: impl Div for Point { type Output = Point; fn div(self, _rhs: f64) -> Point { assert!(_rhs != 0f64); Point { x: self.x / _rhs, y: self.y / _rhs, } } } ================================================ FILE: src/geometry/size.rs ================================================ use rand::Rng; use super::Point; /// A `Size` represents a region in space #[derive(Clone, Copy, Default)] pub struct Size { pub width: f64, pub height: f64 } impl Size { /// Returns a new `Size` of the given dimensions pub fn new(width: f64, height: f64) -> Size { Size { width: width, height: height } } /// Returns true if the `Point` is contained in this `Size` or false otherwise pub fn contains(&self, point: Point) -> bool { 0.0 <= point.x && point.x <= self.width && 0.0 <= point.y && point.y <= self.height } /// Returns a random x coordinate within the bounds of this `Size` pub fn random_x(&self, rng: &mut R) -> f64 { rng.gen_range(0.0, self.width) } /// Returns a random y coordinate within the bounds of this `Size` pub fn random_y(&self, rng: &mut R) -> f64 { rng.gen_range(0.0, self.height) } } ================================================ FILE: src/geometry/traits.rs ================================================ //! Traits used by the models use std::f64; use super::{Point, Size}; /// A trait for objects that occupy a position in space pub trait Position { /// Returns the x coordinate of the object fn x(&self) -> f64; /// Returns a mutable reference to the x coordinate fn x_mut(&mut self) -> &mut f64; /// Returns the y coordinate of the object fn y(&self) -> f64; /// Returns a mutable reference to the y coordinate fn y_mut(&mut self) -> &mut f64; /// Returns the position of the object fn position(&self) -> Point { Point::new(self.x(), self.y()) } } /// A trait for objects that have can move in a given direction pub trait Advance: Position { /// Returns the direction of the object, measured in radians /// /// Note: 0.0 points to the right and a positive number means a clockwise /// rotation fn direction(&self) -> f64; /// Returns a mutable reference to the direction of the object fn direction_mut(&mut self) -> &mut f64; /// Changes the direction of the vector to point to the given target fn point_to(&mut self, target: Point) { let m = (self.y() - target.y) / (self.x() - target.x); *self.direction_mut() = if target.x > self.x() { m.atan() } else { m.atan() + f64::consts::PI }; } /// Advances the object in the given amount of units, according to its direction fn advance(&mut self, units: f64) { *self.x_mut() += self.direction().cos() * units; *self.y_mut() += self.direction().sin() * units; } /// Similar to `Advance::advance`, but the final position will be wrapped /// around the given bounds fn advance_wrapping(&mut self, units: f64, bounds: Size) { self.advance(units); fn wrap(k: &mut f64, bound: f64) { if *k < 0.0 { *k += bound; } else if *k >= bound { *k -= bound; } } wrap(self.x_mut(), bounds.width); wrap(self.y_mut(), bounds.height); } } /// A trait that provides collision detection for objects with a position and a radius /// /// For collision purposes, all objects are treated as circles pub trait Collide: Position { /// Returns the radius of the object fn radius(&self) -> f64; /// Returns the diameter of the objects fn diameter(&self) -> f64 { self.radius() * 2.0 } /// Returns true if the two objects collide and false otherwise fn collides_with(&self, other: &O) -> bool { let radii = self.radius() + other.radius(); self.position().squared_distance_to(&other.position()) < radii * radii } } ================================================ FILE: src/lib.rs ================================================ extern crate itertools_num; #[macro_use] extern crate lazy_static; extern crate rand; extern crate pcg_rand; mod controllers; mod game_state; mod geometry; mod models; mod util; use std::os::raw::{c_double, c_int}; use std::sync::Mutex; use pcg_rand::Pcg32Basic; use rand::SeedableRng; use self::game_state::GameState; use self::geometry::Size; use self::controllers::{Actions, TimeController, CollisionsController}; lazy_static! { static ref DATA: Mutex = Mutex::new(new_game_data(1024.0, 600.0)); } struct GameData { state: GameState, actions: Actions, time_controller: TimeController } fn new_game_data(width: f64, height: f64) -> GameData { GameData { state: GameState::new(Size::new(width, height)), actions: Actions::default(), time_controller: TimeController::new(Pcg32Basic::from_seed([42, 42])) } } // These functions are provided by the runtime extern "C" { fn clear_screen(); fn draw_player(_: c_double, _: c_double, _: c_double); fn draw_enemy(_: c_double, _: c_double); fn draw_bullet(_: c_double, _: c_double); fn draw_particle(_: c_double, _: c_double, _: c_double); fn draw_score(_: c_double); } #[no_mangle] pub extern "C" fn resize(width: c_double, height: c_double) { *DATA.lock().unwrap() = new_game_data(width, height); } #[no_mangle] pub unsafe extern "C" fn draw() { use geometry::{Advance, Position}; let data = &mut DATA.lock().unwrap(); let world = &data.state.world; clear_screen(); for particle in &world.particles { draw_particle(particle.x(), particle.y(), 5.0 * particle.ttl); } for bullet in &world.bullets { draw_bullet(bullet.x(), bullet.y()); } for enemy in &world.enemies { draw_enemy(enemy.x(), enemy.y()); } draw_player(world.player.x(), world.player.y(), world.player.direction()); draw_score(data.state.score as f64); } #[no_mangle] pub extern "C" fn update(time: c_double) { let data: &mut GameData = &mut DATA.lock().unwrap(); data.time_controller.update_seconds(time, &data.actions, &mut data.state); CollisionsController::handle_collisions(&mut data.state); } fn int_to_bool(i: c_int) -> bool { i != 0 } #[no_mangle] pub extern "C" fn toggle_shoot(b: c_int) { let data = &mut DATA.lock().unwrap(); data.actions.shoot = int_to_bool(b); } #[no_mangle] pub extern "C" fn toggle_boost(b: c_int) { let data = &mut DATA.lock().unwrap(); data.actions.boost = int_to_bool(b); } #[no_mangle] pub extern "C" fn toggle_turn_left(b: c_int) { let data = &mut DATA.lock().unwrap(); data.actions.rotate_left = int_to_bool(b); } #[no_mangle] pub extern "C" fn toggle_turn_right(b: c_int) { let data = &mut DATA.lock().unwrap(); data.actions.rotate_right = int_to_bool(b); } ================================================ FILE: src/models/bullet.rs ================================================ use super::Vector; use geometry::{Advance, Collide}; /// Bullets are spawned when the player shoots /// /// When an enemy is reached by a bullet, it will explode pub struct Bullet { vector: Vector } derive_position_direction!(Bullet); impl Bullet { /// Create a bullet with the given vector pub fn new(vector: Vector) -> Bullet { Bullet { vector: vector } } /// Update the bullet's position pub fn update(&mut self, units: f64) { self.advance(units); } } impl Collide for Bullet { fn radius(&self) -> f64 { 3.0 } } ================================================ FILE: src/models/enemy.rs ================================================ use geometry::Point; use super::Vector; use geometry::{Advance, Collide}; /// Enemies follow the player in order to cause a collision and let him explode pub struct Enemy { vector: Vector } derive_position_direction!(Enemy); impl Enemy { /// Create a enemy with the given vector pub fn new(vector: Vector) -> Enemy { Enemy { vector: vector } } /// Update the enemy pub fn update(&mut self, speed: f64, player_position: Point) { // Point to the player self.point_to(player_position); self.advance(speed); } } impl Collide for Enemy { fn radius(&self) -> f64 { 10.0 } } ================================================ FILE: src/models/mod.rs ================================================ // macro_use needs to go first so the macro is visible for the other modules #[macro_use] mod vector; mod bullet; mod enemy; mod particle; mod player; mod world; pub use self::bullet::Bullet; pub use self::enemy::Enemy; pub use self::particle::Particle; pub use self::player::{Player, POLYGON as PLAYER_POLYGON}; pub use self::vector::Vector; pub use self::world::World; ================================================ FILE: src/models/particle.rs ================================================ use super::Vector; use geometry::Advance; /// A model representing a particle /// /// Particles are visible objects that have a time to live and move around /// in a given direction until their time is up. They are spawned when the /// player or an enemy is killed pub struct Particle { pub vector: Vector, pub ttl: f64 } derive_position_direction!(Particle); impl Particle { /// Create a particle with the given vector and time to live in seconds pub fn new(vector: Vector, ttl: f64) -> Particle { Particle { vector: vector, ttl: ttl } } /// Update the particle pub fn update(&mut self, elapsed_time: f64) { self.ttl -= elapsed_time; let speed = 500.0 * self.ttl * self.ttl; self.advance(elapsed_time * speed); } } ================================================ FILE: src/models/player.rs ================================================ use rand::Rng; use geometry::{Point, Size}; use super::Vector; use geometry::{Advance, Collide, Position}; /// The `Player` is the rocket controlled by the user #[derive(Default)] pub struct Player { pub vector: Vector } derive_position_direction!(Player); /// The player is represented as the polygon below pub const POLYGON: &'static [[f64; 2]] = &[ [0.0, -8.0], [20.0, 0.0], [0.0, 8.0] ]; impl Player { /// Create a new `Player` with a random position and direction pub fn random(rng: &mut R, bounds: Size) -> Player { Player { vector: Vector::random(rng, bounds) } } /// Returns the front of the rocket pub fn front(&self) -> Point { Point::new(POLYGON[1][0], POLYGON[1][1]) .rotate(self.direction()) .translate(&self.position()) } } impl Collide for Player { fn radius(&self) -> f64 { 6.0 } } ================================================ FILE: src/models/vector.rs ================================================ use std::f64; use rand::Rng; use geometry::{Point, Size}; /// A `Vector` #[derive(Clone, Default)] pub struct Vector { /// The position of the vector pub position: Point, /// The direction angle, in radians pub direction: f64 } impl Vector { /// Returns a new `Vector` pub fn new(position: Point, direction: f64) -> Vector { Vector { position: position, direction: direction } } /// Returns a random `Vector` within the given bounds pub fn random(rng: &mut R, bounds: Size) -> Vector { Vector::new(Point::random(rng, bounds), rng.gen()) } /// Consumes the vector and returns a new one with inverted direction pub fn invert(mut self) -> Vector { self.direction -= f64::consts::PI; self } } /// A macro to implement `Position` and `Direction` for any type that has a field named `vector` #[macro_export] macro_rules! derive_position_direction { ($t:ty) => { impl ::geometry::Position for $t { fn x(&self) -> f64 { self.vector.position.x } fn x_mut(&mut self) -> &mut f64 { &mut self.vector.position.x } fn y(&self) -> f64 { self.vector.position.y } fn y_mut(&mut self) -> &mut f64 { &mut self.vector.position.y } } impl ::geometry::Advance for $t { fn direction(&self) -> f64 { self.vector.direction } fn direction_mut(&mut self) -> &mut f64 { &mut self.vector.direction } } } } ================================================ FILE: src/models/world.rs ================================================ use rand::Rng; use geometry::Size; use models::{Bullet, Enemy, Particle, Player}; /// A model that contains the other models and renders them pub struct World { pub player: Player, pub particles: Vec, pub bullets: Vec, pub enemies: Vec, pub size: Size } impl World { /// Returns a new world of the given size pub fn new(rng: &mut R, size: Size) -> World { World { player: Player::random(rng, size), particles: Vec::with_capacity(1000), bullets: vec![], enemies: vec![], size: size } } } ================================================ FILE: src/util.rs ================================================ use geometry::Point; use models::{Particle, Vector}; /// Optimized version of `Vec::retain` /// /// We achieve better performance by renouncing to keep the original order of the `Vec` pub fn fast_retain(vec: &mut Vec, mut f: F) where F: FnMut(&T) -> bool { let mut i = 0; while i < vec.len() { if !f(&vec[i]) { vec.swap_remove(i); } i += 1; } } /// Generates a new explosion of the given intensity at the given position. /// This works best with values between 5 and 25 pub fn make_explosion(particles: &mut Vec, position: &Point, intensity: u8) { use itertools_num; for rotation in itertools_num::linspace(0.0, 2.0 * ::std::f64::consts::PI, 30) { for ttl in (1..intensity).map(|x| (x as f64) / 10.0) { particles.push(Particle::new(Vector::new(position.clone(), rotation), ttl)); } } }