diff --git a/src/config.rs b/src/config.rs index 0b02307..ac94e31 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,29 +1,48 @@ -const SCREEN_ASPECT_RATIO: f32 = 0.75; +//! configuration for constants used within the game + +// aspect ratio of the screen (height / width) +const SCREEN_ASPECT_RATIO: f32 = 600.0 / 800.0; +// width and height of enemies in pixels const ENEMY_WIDTH_PX: f32 = 12.0; const ENEMY_HEIGHT_PX: f32 = 8.0; +// how many rows / columns of enemies to draw pub const ENEMY_ROWS: usize = 3; pub const ENEMY_COLS: usize = 6; +// space in between enemies pub const ENEMY_DISTANCE: f32 = 12.0; +// size of each pixel of the enemy texture pub const ENEMY_PIXEL_SIZE: f32 = 0.01; +// width and height of an enemy pub const ENEMY_WIDTH: f32 = ENEMY_WIDTH_PX * SCREEN_ASPECT_RATIO; pub const ENEMY_HEIGHT: f32 = ENEMY_HEIGHT_PX; +// horizontal and vertical enemy movement speed pub const ENEMY_SPEED: f32 = 0.2; pub const ENEMY_SPEED_VERT: f32 = 0.03; +// maximum horizontal enemy offset pub const ENEMY_CLAMP: f32 = 0.25; +// distance from one enemy to the next enemy (respective to their center points) pub const ENEMY_COL_SKIP: f32 = (ENEMY_WIDTH + ENEMY_DISTANCE) * ENEMY_PIXEL_SIZE; pub const ENEMY_ROW_SKIP: f32 = (ENEMY_HEIGHT + ENEMY_DISTANCE) * ENEMY_PIXEL_SIZE; +// where to place the top-left enemy pub const ENEMY_OFFSET_TOP: f32 = 0.7; pub const ENEMY_OFFSET_LEFT: f32 = ENEMY_PIXEL_SIZE * -((ENEMY_COLS as f32 / 2.0) * (ENEMY_WIDTH + ENEMY_DISTANCE) - ENEMY_DISTANCE); +// speed of the player pub const PLAYER_SPEED: f32 = 0.8; +// maximum / minimum player x-coordinate pub const PLAYER_CLAMP: f32 = 0.8; -pub const PLAYER_WIDTH: f32 = 0.13; +// width and height of the player +pub const PLAYER_WIDTH: f32 = 0.12; pub const PLAYER_HEIGHT: f32 = PLAYER_WIDTH * (8.0 / 12.0) / SCREEN_ASPECT_RATIO; +// y-coordinate of player pub const PLAYER_OFFSET_BOTTOM: f32 = -0.8; +// width and height of bullets pub const BULLET_WIDTH: f32 = 0.01; pub const BULLET_HEIGHT: f32 = 0.05; +// bullet speed pub const BULLET_SPEED: f32 = 1.5; +// time between shots pub const BULLET_DELAY: f32 = 500.0; diff --git a/src/game.rs b/src/game.rs index 96fbb81..347b87c 100644 --- a/src/game.rs +++ b/src/game.rs @@ -6,65 +6,108 @@ use web_sys::{window, KeyboardEvent}; use crate::{config, render::Renderer, utils}; +// include the source code for the vertex and fragment shader. the [`include_str!`] macro embeds +// the contents of the file into the program source code as a string. const VERT_SRC: &str = include_str!("shader/vert.glsl"); const FRAG_SRC: &str = include_str!("shader/frag.glsl"); +/// stores the current player state. struct Player { + // x position pos_x: f32, + // variables set by key up / key down events, used to determine if the left / right / shoot + // buttons are pressed is_moving_left: bool, is_moving_right: bool, is_shooting: bool, + // timestamp of the last time the player shot a bullet, used for achieving the delay between + // bullets fired by the player last_shoot_time: f32, } +/// stores position of an enemy. struct Enemy { + // x and y position pos_x: f32, pos_y: f32, + // the texture to display. expects a value from 0 to 2. sprite_type: u8, } +/// stores the position of a bullet. struct Bullet { + // x and y position pos_x: f32, pos_y: f32, + // direction that the bullet is moving in direction: f32, + // color of the bullet color: [f32; 3], } +/// stores the current state of the game and the game objects. pub struct Game { + // renderer object - see `render.rs` renderer: Renderer, + // player, enemies and bullets player: Player, enemies: Vec, - enemy_direction: f32, - enemy_offset: f32, player_bullets: Vec, enemy_bullets: Vec, + // the direction the enemies are moving in + enemy_direction: f32, + // the offset of the enemies, used for determining when they should turn around + enemy_offset: f32, + // timestamp of the last time an enemy shot a bullet, used for achieving the delay between + // bullets fired by enemies enemy_last_shoot_time: f32, + // time of the last frame, used for calculating delta time last_time: f32, + // is the game running? game_running: bool, } impl Game { + /// initialize the renderer and set up the game pub fn new() -> Result { + // obtain a renderer for the canvas of id `canvas` let mut renderer = Renderer::new("canvas")?; let vertices = vec![]; let indices = vec![]; - renderer.set_buffer(&vertices, &indices, false)?; + // start out with empty vertex and index arrays + renderer.initialize_buffer(&vertices, &indices, false)?; + + // create a shader program based on the shader source code included earlier renderer.set_shader_program(VERT_SRC, FRAG_SRC)?; + + // map the shader attributes. the layout of our vertices looks like this: + // + // ┌─────────────────┬─────────────────┬───────────┐ + // │ coordinates │ color │ texture │ + // │ ╷ ╷ │ ╷ ╷ │ ╷ │ + // │ X │ Y │ Z │ R │ G │ B │ U │ V │ + // └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘ + // + // stride, offset and data width have to be adjusted accordingly. renderer.map_shader_attribute("aCoord", 3, 8, 0)?; renderer.map_shader_attribute("aColor", 3, 8, 3)?; renderer.map_shader_attribute("aTexCoord", 2, 8, 6)?; + + // load the texture file renderer.set_texture("/texture.png")?; + // initialize the player let player = Player { pos_x: 0.0, is_moving_left: false, is_moving_right: false, is_shooting: false, - last_shoot_time: f32::NEG_INFINITY, + last_shoot_time: f32::NEG_INFINITY, // allow player to shoot right away }; + // create the enemy game objects let enemies = { let mut v = vec![]; let mut pos_x = config::ENEMY_OFFSET_LEFT; @@ -75,7 +118,7 @@ impl Game { v.push(Enemy { pos_x, pos_y, - sprite_type: row as u8, + sprite_type: row as u8, // give each row a different sprite }); pos_y -= config::ENEMY_ROW_SKIP; } @@ -86,48 +129,64 @@ impl Game { v }; + // finally, construct the game struct Ok(Self { renderer, player, enemies, - enemy_direction: config::ENEMY_SPEED, - enemy_offset: 0.0, - last_time: window().unwrap().performance().unwrap().now() as f32, player_bullets: vec![], enemy_bullets: vec![], - enemy_last_shoot_time: window().unwrap().performance().unwrap().now() as f32, + enemy_direction: config::ENEMY_SPEED, + enemy_offset: 0.0, game_running: true, + // obtain the current time and set it as the "first frame" + last_time: window().unwrap().performance().unwrap().now() as f32, + // enemies should wait a full shooting cycle before they can fire a bullet + enemy_last_shoot_time: window().unwrap().performance().unwrap().now() as f32, }) } + /// update the game state and draw it to the canvas fn update(&mut self, time: f32) { + // obtain delta time in seconds let delta = (time - self.last_time) / 1000.0; self.last_time = time; + // update player position self.player.update(delta); + // update bulletes fired by player self.player_bullets .iter_mut() .for_each(|bullet| bullet.update(delta)); + // update bulletes fired by enemies self.enemy_bullets .iter_mut() .for_each(|bullet| bullet.update(delta)); + // update enemy positions self.update_enemies(delta); + // allow player and enemies to fire bullets if they can self.check_player_bullet_shoot(time); self.check_enemy_bullet_shoot(time); + // check if bullets are colliding with anything self.check_player_bullet_collisions(); self.check_enemy_bullet_collisions(); + // check if the enemies got too low self.check_enemy_y(); + // draw the game self.draw(); } + /// update the position of the enemies fn update_enemies(&mut self, delta: f32) { + // if the enemies are outside of the valid range specified by [`config::ENEMY_CLAMP`], swap + // their movement direction if self.enemy_offset.abs() >= config::ENEMY_CLAMP { self.enemy_direction = -self.enemy_direction; self.enemy_offset = self @@ -135,6 +194,7 @@ impl Game { .clamp(-config::ENEMY_CLAMP, config::ENEMY_CLAMP); } + // move the enemies let step = self.enemy_direction * delta; self.enemy_offset += step; for enemy in self.enemies.iter_mut() { @@ -143,54 +203,66 @@ impl Game { } } + /// check if the player can shoot a bullet. if the player has the shoot button pressed and + /// enough time has passed since the previous shot, shoot a bullet. fn check_player_bullet_shoot(&mut self, time: f32) { if self.player.is_shooting && time - self.player.last_shoot_time >= config::BULLET_DELAY { + // shoot the bullet self.player_bullets.push(Bullet { - pos_x: self.player.pos_x, + pos_x: self.player.pos_x, // shoot from current player position pos_y: config::PLAYER_OFFSET_BOTTOM + 0.1, - direction: 1.0, - color: [0.0, 1.0, 0.0], + direction: 1.0, // upwards + color: [0.0, 1.0, 0.0], // green }); + + // update shoot time self.player.last_shoot_time = time; } - let player_bullets_to_remove = self - .player_bullets - .iter() - .filter(|x| x.out_of_bounds()) - .enumerate() - .map(|(i, _)| i) - .collect::>(); - - for i in player_bullets_to_remove { - self.player_bullets.remove(i); - } + // remove out-of-bounds bullets + self.player_bullets.retain(|x| !x.out_of_bounds()); } + /// check if the enemies can shoot a bullet. if enough time has passed since the previous shot, + /// a random enemy is elected to shoot a bullet. fn check_enemy_bullet_shoot(&mut self, time: f32) { + // if less enemies are present in the game, the delay between shots should be increased. + // otherwise, the shots will be concentrated on very few enemies, making it hard to avoid + // the bullets while aiming to kill enemies. let delay_factor = 2.0 - self.enemies.len() as f32 / (config::ENEMY_COLS * config::ENEMY_ROWS) as f32; + + // check if enough time has passed since the last shot if time - self.enemy_last_shoot_time >= (config::BULLET_DELAY * delay_factor) { + // choose a random enemy let enemy = self.enemies.choose(&mut rand::thread_rng()); if let Some(enemy) = enemy { + // shoot the bullet self.enemy_bullets.push(Bullet { - pos_x: enemy.pos_x, + pos_x: enemy.pos_x, // shoot from current enemy position pos_y: enemy.pos_y, - direction: -1.0, - color: [1.0, 1.0, 1.0], + direction: -1.0, // downwards + color: [1.0, 1.0, 1.0], // white }); } + + // update last shoot time self.enemy_last_shoot_time = time; } + // remove out-of-bounds bullets self.enemy_bullets.retain(|x| !x.out_of_bounds()); } + /// check if the player bullets are colliding with enemies. if they are, kill the enemies. fn check_player_bullet_collisions(&mut self) { + // list of bullets and enemies to remove / kill let mut bullet_remove = vec![]; let mut enemy_remove = vec![]; + for (bullet_idx, bullet) in self.player_bullets.iter().enumerate() { 'inner: for (enemy_idx, enemy) in self.enemies.iter().enumerate() { + // check if the bullet is colliding with an enemy let is_colliding = bullet.is_colliding_with_shape( enemy.pos_x, enemy.pos_y, @@ -198,6 +270,7 @@ impl Game { config::ENEMY_HEIGHT * config::ENEMY_PIXEL_SIZE, ); + // if it is colliding, add enemy and bullet to the removal list if is_colliding { enemy_remove.push(enemy_idx); bullet_remove.push(bullet_idx); @@ -206,22 +279,27 @@ impl Game { } } + // remove bullets that hit an enemy for i in bullet_remove { self.player_bullets.remove(i); } + // remove enemies that were hit by a bullet for i in enemy_remove { self.enemies.remove(i); } + // if the last enemy was shot, display the "you won!" screen and end the game if self.enemies.is_empty() { js_sys::eval("window.game_won()").unwrap(); self.game_running = false; } } + /// check if enemy bullets are colliding with the player fn check_enemy_bullet_collisions(&mut self) { for bullet in &self.enemy_bullets { + // check if the bullet is colliding with the player let is_colliding = bullet.is_colliding_with_shape( self.player.pos_x, config::PLAYER_OFFSET_BOTTOM, @@ -229,6 +307,7 @@ impl Game { config::PLAYER_HEIGHT, ); + // if it is colliding, display the "you lost!" screen and end the game if is_colliding { js_sys::eval("window.game_lost()").unwrap(); self.game_running = false; @@ -237,12 +316,18 @@ impl Game { } } + /// check if enemies are too low. when the enemies have moved down low enough so that the + /// player could touch them, the player will lose the game. fn check_enemy_y(&mut self) { + // calculate the y position at which the enemies are considered to be too close to the + // player (i.e. too low) const ENEMY_MIN_Y: f32 = config::PLAYER_OFFSET_BOTTOM + config::PLAYER_HEIGHT / 2.0 + config::ENEMY_HEIGHT * config::ENEMY_PIXEL_SIZE; + // check every enemy's y position if it is too low for enemy in &self.enemies { + // if an enemy is too low, display the "you lost!" screen and end the game if enemy.pos_y < ENEMY_MIN_Y { js_sys::eval("window.game_lost()").unwrap(); self.game_running = false; @@ -251,21 +336,27 @@ impl Game { } } + /// draw the game. fn draw(&mut self) { + // obtain the vertices of all game objects let (vertices, indices) = self.get_vertices(); + // update the vertex and index buffers of the renderer self.renderer .update_buffer(&vertices, &indices, false) .unwrap(); + // ask the renderer to draw the scene self.renderer.draw().unwrap(); } + /// obtain the vertices and indices of all game objects. returns a tuple `(vertices, indices)`. fn get_vertices(&self) -> (Vec, Vec) { let mut vertices = vec![]; let mut indices = vec![]; let mut i: u16 = 0; + // helper function that adds vertices and indices to the array let mut add_vertices = |data: (Vec, Vec, u16)| { let (mut vert, ind, num_vert) = data; vertices.append(&mut vert); @@ -273,16 +364,20 @@ impl Game { i += num_vert; }; + // add player vertices add_vertices(self.player.get_vertices()); + // add vertices for each enemy for enemy in &self.enemies { add_vertices(enemy.get_vertices(self.last_time)); } + // add vertices for each player bullet for bullet in &self.player_bullets { add_vertices(bullet.get_vertices()); } + // add vertices for each enemy bullet for bullet in &self.enemy_bullets { add_vertices(bullet.get_vertices()); } @@ -290,8 +385,10 @@ impl Game { (vertices, indices) } + /// function that is executed when a key has been pressed pub fn on_keydown(&mut self, event: KeyboardEvent) { let key = event.key(); + if key == "a" || key == "h" || key == "ArrowLeft" { self.player.is_moving_left = true; } else if key == "d" || key == "l" || key == "ArrowRight" { @@ -301,8 +398,10 @@ impl Game { } } + /// function that is executed when a key has been released pub fn on_keyup(&mut self, event: KeyboardEvent) { let key = event.key(); + if key == "a" || key == "h" || key == "ArrowLeft" { self.player.is_moving_left = false; } else if key == "d" || key == "l" || key == "ArrowRight" { @@ -314,43 +413,41 @@ impl Game { } impl Enemy { + /// get the vertices of an enemy. returns a tuple consisting of vertices, indices and the + /// number of vertices (since the vertices themselves are stored as a flat array, making + /// determining the number of vertices impossible without knowing their exact layout). fn get_vertices(&self, time: f32) -> (Vec, Vec, u16) { let mut vertices = vec![]; + // the position of an enemy is stored as the center point of the rectangle. in order to get + // to the corners of the rectangle, half of the width or height needs to be added or + // subtracted from the center coordinates. const ENEMY_WIDTH_HALF: f32 = (config::ENEMY_WIDTH * config::ENEMY_PIXEL_SIZE) / 2.0; const ENEMY_HEIGHT_HALF: f32 = (config::ENEMY_HEIGHT * config::ENEMY_PIXEL_SIZE) / 2.0; + + // since there are multiple sprites for enemies that are stored below each other, calculate + // the offset [`to`] required to get to a certain sprite. the texture offset consists of a + // time offset (since the enemies are animated) and a sprite offset (since there are + // different sprites to choose from). let to_time = (((time % 2000.0) / 1000.0) as i32) as f32 * 0.125; let to_sprite = self.sprite_type as f32 * 0.25; let to = to_time + to_sprite; + // coordinates of the edges of the rectangle let left = self.pos_x - ENEMY_WIDTH_HALF; let right = self.pos_x + ENEMY_WIDTH_HALF; let top = self.pos_y - ENEMY_HEIGHT_HALF; let bottom = self.pos_y + ENEMY_HEIGHT_HALF; - vertices.append(&mut vec![left, top, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0625 + to]); - vertices.append(&mut vec![left, bottom, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0 + to]); - vertices.append(&mut vec![ - right, - bottom, - 0.0, - 1.0, - 1.0, - 1.0, - 0.09375, - 0.0 + to, - ]); - vertices.append(&mut vec![ - right, - top, - 0.0, - 1.0, - 1.0, - 1.0, - 0.09375, - 0.0625 + to, - ]); + // append the vertices of the rectangle corners. since rustfmt likes to split these arrays + // into multiple lines (which dirsupts the reading flow), use an attribute to turn it of + // for each line separately. + #[rustfmt::skip] vertices.append(&mut vec![left, top, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0625 + to]); + #[rustfmt::skip] vertices.append(&mut vec![left, bottom, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0 + to]); + #[rustfmt::skip] vertices.append(&mut vec![right, bottom, 0.0, 1.0, 1.0, 1.0, 0.09375, 0.0 + to]); + #[rustfmt::skip] vertices.append(&mut vec![right, top, 0.0, 1.0, 1.0, 1.0, 0.09375, 0.0625 + to]); + // the indices of the triangles that make up the rectangle let indices = vec![0, 1, 2, 0, 2, 3]; (vertices, indices, 4) @@ -358,30 +455,37 @@ impl Enemy { } impl Player { + /// get the vertices of the player. returns a tuple consisting of vertices, indices and the + /// number of vertices (since the vertices themselves are stored as a flat array, making + /// determining the number of vertices impossible without knowing their exact layout). fn get_vertices(&self) -> (Vec, Vec, u16) { let mut vertices = vec![]; + // coordinates of the edges of the rectangle let left = self.pos_x - config::PLAYER_WIDTH / 2.0; let right = self.pos_x + config::PLAYER_WIDTH / 2.0; - let bottom = config::PLAYER_OFFSET_BOTTOM; - let top = bottom + config::PLAYER_HEIGHT; + let bot = config::PLAYER_OFFSET_BOTTOM; + let top = bot + config::PLAYER_HEIGHT; + // append the vertices of the rectangle corners vertices.append(&mut vec![left, top, 0.0, 1.0, 1.0, 1.0, 0.125, 0.0]); - vertices.append(&mut vec![left, bottom, 0.0, 1.0, 1.0, 1.0, 0.125, 0.0625]); - vertices.append(&mut vec![ - right, bottom, 0.0, 1.0, 1.0, 1.0, 0.21875, 0.0625, - ]); + vertices.append(&mut vec![left, bot, 0.0, 1.0, 1.0, 1.0, 0.125, 0.0625]); + vertices.append(&mut vec![right, bot, 0.0, 1.0, 1.0, 1.0, 0.21875, 0.0625]); vertices.append(&mut vec![right, top, 0.0, 1.0, 1.0, 1.0, 0.21875, 0.0]); + // the indices of the triangles that make up the rectangle let indices = vec![0, 1, 2, 0, 2, 3]; (vertices, indices, 4) } + /// update the player position fn update(&mut self, delta: f32) { + // move the player left or right self.pos_x += (self.is_moving_right as i32 - self.is_moving_left as i32) as f32 * config::PLAYER_SPEED * delta; + // prevent the player from going outside the range that it is allowed to move in self.pos_x = self .pos_x .clamp(-config::PLAYER_CLAMP, config::PLAYER_CLAMP); @@ -389,54 +493,75 @@ impl Player { } impl Bullet { + /// get the vertices of a bullet. returns a tuple consisting of vertices, indices and the + /// number of vertices (since the vertices themselves are stored as a flat array, making + /// determining the number of vertices impossible without knowing their exact layout). fn get_vertices(&self) -> (Vec, Vec, u16) { let mut vertices = vec![]; + // coordinates of the edges of the rectangle let left = self.pos_x - config::BULLET_WIDTH / 2.0; let right = self.pos_x + config::BULLET_WIDTH / 2.0; let bottom = self.pos_y - config::BULLET_HEIGHT / 2.0; let top = self.pos_y + config::BULLET_HEIGHT / 2.0; + // get the color of the bullet let (r, g, b) = (self.color[0], self.color[1], self.color[2]); + // append the vertices of the rectangle corners vertices.append(&mut vec![left, top, 0.0, r, g, b, 0.9375, 0.9375]); vertices.append(&mut vec![left, bottom, 0.0, r, g, b, 0.9375, 1.0]); vertices.append(&mut vec![right, bottom, 0.0, r, g, b, 1.0, 1.0]); vertices.append(&mut vec![right, top, 0.0, r, g, b, 1.0, 0.9375]); + // the indices of the triangles that make up the rectangle let indices = vec![0, 1, 2, 0, 2, 3]; (vertices, indices, 4) } + /// update the bullet position fn update(&mut self, delta: f32) { self.pos_y += self.direction * config::BULLET_SPEED * delta; } + /// check if the bullet is out of bounds (i.e. below or above the visible area) fn out_of_bounds(&self) -> bool { self.pos_y < -1.0 || self.pos_y > 1.0 } + /// check if the bullet is colliding with a rectangle. the position of the rectangle is + /// provided as a center point, width and height. the bullet is handled as if it were a + /// 1-dimensional line without any width, since that is good enough for this use case. fn is_colliding_with_shape(&self, pos_x: f32, pos_y: f32, width: f32, height: f32) -> bool { + // obtain the rectangle corners let left = pos_x - width / 2.0; let right = pos_x + width / 2.0; let bottom = pos_y - height / 2.0; let top = pos_y + height / 2.0; + // get top and bottom of bullet let bullet_top = self.pos_y + config::BULLET_HEIGHT / 2.0; let bullet_bottom = self.pos_y - config::BULLET_HEIGHT / 2.0; + // check if bullet is colliding on the x-axis let x_collision = left <= self.pos_x && self.pos_x <= right; + // check if the bottom of the bullet is inside the shape let y_collision_top = bottom <= bullet_bottom && bullet_bottom <= top; + // check if the top of the bullet is inside the shape let y_collision_bottom = bottom <= bullet_top && bullet_top <= top; + // check if the shape is "inside" the bullet let y_collision_through = bullet_bottom <= bottom && top <= bullet_top; x_collision && (y_collision_top || y_collision_bottom || y_collision_through) } } +/// the game struct as a mutable global variable. this is a rather sketchy solution, but it works +/// fine for this usecase since multiple closures require mutable access to the game state. static mut GAME: Option = None; +/// initialize the game. fn game_init() -> Result<(), JsValue> { unsafe { GAME = Some(Game::new()?); @@ -444,29 +569,37 @@ fn game_init() -> Result<(), JsValue> { Ok(()) } +/// obtain a mutable reference to the game. fn game_mut() -> &'static mut Game { return unsafe { GAME.as_mut().unwrap() }; } +/// the main loop for the game fn game_animation_loop() { let f = Rc::new(RefCell::new(None)); let g = f.clone(); *g.borrow_mut() = Some(Closure::wrap(Box::new(move |time: f32| { let game = game_mut(); - game.update(time); + // if the game is running, update the game and request another animation frame if game.game_running { + game.update(time); utils::request_animation_frame(f.borrow().as_ref().unwrap()); } }) as Box)); + // call `window.requestAnimationFrame()` for the closure specified above utils::request_animation_frame(g.borrow().as_ref().unwrap()); } -fn game_keydown_loop() -> Result<(), JsValue> { +/// register keydown event handler +fn register_keydown_handler() -> Result<(), JsValue> { + // define a closure that gets called when a key is pressed let c = Closure::wrap(Box::new(move |e| { game_mut().on_keydown(e); }) as Box); + + // add the event listener let document = window().unwrap().document().unwrap(); document.add_event_listener_with_callback("keydown", c.as_ref().unchecked_ref())?; c.forget(); @@ -474,10 +607,14 @@ fn game_keydown_loop() -> Result<(), JsValue> { Ok(()) } -fn game_keyup_loop() -> Result<(), JsValue> { +/// register keyup event handler +fn register_keyup_handler() -> Result<(), JsValue> { + // define a closure that gets called when a key is released let c = Closure::wrap(Box::new(move |e| { game_mut().on_keyup(e); }) as Box); + + // add the event listener let document = window().unwrap().document().unwrap(); document.add_event_listener_with_callback("keyup", c.as_ref().unchecked_ref())?; c.forget(); @@ -485,12 +622,13 @@ fn game_keyup_loop() -> Result<(), JsValue> { Ok(()) } +/// run the game. pub fn run() -> Result<(), JsValue> { game_init()?; + register_keydown_handler()?; + register_keyup_handler()?; game_animation_loop(); - game_keydown_loop()?; - game_keyup_loop()?; Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index eeff8c6..a597263 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,16 +5,19 @@ mod game; mod render; mod utils; +// use wee_alloc as the global allocator if the feature is enabled #[cfg(feature = "wee_alloc")] #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; #[wasm_bindgen] pub fn wasm_main() { + // set panic hook when console_error_panic_hook feature is enabled #[cfg(feature = "console_error_panic_hook")] utils::set_panic_hook(); } +/// starts the game. gets called when the "play game" button is pressed. #[wasm_bindgen] pub fn game_start() { match game::run() { diff --git a/src/render.rs b/src/render.rs index 84825dc..cc4b139 100644 --- a/src/render.rs +++ b/src/render.rs @@ -9,28 +9,39 @@ use web_sys::{ use crate::utils; +/// stores the rendering context and data required for rendering the scene pub struct Renderer { + // the webgl2 rendering context obtained from the canvas gl: WebGl2RenderingContext, + canvas: HtmlCanvasElement, + // rendering data: buffers, shader program and texture. stored as `Option`s since they still + // need to be set by the user after initializing the renderer. buffer: Option, shader_program: Option, texture: Option>, } +/// store vertex and index buffer pub struct Buffer { + // vertex and index buffer vertex_buffer: WebGlBuffer, index_buffer: WebGlBuffer, + // number of elements inside the index buffer index_len: usize, } impl Renderer { + /// create a new renderer for the canvas with the id `canvas_id`. pub fn new(canvas_id: &str) -> Result { let document = window().unwrap().document().unwrap(); + // obtain the canvas element let canvas = document .get_element_by_id(canvas_id) .ok_or("failed to get canvas element")? .dyn_into::()?; + // obtain a webgl2 graphics context let gl = canvas .get_context("webgl2")? .ok_or("failed to get graphics context")? @@ -38,13 +49,16 @@ impl Renderer { Ok(Renderer { gl, + canvas, buffer: None, shader_program: None, texture: None, }) } + /// draw the scene. pub fn draw(&self) -> Result<(), JsValue> { + // create shorthands for the rendering context and rendering data let gl = &self.gl; let buffer = self.buffer.as_ref().ok_or("no buffer is specified")?; let shader_program = self @@ -56,14 +70,19 @@ impl Renderer { .as_ref() .map(|x| unsafe { Rc::downgrade(x).as_ptr().as_ref() }.unwrap()); + // set the background color of the canvas to black gl.clear_color(0.0, 0.0, 0.0, 1.0); gl.clear(Gl::COLOR_BUFFER_BIT); + // apply the shader program gl.use_program(Some(shader_program)); + // bind the vertex and array buffer to ARRAY_BUFFER and ELEMENT_ARRAY_BUFFER, respectively, + // so that opengl knows which data we want to access gl.bind_buffer(Gl::ARRAY_BUFFER, Some(&buffer.vertex_buffer)); gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, Some(&buffer.index_buffer)); + // bind the texture gl.active_texture(Gl::TEXTURE0); gl.bind_texture(Gl::TEXTURE_2D, texture); let sampler_location = gl @@ -71,6 +90,15 @@ impl Renderer { .ok_or("failed to get uniform location")?; gl.uniform1i(Some(&sampler_location), 0); + // set the viewport to the width and height of the canvas + gl.viewport( + 0, + 0, + self.canvas.width() as i32, + self.canvas.height() as i32, + ); + + // draw the vertices as triangles gl.draw_elements_with_i32( Gl::TRIANGLES, buffer.index_len as i32, @@ -78,6 +106,7 @@ impl Renderer { 0, ); + // unbind buffers and textures gl.bind_buffer(Gl::ARRAY_BUFFER, None); gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, None); gl.bind_texture(Gl::TEXTURE_2D, None); @@ -85,12 +114,14 @@ impl Renderer { Ok(()) } - pub fn set_buffer( + /// initialize the buffer contents + pub fn initialize_buffer( &mut self, vertices: &Vec, indices: &Vec, static_draw: bool, ) -> Result<(), JsValue> { + // create vertex and index buffers based on the arrays from the function parameters let vertex_buffer = self.create_vertex_buffer(vertices, static_draw)?; let index_buffer = self.create_index_buffer(indices, static_draw)?; @@ -103,6 +134,7 @@ impl Renderer { Ok(()) } + /// create a vertex buffer fn create_vertex_buffer( &self, buffer_data: &Vec, @@ -110,19 +142,24 @@ impl Renderer { ) -> Result { let gl = &self.gl; + // create a javascript `Float32Array` to store the data in let vertices_array = { let memory_buffer = wasm_bindgen::memory().dyn_into::()?.buffer(); let loc = buffer_data.as_ptr() as u32 / 4; Float32Array::new(&memory_buffer).subarray(loc, loc + buffer_data.len() as u32) }; + // specify the draw mode. `STATIC_DRAW` specifies that the buffer data is set only once and + // used many times. `DYNAMIC_DRAW` is used when the data is changed frequently. let draw_mode = match static_draw { true => Gl::STATIC_DRAW, false => Gl::DYNAMIC_DRAW, }; + // create the buffer let vertex_buffer = gl.create_buffer().ok_or("failed to create buffer")?; + // bind the buffer to `ARRAY_BUFFER` and write the data to it gl.bind_buffer(Gl::ARRAY_BUFFER, Some(&vertex_buffer)); gl.buffer_data_with_array_buffer_view(Gl::ARRAY_BUFFER, &vertices_array, draw_mode); gl.bind_buffer(Gl::ARRAY_BUFFER, None); @@ -130,6 +167,7 @@ impl Renderer { Ok(vertex_buffer) } + /// create an index buffer fn create_index_buffer( &self, buffer_data: &Vec, @@ -137,19 +175,24 @@ impl Renderer { ) -> Result { let gl = &self.gl; + // create a javascript `Uint16Array` to store the data in let indices_array = { let memory_buffer = wasm_bindgen::memory().dyn_into::()?.buffer(); let loc = buffer_data.as_ptr() as u32 / 2; Uint16Array::new(&memory_buffer).subarray(loc, loc + buffer_data.len() as u32) }; + // specify the draw mode. `STATIC_DRAW` specifies that the buffer data is set only once and + // used many times. `DYNAMIC_DRAW` is used when the data is changed frequently. let draw_mode = match static_draw { true => Gl::STATIC_DRAW, false => Gl::DYNAMIC_DRAW, }; + // create the buffer let index_buffer = gl.create_buffer().ok_or("failed to create buffer")?; + // bind the buffer to `ELEMENT_ARRAY_BUFFER` and write the data to it gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, Some(&index_buffer)); gl.buffer_data_with_array_buffer_view(Gl::ELEMENT_ARRAY_BUFFER, &indices_array, draw_mode); gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, None); @@ -157,6 +200,7 @@ impl Renderer { Ok(index_buffer) } + /// update the buffer contents pub fn update_buffer( &mut self, vertices: &Vec, @@ -166,36 +210,44 @@ impl Renderer { let gl = &self.gl; let buffer = self.buffer.as_mut().ok_or("no buffer is specified")?; + // create a javascript `Float32Array` to store the vertex array data in let vertices_array = { let memory_buffer = wasm_bindgen::memory().dyn_into::()?.buffer(); let loc = vertices.as_ptr() as u32 / 4; Float32Array::new(&memory_buffer).subarray(loc, loc + vertices.len() as u32) }; + // create a javascript `Uint16Array` to store the index array data in let indices_array = { let memory_buffer = wasm_bindgen::memory().dyn_into::()?.buffer(); let loc = indices.as_ptr() as u32 / 2; Uint16Array::new(&memory_buffer).subarray(loc, loc + indices.len() as u32) }; + // specify the draw mode. `STATIC_DRAW` specifies that the buffer data is set only once and + // used many times. `DYNAMIC_DRAW` is used when the data is changed frequently. let draw_mode = match static_draw { true => Gl::STATIC_DRAW, false => Gl::DYNAMIC_DRAW, }; + // bind the vertex buffer to `ARRAY_BUFFER` and write the data into it gl.bind_buffer(Gl::ARRAY_BUFFER, Some(&buffer.vertex_buffer)); gl.buffer_data_with_array_buffer_view(Gl::ARRAY_BUFFER, &vertices_array, draw_mode); gl.bind_buffer(Gl::ARRAY_BUFFER, None); + // bind the index buffer to `ELEMENT_ARRAY_BUFFER` and write the data into it gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, Some(&buffer.index_buffer)); gl.buffer_data_with_array_buffer_view(Gl::ELEMENT_ARRAY_BUFFER, &indices_array, draw_mode); gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, None); + // obtain the length of the index array buffer.index_len = indices.len(); Ok(()) } + /// initialize the shader program pub fn set_shader_program( &mut self, vertex_src: &str, @@ -203,26 +255,36 @@ impl Renderer { ) -> Result<(), JsValue> { let gl = &self.gl; + // compile vertex and fragment shader let vertex_shader = self.compile_shader(Gl::VERTEX_SHADER, vertex_src)?; let fragment_shader = self.compile_shader(Gl::FRAGMENT_SHADER, fragment_src)?; + + // link the shader program let shader_program = self.link_shader_program(&vertex_shader, &fragment_shader)?; + + // delete the shaders since we no longer need them after linking the program gl.delete_shader(Some(&vertex_shader)); gl.delete_shader(Some(&fragment_shader)); + self.shader_program = Some(shader_program); Ok(()) } + /// compile an opengl shader fn compile_shader(&self, shader_type: u32, source: &str) -> Result { let gl = &self.gl; + // create the shader let shader = gl .create_shader(shader_type) .ok_or("unable to create shader object")?; + // add the shader source code and compile it gl.shader_source(&shader, source); gl.compile_shader(&shader); + // check if the compilation was successful if gl .get_shader_parameter(&shader, Gl::COMPILE_STATUS) .as_bool() @@ -236,6 +298,7 @@ impl Renderer { } } + /// link a shader program consisting of a vertex and a fragment shader fn link_shader_program( &self, vert_shader: &WebGlShader, @@ -243,14 +306,17 @@ impl Renderer { ) -> Result { let gl = &self.gl; + // create the program let program = gl .create_program() .ok_or("unable to create shader program")?; + // attach the shaders and link them together gl.attach_shader(&program, vert_shader); gl.attach_shader(&program, frag_shader); gl.link_program(&program); + // check if the linking was successful if gl .get_program_parameter(&program, Gl::LINK_STATUS) .as_bool() @@ -264,6 +330,7 @@ impl Renderer { } } + /// map a shader attribute to a position in the vertex data pub fn map_shader_attribute( &self, attribute_name: &str, @@ -271,6 +338,7 @@ impl Renderer { stride: i32, offset: i32, ) -> Result<(), JsValue> { + // define shorthands for rendering context and rendering data let gl = &self.gl; let buffer = self.buffer.as_ref().ok_or("no buffer is specified")?; let shader_program = self @@ -278,10 +346,15 @@ impl Renderer { .as_ref() .ok_or("no shader program is specified")?; + // bind the buffers we want to map gl.bind_buffer(Gl::ARRAY_BUFFER, Some(&buffer.vertex_buffer)); gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, Some(&buffer.index_buffer)); + // get the location of the attribute within the shader let attribute = gl.get_attrib_location(shader_program, attribute_name) as u32; + + // map the attribute to the vertex buffer data, specifying the size, stride and offset of + // the data elements. gl.vertex_attrib_pointer_with_i32( attribute, size, @@ -290,14 +363,18 @@ impl Renderer { stride * size_of::() as i32, offset * size_of::() as i32, ); + + // enable the attribute since it is not enabled by default gl.enable_vertex_attrib_array(attribute); + // unbind the buffers gl.bind_buffer(Gl::ARRAY_BUFFER, None); gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, None); Ok(()) } + /// initialize the texture pub fn set_texture(&mut self, path: &str) -> Result<(), JsValue> { let texture = self.load_texture(path)?; self.texture = Some(texture); @@ -305,12 +382,16 @@ impl Renderer { Ok(()) } + /// load the texture from the internet fn load_texture(&self, path: &str) -> Result, JsValue> { let gl = &self.gl; + // create and bind the texture let texture = gl.create_texture().ok_or("failed to create texture")?; gl.bind_texture(Gl::TEXTURE_2D, Some(&texture)); + // temporarily create a black rectangle since we need to wait for the texture to be + // transferred over the internet let pixel: [u8; 4] = [0, 0, 0, 255]; gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( Gl::TEXTURE_2D, @@ -324,6 +405,7 @@ impl Renderer { Some(&pixel), )?; + // create an `` element let img = HtmlImageElement::new().unwrap(); img.set_cross_origin(Some("")); let imgrc = Rc::new(img); @@ -334,8 +416,12 @@ impl Renderer { let img = imgrc.clone(); let texture = texture.clone(); let gl = Rc::new(gl.clone()); + let a = Closure::wrap(Box::new(move || { + // bind the texture gl.bind_texture(Gl::TEXTURE_2D, Some(&texture)); + + // load the data of the `` element into the texture if let Err(e) = gl.tex_image_2d_with_u32_and_u32_and_html_image_element( Gl::TEXTURE_2D, 0, @@ -346,18 +432,30 @@ impl Renderer { ) { utils::alert_err(e); }; + + // set texture properties: texture filtering (to make the textures appear + // pixelated) and wrapping (in order to fix a bug) gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MIN_FILTER, Gl::NEAREST as i32); gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MAG_FILTER, Gl::NEAREST as i32); gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_S, Gl::CLAMP_TO_EDGE as i32); gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_T, Gl::CLAMP_TO_EDGE as i32); + + // generate mipmaps for the texture gl.generate_mipmap(Gl::TEXTURE_2D); + + // unbind the texture + gl.bind_texture(Gl::TEXTURE_2D, None); }) as Box); + + // once the `` loads, execute the closure that loads the image into the texture imgrc.set_onload(Some(a.as_ref().unchecked_ref())); a.forget(); } + // set the source of the image imgrc.set_src(path); + // unbind the texture gl.bind_texture(Gl::TEXTURE_2D, None); Ok(texture) diff --git a/src/utils.rs b/src/utils.rs index a29db01..5a40558 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,17 +1,20 @@ use wasm_bindgen::prelude::*; use web_sys::window; +/// set the console error panic hook if the respecive feature is enabled pub fn set_panic_hook() { #[cfg(feature = "console_error_panic_hook")] console_error_panic_hook::set_once(); } +/// display an alert with an error message passed as a JsValue. pub fn alert_err(msg: JsValue) { let mut s = String::from("ERROR! --- "); s.push_str(&msg.as_string().unwrap_or_else(|| String::from("unknown"))); window().unwrap().alert_with_message(&s).unwrap(); } +/// helper function for using javascript's `window.requestAnimationFrame()` pub fn request_animation_frame(f: &Closure) { window() .unwrap()