doc: add comments
This commit is contained in:
parent
340bdd625c
commit
4f7b17a2b6
|
@ -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_WIDTH_PX: f32 = 12.0;
|
||||||
const ENEMY_HEIGHT_PX: f32 = 8.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_ROWS: usize = 3;
|
||||||
pub const ENEMY_COLS: usize = 6;
|
pub const ENEMY_COLS: usize = 6;
|
||||||
|
// space in between enemies
|
||||||
pub const ENEMY_DISTANCE: f32 = 12.0;
|
pub const ENEMY_DISTANCE: f32 = 12.0;
|
||||||
|
// size of each pixel of the enemy texture
|
||||||
pub const ENEMY_PIXEL_SIZE: f32 = 0.01;
|
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_WIDTH: f32 = ENEMY_WIDTH_PX * SCREEN_ASPECT_RATIO;
|
||||||
pub const ENEMY_HEIGHT: f32 = ENEMY_HEIGHT_PX;
|
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: f32 = 0.2;
|
||||||
pub const ENEMY_SPEED_VERT: f32 = 0.03;
|
pub const ENEMY_SPEED_VERT: f32 = 0.03;
|
||||||
|
// maximum horizontal enemy offset
|
||||||
pub const ENEMY_CLAMP: f32 = 0.25;
|
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_COL_SKIP: f32 = (ENEMY_WIDTH + ENEMY_DISTANCE) * ENEMY_PIXEL_SIZE;
|
||||||
pub const ENEMY_ROW_SKIP: f32 = (ENEMY_HEIGHT + 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_TOP: f32 = 0.7;
|
||||||
pub const ENEMY_OFFSET_LEFT: f32 = ENEMY_PIXEL_SIZE
|
pub const ENEMY_OFFSET_LEFT: f32 = ENEMY_PIXEL_SIZE
|
||||||
* -((ENEMY_COLS as f32 / 2.0) * (ENEMY_WIDTH + ENEMY_DISTANCE) - ENEMY_DISTANCE);
|
* -((ENEMY_COLS as f32 / 2.0) * (ENEMY_WIDTH + ENEMY_DISTANCE) - ENEMY_DISTANCE);
|
||||||
|
|
||||||
|
// speed of the player
|
||||||
pub const PLAYER_SPEED: f32 = 0.8;
|
pub const PLAYER_SPEED: f32 = 0.8;
|
||||||
|
// maximum / minimum player x-coordinate
|
||||||
pub const PLAYER_CLAMP: f32 = 0.8;
|
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;
|
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;
|
pub const PLAYER_OFFSET_BOTTOM: f32 = -0.8;
|
||||||
|
|
||||||
|
// width and height of bullets
|
||||||
pub const BULLET_WIDTH: f32 = 0.01;
|
pub const BULLET_WIDTH: f32 = 0.01;
|
||||||
pub const BULLET_HEIGHT: f32 = 0.05;
|
pub const BULLET_HEIGHT: f32 = 0.05;
|
||||||
|
// bullet speed
|
||||||
pub const BULLET_SPEED: f32 = 1.5;
|
pub const BULLET_SPEED: f32 = 1.5;
|
||||||
|
// time between shots
|
||||||
pub const BULLET_DELAY: f32 = 500.0;
|
pub const BULLET_DELAY: f32 = 500.0;
|
||||||
|
|
256
src/game.rs
256
src/game.rs
|
@ -6,65 +6,108 @@ use web_sys::{window, KeyboardEvent};
|
||||||
|
|
||||||
use crate::{config, render::Renderer, utils};
|
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 VERT_SRC: &str = include_str!("shader/vert.glsl");
|
||||||
const FRAG_SRC: &str = include_str!("shader/frag.glsl");
|
const FRAG_SRC: &str = include_str!("shader/frag.glsl");
|
||||||
|
|
||||||
|
/// stores the current player state.
|
||||||
struct Player {
|
struct Player {
|
||||||
|
// x position
|
||||||
pos_x: f32,
|
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_left: bool,
|
||||||
is_moving_right: bool,
|
is_moving_right: bool,
|
||||||
is_shooting: 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,
|
last_shoot_time: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// stores position of an enemy.
|
||||||
struct Enemy {
|
struct Enemy {
|
||||||
|
// x and y position
|
||||||
pos_x: f32,
|
pos_x: f32,
|
||||||
pos_y: f32,
|
pos_y: f32,
|
||||||
|
// the texture to display. expects a value from 0 to 2.
|
||||||
sprite_type: u8,
|
sprite_type: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// stores the position of a bullet.
|
||||||
struct Bullet {
|
struct Bullet {
|
||||||
|
// x and y position
|
||||||
pos_x: f32,
|
pos_x: f32,
|
||||||
pos_y: f32,
|
pos_y: f32,
|
||||||
|
// direction that the bullet is moving in
|
||||||
direction: f32,
|
direction: f32,
|
||||||
|
// color of the bullet
|
||||||
color: [f32; 3],
|
color: [f32; 3],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// stores the current state of the game and the game objects.
|
||||||
pub struct Game {
|
pub struct Game {
|
||||||
|
// renderer object - see `render.rs`
|
||||||
renderer: Renderer,
|
renderer: Renderer,
|
||||||
|
// player, enemies and bullets
|
||||||
player: Player,
|
player: Player,
|
||||||
enemies: Vec<Enemy>,
|
enemies: Vec<Enemy>,
|
||||||
enemy_direction: f32,
|
|
||||||
enemy_offset: f32,
|
|
||||||
player_bullets: Vec<Bullet>,
|
player_bullets: Vec<Bullet>,
|
||||||
enemy_bullets: Vec<Bullet>,
|
enemy_bullets: Vec<Bullet>,
|
||||||
|
// 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,
|
enemy_last_shoot_time: f32,
|
||||||
|
// time of the last frame, used for calculating delta time
|
||||||
last_time: f32,
|
last_time: f32,
|
||||||
|
// is the game running?
|
||||||
game_running: bool,
|
game_running: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Game {
|
impl Game {
|
||||||
|
/// initialize the renderer and set up the game
|
||||||
pub fn new() -> Result<Self, JsValue> {
|
pub fn new() -> Result<Self, JsValue> {
|
||||||
|
// obtain a renderer for the canvas of id `canvas`
|
||||||
let mut renderer = Renderer::new("canvas")?;
|
let mut renderer = Renderer::new("canvas")?;
|
||||||
|
|
||||||
let vertices = vec![];
|
let vertices = vec![];
|
||||||
let indices = 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)?;
|
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("aCoord", 3, 8, 0)?;
|
||||||
renderer.map_shader_attribute("aColor", 3, 8, 3)?;
|
renderer.map_shader_attribute("aColor", 3, 8, 3)?;
|
||||||
renderer.map_shader_attribute("aTexCoord", 2, 8, 6)?;
|
renderer.map_shader_attribute("aTexCoord", 2, 8, 6)?;
|
||||||
|
|
||||||
|
// load the texture file
|
||||||
renderer.set_texture("/texture.png")?;
|
renderer.set_texture("/texture.png")?;
|
||||||
|
|
||||||
|
// initialize the player
|
||||||
let player = Player {
|
let player = Player {
|
||||||
pos_x: 0.0,
|
pos_x: 0.0,
|
||||||
is_moving_left: false,
|
is_moving_left: false,
|
||||||
is_moving_right: false,
|
is_moving_right: false,
|
||||||
is_shooting: 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 enemies = {
|
||||||
let mut v = vec![];
|
let mut v = vec![];
|
||||||
let mut pos_x = config::ENEMY_OFFSET_LEFT;
|
let mut pos_x = config::ENEMY_OFFSET_LEFT;
|
||||||
|
@ -75,7 +118,7 @@ impl Game {
|
||||||
v.push(Enemy {
|
v.push(Enemy {
|
||||||
pos_x,
|
pos_x,
|
||||||
pos_y,
|
pos_y,
|
||||||
sprite_type: row as u8,
|
sprite_type: row as u8, // give each row a different sprite
|
||||||
});
|
});
|
||||||
pos_y -= config::ENEMY_ROW_SKIP;
|
pos_y -= config::ENEMY_ROW_SKIP;
|
||||||
}
|
}
|
||||||
|
@ -86,48 +129,64 @@ impl Game {
|
||||||
v
|
v
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// finally, construct the game struct
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
renderer,
|
renderer,
|
||||||
player,
|
player,
|
||||||
enemies,
|
enemies,
|
||||||
enemy_direction: config::ENEMY_SPEED,
|
|
||||||
enemy_offset: 0.0,
|
|
||||||
last_time: window().unwrap().performance().unwrap().now() as f32,
|
|
||||||
player_bullets: vec![],
|
player_bullets: vec![],
|
||||||
enemy_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,
|
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) {
|
fn update(&mut self, time: f32) {
|
||||||
|
// obtain delta time in seconds
|
||||||
let delta = (time - self.last_time) / 1000.0;
|
let delta = (time - self.last_time) / 1000.0;
|
||||||
self.last_time = time;
|
self.last_time = time;
|
||||||
|
|
||||||
|
// update player position
|
||||||
self.player.update(delta);
|
self.player.update(delta);
|
||||||
|
|
||||||
|
// update bulletes fired by player
|
||||||
self.player_bullets
|
self.player_bullets
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.for_each(|bullet| bullet.update(delta));
|
.for_each(|bullet| bullet.update(delta));
|
||||||
|
|
||||||
|
// update bulletes fired by enemies
|
||||||
self.enemy_bullets
|
self.enemy_bullets
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.for_each(|bullet| bullet.update(delta));
|
.for_each(|bullet| bullet.update(delta));
|
||||||
|
|
||||||
|
// update enemy positions
|
||||||
self.update_enemies(delta);
|
self.update_enemies(delta);
|
||||||
|
|
||||||
|
// allow player and enemies to fire bullets if they can
|
||||||
self.check_player_bullet_shoot(time);
|
self.check_player_bullet_shoot(time);
|
||||||
self.check_enemy_bullet_shoot(time);
|
self.check_enemy_bullet_shoot(time);
|
||||||
|
|
||||||
|
// check if bullets are colliding with anything
|
||||||
self.check_player_bullet_collisions();
|
self.check_player_bullet_collisions();
|
||||||
self.check_enemy_bullet_collisions();
|
self.check_enemy_bullet_collisions();
|
||||||
|
|
||||||
|
// check if the enemies got too low
|
||||||
self.check_enemy_y();
|
self.check_enemy_y();
|
||||||
|
|
||||||
|
// draw the game
|
||||||
self.draw();
|
self.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// update the position of the enemies
|
||||||
fn update_enemies(&mut self, delta: f32) {
|
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 {
|
if self.enemy_offset.abs() >= config::ENEMY_CLAMP {
|
||||||
self.enemy_direction = -self.enemy_direction;
|
self.enemy_direction = -self.enemy_direction;
|
||||||
self.enemy_offset = self
|
self.enemy_offset = self
|
||||||
|
@ -135,6 +194,7 @@ impl Game {
|
||||||
.clamp(-config::ENEMY_CLAMP, config::ENEMY_CLAMP);
|
.clamp(-config::ENEMY_CLAMP, config::ENEMY_CLAMP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// move the enemies
|
||||||
let step = self.enemy_direction * delta;
|
let step = self.enemy_direction * delta;
|
||||||
self.enemy_offset += step;
|
self.enemy_offset += step;
|
||||||
for enemy in self.enemies.iter_mut() {
|
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) {
|
fn check_player_bullet_shoot(&mut self, time: f32) {
|
||||||
if self.player.is_shooting && time - self.player.last_shoot_time >= config::BULLET_DELAY {
|
if self.player.is_shooting && time - self.player.last_shoot_time >= config::BULLET_DELAY {
|
||||||
|
// shoot the bullet
|
||||||
self.player_bullets.push(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,
|
pos_y: config::PLAYER_OFFSET_BOTTOM + 0.1,
|
||||||
direction: 1.0,
|
direction: 1.0, // upwards
|
||||||
color: [0.0, 1.0, 0.0],
|
color: [0.0, 1.0, 0.0], // green
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// update shoot time
|
||||||
self.player.last_shoot_time = time;
|
self.player.last_shoot_time = time;
|
||||||
}
|
}
|
||||||
|
|
||||||
let player_bullets_to_remove = self
|
// remove out-of-bounds bullets
|
||||||
.player_bullets
|
self.player_bullets.retain(|x| !x.out_of_bounds());
|
||||||
.iter()
|
|
||||||
.filter(|x| x.out_of_bounds())
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, _)| i)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
for i in player_bullets_to_remove {
|
|
||||||
self.player_bullets.remove(i);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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) {
|
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 =
|
let delay_factor =
|
||||||
2.0 - self.enemies.len() as f32 / (config::ENEMY_COLS * config::ENEMY_ROWS) as f32;
|
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) {
|
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());
|
let enemy = self.enemies.choose(&mut rand::thread_rng());
|
||||||
if let Some(enemy) = enemy {
|
if let Some(enemy) = enemy {
|
||||||
|
// shoot the bullet
|
||||||
self.enemy_bullets.push(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,
|
pos_y: enemy.pos_y,
|
||||||
direction: -1.0,
|
direction: -1.0, // downwards
|
||||||
color: [1.0, 1.0, 1.0],
|
color: [1.0, 1.0, 1.0], // white
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update last shoot time
|
||||||
self.enemy_last_shoot_time = time;
|
self.enemy_last_shoot_time = time;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove out-of-bounds bullets
|
||||||
self.enemy_bullets.retain(|x| !x.out_of_bounds());
|
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) {
|
fn check_player_bullet_collisions(&mut self) {
|
||||||
|
// list of bullets and enemies to remove / kill
|
||||||
let mut bullet_remove = vec![];
|
let mut bullet_remove = vec![];
|
||||||
let mut enemy_remove = vec![];
|
let mut enemy_remove = vec![];
|
||||||
|
|
||||||
for (bullet_idx, bullet) in self.player_bullets.iter().enumerate() {
|
for (bullet_idx, bullet) in self.player_bullets.iter().enumerate() {
|
||||||
'inner: for (enemy_idx, enemy) in self.enemies.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(
|
let is_colliding = bullet.is_colliding_with_shape(
|
||||||
enemy.pos_x,
|
enemy.pos_x,
|
||||||
enemy.pos_y,
|
enemy.pos_y,
|
||||||
|
@ -198,6 +270,7 @@ impl Game {
|
||||||
config::ENEMY_HEIGHT * config::ENEMY_PIXEL_SIZE,
|
config::ENEMY_HEIGHT * config::ENEMY_PIXEL_SIZE,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// if it is colliding, add enemy and bullet to the removal list
|
||||||
if is_colliding {
|
if is_colliding {
|
||||||
enemy_remove.push(enemy_idx);
|
enemy_remove.push(enemy_idx);
|
||||||
bullet_remove.push(bullet_idx);
|
bullet_remove.push(bullet_idx);
|
||||||
|
@ -206,22 +279,27 @@ impl Game {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove bullets that hit an enemy
|
||||||
for i in bullet_remove {
|
for i in bullet_remove {
|
||||||
self.player_bullets.remove(i);
|
self.player_bullets.remove(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove enemies that were hit by a bullet
|
||||||
for i in enemy_remove {
|
for i in enemy_remove {
|
||||||
self.enemies.remove(i);
|
self.enemies.remove(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the last enemy was shot, display the "you won!" screen and end the game
|
||||||
if self.enemies.is_empty() {
|
if self.enemies.is_empty() {
|
||||||
js_sys::eval("window.game_won()").unwrap();
|
js_sys::eval("window.game_won()").unwrap();
|
||||||
self.game_running = false;
|
self.game_running = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// check if enemy bullets are colliding with the player
|
||||||
fn check_enemy_bullet_collisions(&mut self) {
|
fn check_enemy_bullet_collisions(&mut self) {
|
||||||
for bullet in &self.enemy_bullets {
|
for bullet in &self.enemy_bullets {
|
||||||
|
// check if the bullet is colliding with the player
|
||||||
let is_colliding = bullet.is_colliding_with_shape(
|
let is_colliding = bullet.is_colliding_with_shape(
|
||||||
self.player.pos_x,
|
self.player.pos_x,
|
||||||
config::PLAYER_OFFSET_BOTTOM,
|
config::PLAYER_OFFSET_BOTTOM,
|
||||||
|
@ -229,6 +307,7 @@ impl Game {
|
||||||
config::PLAYER_HEIGHT,
|
config::PLAYER_HEIGHT,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// if it is colliding, display the "you lost!" screen and end the game
|
||||||
if is_colliding {
|
if is_colliding {
|
||||||
js_sys::eval("window.game_lost()").unwrap();
|
js_sys::eval("window.game_lost()").unwrap();
|
||||||
self.game_running = false;
|
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) {
|
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
|
const ENEMY_MIN_Y: f32 = config::PLAYER_OFFSET_BOTTOM
|
||||||
+ config::PLAYER_HEIGHT / 2.0
|
+ config::PLAYER_HEIGHT / 2.0
|
||||||
+ config::ENEMY_HEIGHT * config::ENEMY_PIXEL_SIZE;
|
+ config::ENEMY_HEIGHT * config::ENEMY_PIXEL_SIZE;
|
||||||
|
|
||||||
|
// check every enemy's y position if it is too low
|
||||||
for enemy in &self.enemies {
|
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 {
|
if enemy.pos_y < ENEMY_MIN_Y {
|
||||||
js_sys::eval("window.game_lost()").unwrap();
|
js_sys::eval("window.game_lost()").unwrap();
|
||||||
self.game_running = false;
|
self.game_running = false;
|
||||||
|
@ -251,21 +336,27 @@ impl Game {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// draw the game.
|
||||||
fn draw(&mut self) {
|
fn draw(&mut self) {
|
||||||
|
// obtain the vertices of all game objects
|
||||||
let (vertices, indices) = self.get_vertices();
|
let (vertices, indices) = self.get_vertices();
|
||||||
|
|
||||||
|
// update the vertex and index buffers of the renderer
|
||||||
self.renderer
|
self.renderer
|
||||||
.update_buffer(&vertices, &indices, false)
|
.update_buffer(&vertices, &indices, false)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
// ask the renderer to draw the scene
|
||||||
self.renderer.draw().unwrap();
|
self.renderer.draw().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// obtain the vertices and indices of all game objects. returns a tuple `(vertices, indices)`.
|
||||||
fn get_vertices(&self) -> (Vec<f32>, Vec<u16>) {
|
fn get_vertices(&self) -> (Vec<f32>, Vec<u16>) {
|
||||||
let mut vertices = vec![];
|
let mut vertices = vec![];
|
||||||
let mut indices = vec![];
|
let mut indices = vec![];
|
||||||
let mut i: u16 = 0;
|
let mut i: u16 = 0;
|
||||||
|
|
||||||
|
// helper function that adds vertices and indices to the array
|
||||||
let mut add_vertices = |data: (Vec<f32>, Vec<u16>, u16)| {
|
let mut add_vertices = |data: (Vec<f32>, Vec<u16>, u16)| {
|
||||||
let (mut vert, ind, num_vert) = data;
|
let (mut vert, ind, num_vert) = data;
|
||||||
vertices.append(&mut vert);
|
vertices.append(&mut vert);
|
||||||
|
@ -273,16 +364,20 @@ impl Game {
|
||||||
i += num_vert;
|
i += num_vert;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// add player vertices
|
||||||
add_vertices(self.player.get_vertices());
|
add_vertices(self.player.get_vertices());
|
||||||
|
|
||||||
|
// add vertices for each enemy
|
||||||
for enemy in &self.enemies {
|
for enemy in &self.enemies {
|
||||||
add_vertices(enemy.get_vertices(self.last_time));
|
add_vertices(enemy.get_vertices(self.last_time));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add vertices for each player bullet
|
||||||
for bullet in &self.player_bullets {
|
for bullet in &self.player_bullets {
|
||||||
add_vertices(bullet.get_vertices());
|
add_vertices(bullet.get_vertices());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add vertices for each enemy bullet
|
||||||
for bullet in &self.enemy_bullets {
|
for bullet in &self.enemy_bullets {
|
||||||
add_vertices(bullet.get_vertices());
|
add_vertices(bullet.get_vertices());
|
||||||
}
|
}
|
||||||
|
@ -290,8 +385,10 @@ impl Game {
|
||||||
(vertices, indices)
|
(vertices, indices)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// function that is executed when a key has been pressed
|
||||||
pub fn on_keydown(&mut self, event: KeyboardEvent) {
|
pub fn on_keydown(&mut self, event: KeyboardEvent) {
|
||||||
let key = event.key();
|
let key = event.key();
|
||||||
|
|
||||||
if key == "a" || key == "h" || key == "ArrowLeft" {
|
if key == "a" || key == "h" || key == "ArrowLeft" {
|
||||||
self.player.is_moving_left = true;
|
self.player.is_moving_left = true;
|
||||||
} else if key == "d" || key == "l" || key == "ArrowRight" {
|
} 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) {
|
pub fn on_keyup(&mut self, event: KeyboardEvent) {
|
||||||
let key = event.key();
|
let key = event.key();
|
||||||
|
|
||||||
if key == "a" || key == "h" || key == "ArrowLeft" {
|
if key == "a" || key == "h" || key == "ArrowLeft" {
|
||||||
self.player.is_moving_left = false;
|
self.player.is_moving_left = false;
|
||||||
} else if key == "d" || key == "l" || key == "ArrowRight" {
|
} else if key == "d" || key == "l" || key == "ArrowRight" {
|
||||||
|
@ -314,43 +413,41 @@ impl Game {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Enemy {
|
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<f32>, Vec<u16>, u16) {
|
fn get_vertices(&self, time: f32) -> (Vec<f32>, Vec<u16>, u16) {
|
||||||
let mut vertices = vec![];
|
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_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;
|
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_time = (((time % 2000.0) / 1000.0) as i32) as f32 * 0.125;
|
||||||
let to_sprite = self.sprite_type as f32 * 0.25;
|
let to_sprite = self.sprite_type as f32 * 0.25;
|
||||||
let to = to_time + to_sprite;
|
let to = to_time + to_sprite;
|
||||||
|
|
||||||
|
// coordinates of the edges of the rectangle
|
||||||
let left = self.pos_x - ENEMY_WIDTH_HALF;
|
let left = self.pos_x - ENEMY_WIDTH_HALF;
|
||||||
let right = self.pos_x + ENEMY_WIDTH_HALF;
|
let right = self.pos_x + ENEMY_WIDTH_HALF;
|
||||||
let top = self.pos_y - ENEMY_HEIGHT_HALF;
|
let top = self.pos_y - ENEMY_HEIGHT_HALF;
|
||||||
let bottom = 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]);
|
// append the vertices of the rectangle corners. since rustfmt likes to split these arrays
|
||||||
vertices.append(&mut vec![left, bottom, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0 + to]);
|
// into multiple lines (which dirsupts the reading flow), use an attribute to turn it of
|
||||||
vertices.append(&mut vec![
|
// for each line separately.
|
||||||
right,
|
#[rustfmt::skip] vertices.append(&mut vec![left, top, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0625 + to]);
|
||||||
bottom,
|
#[rustfmt::skip] vertices.append(&mut vec![left, bottom, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0 + to]);
|
||||||
0.0,
|
#[rustfmt::skip] vertices.append(&mut vec![right, bottom, 0.0, 1.0, 1.0, 1.0, 0.09375, 0.0 + to]);
|
||||||
1.0,
|
#[rustfmt::skip] vertices.append(&mut vec![right, top, 0.0, 1.0, 1.0, 1.0, 0.09375, 0.0625 + to]);
|
||||||
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,
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
// the indices of the triangles that make up the rectangle
|
||||||
let indices = vec![0, 1, 2, 0, 2, 3];
|
let indices = vec![0, 1, 2, 0, 2, 3];
|
||||||
|
|
||||||
(vertices, indices, 4)
|
(vertices, indices, 4)
|
||||||
|
@ -358,30 +455,37 @@ impl Enemy {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Player {
|
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<f32>, Vec<u16>, u16) {
|
fn get_vertices(&self) -> (Vec<f32>, Vec<u16>, u16) {
|
||||||
let mut vertices = vec![];
|
let mut vertices = vec![];
|
||||||
|
|
||||||
|
// coordinates of the edges of the rectangle
|
||||||
let left = self.pos_x - config::PLAYER_WIDTH / 2.0;
|
let left = self.pos_x - config::PLAYER_WIDTH / 2.0;
|
||||||
let right = 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 bot = config::PLAYER_OFFSET_BOTTOM;
|
||||||
let top = bottom + config::PLAYER_HEIGHT;
|
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, 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![left, bot, 0.0, 1.0, 1.0, 1.0, 0.125, 0.0625]);
|
||||||
vertices.append(&mut vec![
|
vertices.append(&mut vec![right, bot, 0.0, 1.0, 1.0, 1.0, 0.21875, 0.0625]);
|
||||||
right, bottom, 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]);
|
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];
|
let indices = vec![0, 1, 2, 0, 2, 3];
|
||||||
|
|
||||||
(vertices, indices, 4)
|
(vertices, indices, 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// update the player position
|
||||||
fn update(&mut self, delta: f32) {
|
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
|
self.pos_x += (self.is_moving_right as i32 - self.is_moving_left as i32) as f32
|
||||||
* config::PLAYER_SPEED
|
* config::PLAYER_SPEED
|
||||||
* delta;
|
* delta;
|
||||||
|
// prevent the player from going outside the range that it is allowed to move in
|
||||||
self.pos_x = self
|
self.pos_x = self
|
||||||
.pos_x
|
.pos_x
|
||||||
.clamp(-config::PLAYER_CLAMP, config::PLAYER_CLAMP);
|
.clamp(-config::PLAYER_CLAMP, config::PLAYER_CLAMP);
|
||||||
|
@ -389,54 +493,75 @@ impl Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Bullet {
|
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<f32>, Vec<u16>, u16) {
|
fn get_vertices(&self) -> (Vec<f32>, Vec<u16>, u16) {
|
||||||
let mut vertices = vec![];
|
let mut vertices = vec![];
|
||||||
|
|
||||||
|
// coordinates of the edges of the rectangle
|
||||||
let left = self.pos_x - config::BULLET_WIDTH / 2.0;
|
let left = self.pos_x - config::BULLET_WIDTH / 2.0;
|
||||||
let right = 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 bottom = self.pos_y - config::BULLET_HEIGHT / 2.0;
|
||||||
let top = 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]);
|
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, 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![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, 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]);
|
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];
|
let indices = vec![0, 1, 2, 0, 2, 3];
|
||||||
|
|
||||||
(vertices, indices, 4)
|
(vertices, indices, 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// update the bullet position
|
||||||
fn update(&mut self, delta: f32) {
|
fn update(&mut self, delta: f32) {
|
||||||
self.pos_y += self.direction * config::BULLET_SPEED * delta;
|
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 {
|
fn out_of_bounds(&self) -> bool {
|
||||||
self.pos_y < -1.0 || self.pos_y > 1.0
|
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 {
|
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 left = pos_x - width / 2.0;
|
||||||
let right = pos_x + width / 2.0;
|
let right = pos_x + width / 2.0;
|
||||||
let bottom = pos_y - height / 2.0;
|
let bottom = pos_y - height / 2.0;
|
||||||
let top = 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_top = self.pos_y + config::BULLET_HEIGHT / 2.0;
|
||||||
let bullet_bottom = 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;
|
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;
|
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;
|
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;
|
let y_collision_through = bullet_bottom <= bottom && top <= bullet_top;
|
||||||
|
|
||||||
x_collision && (y_collision_top || y_collision_bottom || y_collision_through)
|
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<Game> = None;
|
static mut GAME: Option<Game> = None;
|
||||||
|
|
||||||
|
/// initialize the game.
|
||||||
fn game_init() -> Result<(), JsValue> {
|
fn game_init() -> Result<(), JsValue> {
|
||||||
unsafe {
|
unsafe {
|
||||||
GAME = Some(Game::new()?);
|
GAME = Some(Game::new()?);
|
||||||
|
@ -444,29 +569,37 @@ fn game_init() -> Result<(), JsValue> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// obtain a mutable reference to the game.
|
||||||
fn game_mut() -> &'static mut Game {
|
fn game_mut() -> &'static mut Game {
|
||||||
return unsafe { GAME.as_mut().unwrap() };
|
return unsafe { GAME.as_mut().unwrap() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// the main loop for the game
|
||||||
fn game_animation_loop() {
|
fn game_animation_loop() {
|
||||||
let f = Rc::new(RefCell::new(None));
|
let f = Rc::new(RefCell::new(None));
|
||||||
let g = f.clone();
|
let g = f.clone();
|
||||||
|
|
||||||
*g.borrow_mut() = Some(Closure::wrap(Box::new(move |time: f32| {
|
*g.borrow_mut() = Some(Closure::wrap(Box::new(move |time: f32| {
|
||||||
let game = game_mut();
|
let game = game_mut();
|
||||||
game.update(time);
|
// if the game is running, update the game and request another animation frame
|
||||||
if game.game_running {
|
if game.game_running {
|
||||||
|
game.update(time);
|
||||||
utils::request_animation_frame(f.borrow().as_ref().unwrap());
|
utils::request_animation_frame(f.borrow().as_ref().unwrap());
|
||||||
}
|
}
|
||||||
}) as Box<dyn FnMut(f32)>));
|
}) as Box<dyn FnMut(f32)>));
|
||||||
|
|
||||||
|
// call `window.requestAnimationFrame()` for the closure specified above
|
||||||
utils::request_animation_frame(g.borrow().as_ref().unwrap());
|
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| {
|
let c = Closure::wrap(Box::new(move |e| {
|
||||||
game_mut().on_keydown(e);
|
game_mut().on_keydown(e);
|
||||||
}) as Box<dyn FnMut(KeyboardEvent)>);
|
}) as Box<dyn FnMut(KeyboardEvent)>);
|
||||||
|
|
||||||
|
// add the event listener
|
||||||
let document = window().unwrap().document().unwrap();
|
let document = window().unwrap().document().unwrap();
|
||||||
document.add_event_listener_with_callback("keydown", c.as_ref().unchecked_ref())?;
|
document.add_event_listener_with_callback("keydown", c.as_ref().unchecked_ref())?;
|
||||||
c.forget();
|
c.forget();
|
||||||
|
@ -474,10 +607,14 @@ fn game_keydown_loop() -> Result<(), JsValue> {
|
||||||
Ok(())
|
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| {
|
let c = Closure::wrap(Box::new(move |e| {
|
||||||
game_mut().on_keyup(e);
|
game_mut().on_keyup(e);
|
||||||
}) as Box<dyn FnMut(KeyboardEvent)>);
|
}) as Box<dyn FnMut(KeyboardEvent)>);
|
||||||
|
|
||||||
|
// add the event listener
|
||||||
let document = window().unwrap().document().unwrap();
|
let document = window().unwrap().document().unwrap();
|
||||||
document.add_event_listener_with_callback("keyup", c.as_ref().unchecked_ref())?;
|
document.add_event_listener_with_callback("keyup", c.as_ref().unchecked_ref())?;
|
||||||
c.forget();
|
c.forget();
|
||||||
|
@ -485,12 +622,13 @@ fn game_keyup_loop() -> Result<(), JsValue> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// run the game.
|
||||||
pub fn run() -> Result<(), JsValue> {
|
pub fn run() -> Result<(), JsValue> {
|
||||||
game_init()?;
|
game_init()?;
|
||||||
|
|
||||||
|
register_keydown_handler()?;
|
||||||
|
register_keyup_handler()?;
|
||||||
game_animation_loop();
|
game_animation_loop();
|
||||||
game_keydown_loop()?;
|
|
||||||
game_keyup_loop()?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,16 +5,19 @@ mod game;
|
||||||
mod render;
|
mod render;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
|
// use wee_alloc as the global allocator if the feature is enabled
|
||||||
#[cfg(feature = "wee_alloc")]
|
#[cfg(feature = "wee_alloc")]
|
||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn wasm_main() {
|
pub fn wasm_main() {
|
||||||
|
// set panic hook when console_error_panic_hook feature is enabled
|
||||||
#[cfg(feature = "console_error_panic_hook")]
|
#[cfg(feature = "console_error_panic_hook")]
|
||||||
utils::set_panic_hook();
|
utils::set_panic_hook();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// starts the game. gets called when the "play game" button is pressed.
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn game_start() {
|
pub fn game_start() {
|
||||||
match game::run() {
|
match game::run() {
|
||||||
|
|
100
src/render.rs
100
src/render.rs
|
@ -9,28 +9,39 @@ use web_sys::{
|
||||||
|
|
||||||
use crate::utils;
|
use crate::utils;
|
||||||
|
|
||||||
|
/// stores the rendering context and data required for rendering the scene
|
||||||
pub struct Renderer {
|
pub struct Renderer {
|
||||||
|
// the webgl2 rendering context obtained from the canvas
|
||||||
gl: WebGl2RenderingContext,
|
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<Buffer>,
|
buffer: Option<Buffer>,
|
||||||
shader_program: Option<WebGlProgram>,
|
shader_program: Option<WebGlProgram>,
|
||||||
texture: Option<Rc<WebGlTexture>>,
|
texture: Option<Rc<WebGlTexture>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// store vertex and index buffer
|
||||||
pub struct Buffer {
|
pub struct Buffer {
|
||||||
|
// vertex and index buffer
|
||||||
vertex_buffer: WebGlBuffer,
|
vertex_buffer: WebGlBuffer,
|
||||||
index_buffer: WebGlBuffer,
|
index_buffer: WebGlBuffer,
|
||||||
|
// number of elements inside the index buffer
|
||||||
index_len: usize,
|
index_len: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Renderer {
|
impl Renderer {
|
||||||
|
/// create a new renderer for the canvas with the id `canvas_id`.
|
||||||
pub fn new(canvas_id: &str) -> Result<Renderer, JsValue> {
|
pub fn new(canvas_id: &str) -> Result<Renderer, JsValue> {
|
||||||
let document = window().unwrap().document().unwrap();
|
let document = window().unwrap().document().unwrap();
|
||||||
|
|
||||||
|
// obtain the canvas element
|
||||||
let canvas = document
|
let canvas = document
|
||||||
.get_element_by_id(canvas_id)
|
.get_element_by_id(canvas_id)
|
||||||
.ok_or("failed to get canvas element")?
|
.ok_or("failed to get canvas element")?
|
||||||
.dyn_into::<HtmlCanvasElement>()?;
|
.dyn_into::<HtmlCanvasElement>()?;
|
||||||
|
|
||||||
|
// obtain a webgl2 graphics context
|
||||||
let gl = canvas
|
let gl = canvas
|
||||||
.get_context("webgl2")?
|
.get_context("webgl2")?
|
||||||
.ok_or("failed to get graphics context")?
|
.ok_or("failed to get graphics context")?
|
||||||
|
@ -38,13 +49,16 @@ impl Renderer {
|
||||||
|
|
||||||
Ok(Renderer {
|
Ok(Renderer {
|
||||||
gl,
|
gl,
|
||||||
|
canvas,
|
||||||
buffer: None,
|
buffer: None,
|
||||||
shader_program: None,
|
shader_program: None,
|
||||||
texture: None,
|
texture: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// draw the scene.
|
||||||
pub fn draw(&self) -> Result<(), JsValue> {
|
pub fn draw(&self) -> Result<(), JsValue> {
|
||||||
|
// create shorthands for the rendering context and rendering data
|
||||||
let gl = &self.gl;
|
let gl = &self.gl;
|
||||||
let buffer = self.buffer.as_ref().ok_or("no buffer is specified")?;
|
let buffer = self.buffer.as_ref().ok_or("no buffer is specified")?;
|
||||||
let shader_program = self
|
let shader_program = self
|
||||||
|
@ -56,14 +70,19 @@ impl Renderer {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|x| unsafe { Rc::downgrade(x).as_ptr().as_ref() }.unwrap());
|
.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_color(0.0, 0.0, 0.0, 1.0);
|
||||||
gl.clear(Gl::COLOR_BUFFER_BIT);
|
gl.clear(Gl::COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
// apply the shader program
|
||||||
gl.use_program(Some(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::ARRAY_BUFFER, Some(&buffer.vertex_buffer));
|
||||||
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, Some(&buffer.index_buffer));
|
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, Some(&buffer.index_buffer));
|
||||||
|
|
||||||
|
// bind the texture
|
||||||
gl.active_texture(Gl::TEXTURE0);
|
gl.active_texture(Gl::TEXTURE0);
|
||||||
gl.bind_texture(Gl::TEXTURE_2D, texture);
|
gl.bind_texture(Gl::TEXTURE_2D, texture);
|
||||||
let sampler_location = gl
|
let sampler_location = gl
|
||||||
|
@ -71,6 +90,15 @@ impl Renderer {
|
||||||
.ok_or("failed to get uniform location")?;
|
.ok_or("failed to get uniform location")?;
|
||||||
gl.uniform1i(Some(&sampler_location), 0);
|
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.draw_elements_with_i32(
|
||||||
Gl::TRIANGLES,
|
Gl::TRIANGLES,
|
||||||
buffer.index_len as i32,
|
buffer.index_len as i32,
|
||||||
|
@ -78,6 +106,7 @@ impl Renderer {
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// unbind buffers and textures
|
||||||
gl.bind_buffer(Gl::ARRAY_BUFFER, None);
|
gl.bind_buffer(Gl::ARRAY_BUFFER, None);
|
||||||
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, None);
|
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, None);
|
||||||
gl.bind_texture(Gl::TEXTURE_2D, None);
|
gl.bind_texture(Gl::TEXTURE_2D, None);
|
||||||
|
@ -85,12 +114,14 @@ impl Renderer {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_buffer(
|
/// initialize the buffer contents
|
||||||
|
pub fn initialize_buffer(
|
||||||
&mut self,
|
&mut self,
|
||||||
vertices: &Vec<f32>,
|
vertices: &Vec<f32>,
|
||||||
indices: &Vec<u16>,
|
indices: &Vec<u16>,
|
||||||
static_draw: bool,
|
static_draw: bool,
|
||||||
) -> Result<(), JsValue> {
|
) -> 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 vertex_buffer = self.create_vertex_buffer(vertices, static_draw)?;
|
||||||
let index_buffer = self.create_index_buffer(indices, static_draw)?;
|
let index_buffer = self.create_index_buffer(indices, static_draw)?;
|
||||||
|
|
||||||
|
@ -103,6 +134,7 @@ impl Renderer {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// create a vertex buffer
|
||||||
fn create_vertex_buffer(
|
fn create_vertex_buffer(
|
||||||
&self,
|
&self,
|
||||||
buffer_data: &Vec<f32>,
|
buffer_data: &Vec<f32>,
|
||||||
|
@ -110,19 +142,24 @@ impl Renderer {
|
||||||
) -> Result<WebGlBuffer, JsValue> {
|
) -> Result<WebGlBuffer, JsValue> {
|
||||||
let gl = &self.gl;
|
let gl = &self.gl;
|
||||||
|
|
||||||
|
// create a javascript `Float32Array` to store the data in
|
||||||
let vertices_array = {
|
let vertices_array = {
|
||||||
let memory_buffer = wasm_bindgen::memory().dyn_into::<Memory>()?.buffer();
|
let memory_buffer = wasm_bindgen::memory().dyn_into::<Memory>()?.buffer();
|
||||||
let loc = buffer_data.as_ptr() as u32 / 4;
|
let loc = buffer_data.as_ptr() as u32 / 4;
|
||||||
Float32Array::new(&memory_buffer).subarray(loc, loc + buffer_data.len() as u32)
|
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 {
|
let draw_mode = match static_draw {
|
||||||
true => Gl::STATIC_DRAW,
|
true => Gl::STATIC_DRAW,
|
||||||
false => Gl::DYNAMIC_DRAW,
|
false => Gl::DYNAMIC_DRAW,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// create the buffer
|
||||||
let vertex_buffer = gl.create_buffer().ok_or("failed to create 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.bind_buffer(Gl::ARRAY_BUFFER, Some(&vertex_buffer));
|
||||||
gl.buffer_data_with_array_buffer_view(Gl::ARRAY_BUFFER, &vertices_array, draw_mode);
|
gl.buffer_data_with_array_buffer_view(Gl::ARRAY_BUFFER, &vertices_array, draw_mode);
|
||||||
gl.bind_buffer(Gl::ARRAY_BUFFER, None);
|
gl.bind_buffer(Gl::ARRAY_BUFFER, None);
|
||||||
|
@ -130,6 +167,7 @@ impl Renderer {
|
||||||
Ok(vertex_buffer)
|
Ok(vertex_buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// create an index buffer
|
||||||
fn create_index_buffer(
|
fn create_index_buffer(
|
||||||
&self,
|
&self,
|
||||||
buffer_data: &Vec<u16>,
|
buffer_data: &Vec<u16>,
|
||||||
|
@ -137,19 +175,24 @@ impl Renderer {
|
||||||
) -> Result<WebGlBuffer, JsValue> {
|
) -> Result<WebGlBuffer, JsValue> {
|
||||||
let gl = &self.gl;
|
let gl = &self.gl;
|
||||||
|
|
||||||
|
// create a javascript `Uint16Array` to store the data in
|
||||||
let indices_array = {
|
let indices_array = {
|
||||||
let memory_buffer = wasm_bindgen::memory().dyn_into::<Memory>()?.buffer();
|
let memory_buffer = wasm_bindgen::memory().dyn_into::<Memory>()?.buffer();
|
||||||
let loc = buffer_data.as_ptr() as u32 / 2;
|
let loc = buffer_data.as_ptr() as u32 / 2;
|
||||||
Uint16Array::new(&memory_buffer).subarray(loc, loc + buffer_data.len() as u32)
|
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 {
|
let draw_mode = match static_draw {
|
||||||
true => Gl::STATIC_DRAW,
|
true => Gl::STATIC_DRAW,
|
||||||
false => Gl::DYNAMIC_DRAW,
|
false => Gl::DYNAMIC_DRAW,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// create the buffer
|
||||||
let index_buffer = gl.create_buffer().ok_or("failed to create 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.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.buffer_data_with_array_buffer_view(Gl::ELEMENT_ARRAY_BUFFER, &indices_array, draw_mode);
|
||||||
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, None);
|
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, None);
|
||||||
|
@ -157,6 +200,7 @@ impl Renderer {
|
||||||
Ok(index_buffer)
|
Ok(index_buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// update the buffer contents
|
||||||
pub fn update_buffer(
|
pub fn update_buffer(
|
||||||
&mut self,
|
&mut self,
|
||||||
vertices: &Vec<f32>,
|
vertices: &Vec<f32>,
|
||||||
|
@ -166,36 +210,44 @@ impl Renderer {
|
||||||
let gl = &self.gl;
|
let gl = &self.gl;
|
||||||
let buffer = self.buffer.as_mut().ok_or("no buffer is specified")?;
|
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 vertices_array = {
|
||||||
let memory_buffer = wasm_bindgen::memory().dyn_into::<Memory>()?.buffer();
|
let memory_buffer = wasm_bindgen::memory().dyn_into::<Memory>()?.buffer();
|
||||||
let loc = vertices.as_ptr() as u32 / 4;
|
let loc = vertices.as_ptr() as u32 / 4;
|
||||||
Float32Array::new(&memory_buffer).subarray(loc, loc + vertices.len() as u32)
|
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 indices_array = {
|
||||||
let memory_buffer = wasm_bindgen::memory().dyn_into::<Memory>()?.buffer();
|
let memory_buffer = wasm_bindgen::memory().dyn_into::<Memory>()?.buffer();
|
||||||
let loc = indices.as_ptr() as u32 / 2;
|
let loc = indices.as_ptr() as u32 / 2;
|
||||||
Uint16Array::new(&memory_buffer).subarray(loc, loc + indices.len() as u32)
|
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 {
|
let draw_mode = match static_draw {
|
||||||
true => Gl::STATIC_DRAW,
|
true => Gl::STATIC_DRAW,
|
||||||
false => Gl::DYNAMIC_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.bind_buffer(Gl::ARRAY_BUFFER, Some(&buffer.vertex_buffer));
|
||||||
gl.buffer_data_with_array_buffer_view(Gl::ARRAY_BUFFER, &vertices_array, draw_mode);
|
gl.buffer_data_with_array_buffer_view(Gl::ARRAY_BUFFER, &vertices_array, draw_mode);
|
||||||
gl.bind_buffer(Gl::ARRAY_BUFFER, None);
|
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.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.buffer_data_with_array_buffer_view(Gl::ELEMENT_ARRAY_BUFFER, &indices_array, draw_mode);
|
||||||
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, None);
|
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, None);
|
||||||
|
|
||||||
|
// obtain the length of the index array
|
||||||
buffer.index_len = indices.len();
|
buffer.index_len = indices.len();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// initialize the shader program
|
||||||
pub fn set_shader_program(
|
pub fn set_shader_program(
|
||||||
&mut self,
|
&mut self,
|
||||||
vertex_src: &str,
|
vertex_src: &str,
|
||||||
|
@ -203,26 +255,36 @@ impl Renderer {
|
||||||
) -> Result<(), JsValue> {
|
) -> Result<(), JsValue> {
|
||||||
let gl = &self.gl;
|
let gl = &self.gl;
|
||||||
|
|
||||||
|
// compile vertex and fragment shader
|
||||||
let vertex_shader = self.compile_shader(Gl::VERTEX_SHADER, vertex_src)?;
|
let vertex_shader = self.compile_shader(Gl::VERTEX_SHADER, vertex_src)?;
|
||||||
let fragment_shader = self.compile_shader(Gl::FRAGMENT_SHADER, fragment_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)?;
|
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(&vertex_shader));
|
||||||
gl.delete_shader(Some(&fragment_shader));
|
gl.delete_shader(Some(&fragment_shader));
|
||||||
|
|
||||||
self.shader_program = Some(shader_program);
|
self.shader_program = Some(shader_program);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// compile an opengl shader
|
||||||
fn compile_shader(&self, shader_type: u32, source: &str) -> Result<WebGlShader, String> {
|
fn compile_shader(&self, shader_type: u32, source: &str) -> Result<WebGlShader, String> {
|
||||||
let gl = &self.gl;
|
let gl = &self.gl;
|
||||||
|
|
||||||
|
// create the shader
|
||||||
let shader = gl
|
let shader = gl
|
||||||
.create_shader(shader_type)
|
.create_shader(shader_type)
|
||||||
.ok_or("unable to create shader object")?;
|
.ok_or("unable to create shader object")?;
|
||||||
|
|
||||||
|
// add the shader source code and compile it
|
||||||
gl.shader_source(&shader, source);
|
gl.shader_source(&shader, source);
|
||||||
gl.compile_shader(&shader);
|
gl.compile_shader(&shader);
|
||||||
|
|
||||||
|
// check if the compilation was successful
|
||||||
if gl
|
if gl
|
||||||
.get_shader_parameter(&shader, Gl::COMPILE_STATUS)
|
.get_shader_parameter(&shader, Gl::COMPILE_STATUS)
|
||||||
.as_bool()
|
.as_bool()
|
||||||
|
@ -236,6 +298,7 @@ impl Renderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// link a shader program consisting of a vertex and a fragment shader
|
||||||
fn link_shader_program(
|
fn link_shader_program(
|
||||||
&self,
|
&self,
|
||||||
vert_shader: &WebGlShader,
|
vert_shader: &WebGlShader,
|
||||||
|
@ -243,14 +306,17 @@ impl Renderer {
|
||||||
) -> Result<WebGlProgram, String> {
|
) -> Result<WebGlProgram, String> {
|
||||||
let gl = &self.gl;
|
let gl = &self.gl;
|
||||||
|
|
||||||
|
// create the program
|
||||||
let program = gl
|
let program = gl
|
||||||
.create_program()
|
.create_program()
|
||||||
.ok_or("unable to create shader 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, vert_shader);
|
||||||
gl.attach_shader(&program, frag_shader);
|
gl.attach_shader(&program, frag_shader);
|
||||||
gl.link_program(&program);
|
gl.link_program(&program);
|
||||||
|
|
||||||
|
// check if the linking was successful
|
||||||
if gl
|
if gl
|
||||||
.get_program_parameter(&program, Gl::LINK_STATUS)
|
.get_program_parameter(&program, Gl::LINK_STATUS)
|
||||||
.as_bool()
|
.as_bool()
|
||||||
|
@ -264,6 +330,7 @@ impl Renderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// map a shader attribute to a position in the vertex data
|
||||||
pub fn map_shader_attribute(
|
pub fn map_shader_attribute(
|
||||||
&self,
|
&self,
|
||||||
attribute_name: &str,
|
attribute_name: &str,
|
||||||
|
@ -271,6 +338,7 @@ impl Renderer {
|
||||||
stride: i32,
|
stride: i32,
|
||||||
offset: i32,
|
offset: i32,
|
||||||
) -> Result<(), JsValue> {
|
) -> Result<(), JsValue> {
|
||||||
|
// define shorthands for rendering context and rendering data
|
||||||
let gl = &self.gl;
|
let gl = &self.gl;
|
||||||
let buffer = self.buffer.as_ref().ok_or("no buffer is specified")?;
|
let buffer = self.buffer.as_ref().ok_or("no buffer is specified")?;
|
||||||
let shader_program = self
|
let shader_program = self
|
||||||
|
@ -278,10 +346,15 @@ impl Renderer {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or("no shader program is specified")?;
|
.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::ARRAY_BUFFER, Some(&buffer.vertex_buffer));
|
||||||
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, Some(&buffer.index_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;
|
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(
|
gl.vertex_attrib_pointer_with_i32(
|
||||||
attribute,
|
attribute,
|
||||||
size,
|
size,
|
||||||
|
@ -290,14 +363,18 @@ impl Renderer {
|
||||||
stride * size_of::<f32>() as i32,
|
stride * size_of::<f32>() as i32,
|
||||||
offset * size_of::<f32>() as i32,
|
offset * size_of::<f32>() as i32,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// enable the attribute since it is not enabled by default
|
||||||
gl.enable_vertex_attrib_array(attribute);
|
gl.enable_vertex_attrib_array(attribute);
|
||||||
|
|
||||||
|
// unbind the buffers
|
||||||
gl.bind_buffer(Gl::ARRAY_BUFFER, None);
|
gl.bind_buffer(Gl::ARRAY_BUFFER, None);
|
||||||
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, None);
|
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, None);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// initialize the texture
|
||||||
pub fn set_texture(&mut self, path: &str) -> Result<(), JsValue> {
|
pub fn set_texture(&mut self, path: &str) -> Result<(), JsValue> {
|
||||||
let texture = self.load_texture(path)?;
|
let texture = self.load_texture(path)?;
|
||||||
self.texture = Some(texture);
|
self.texture = Some(texture);
|
||||||
|
@ -305,12 +382,16 @@ impl Renderer {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// load the texture from the internet
|
||||||
fn load_texture(&self, path: &str) -> Result<Rc<WebGlTexture>, JsValue> {
|
fn load_texture(&self, path: &str) -> Result<Rc<WebGlTexture>, JsValue> {
|
||||||
let gl = &self.gl;
|
let gl = &self.gl;
|
||||||
|
|
||||||
|
// create and bind the texture
|
||||||
let texture = gl.create_texture().ok_or("failed to create texture")?;
|
let texture = gl.create_texture().ok_or("failed to create texture")?;
|
||||||
gl.bind_texture(Gl::TEXTURE_2D, Some(&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];
|
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.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
|
||||||
Gl::TEXTURE_2D,
|
Gl::TEXTURE_2D,
|
||||||
|
@ -324,6 +405,7 @@ impl Renderer {
|
||||||
Some(&pixel),
|
Some(&pixel),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// create an `<img>` element
|
||||||
let img = HtmlImageElement::new().unwrap();
|
let img = HtmlImageElement::new().unwrap();
|
||||||
img.set_cross_origin(Some(""));
|
img.set_cross_origin(Some(""));
|
||||||
let imgrc = Rc::new(img);
|
let imgrc = Rc::new(img);
|
||||||
|
@ -334,8 +416,12 @@ impl Renderer {
|
||||||
let img = imgrc.clone();
|
let img = imgrc.clone();
|
||||||
let texture = texture.clone();
|
let texture = texture.clone();
|
||||||
let gl = Rc::new(gl.clone());
|
let gl = Rc::new(gl.clone());
|
||||||
|
|
||||||
let a = Closure::wrap(Box::new(move || {
|
let a = Closure::wrap(Box::new(move || {
|
||||||
|
// bind the texture
|
||||||
gl.bind_texture(Gl::TEXTURE_2D, Some(&texture));
|
gl.bind_texture(Gl::TEXTURE_2D, Some(&texture));
|
||||||
|
|
||||||
|
// load the data of the `<img>` element into the texture
|
||||||
if let Err(e) = gl.tex_image_2d_with_u32_and_u32_and_html_image_element(
|
if let Err(e) = gl.tex_image_2d_with_u32_and_u32_and_html_image_element(
|
||||||
Gl::TEXTURE_2D,
|
Gl::TEXTURE_2D,
|
||||||
0,
|
0,
|
||||||
|
@ -346,18 +432,30 @@ impl Renderer {
|
||||||
) {
|
) {
|
||||||
utils::alert_err(e);
|
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_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_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_S, Gl::CLAMP_TO_EDGE as i32);
|
||||||
gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_T, 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);
|
gl.generate_mipmap(Gl::TEXTURE_2D);
|
||||||
|
|
||||||
|
// unbind the texture
|
||||||
|
gl.bind_texture(Gl::TEXTURE_2D, None);
|
||||||
}) as Box<dyn FnMut()>);
|
}) as Box<dyn FnMut()>);
|
||||||
|
|
||||||
|
// once the `<img>` loads, execute the closure that loads the image into the texture
|
||||||
imgrc.set_onload(Some(a.as_ref().unchecked_ref()));
|
imgrc.set_onload(Some(a.as_ref().unchecked_ref()));
|
||||||
a.forget();
|
a.forget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set the source of the image
|
||||||
imgrc.set_src(path);
|
imgrc.set_src(path);
|
||||||
|
|
||||||
|
// unbind the texture
|
||||||
gl.bind_texture(Gl::TEXTURE_2D, None);
|
gl.bind_texture(Gl::TEXTURE_2D, None);
|
||||||
|
|
||||||
Ok(texture)
|
Ok(texture)
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use web_sys::window;
|
use web_sys::window;
|
||||||
|
|
||||||
|
/// set the console error panic hook if the respecive feature is enabled
|
||||||
pub fn set_panic_hook() {
|
pub fn set_panic_hook() {
|
||||||
#[cfg(feature = "console_error_panic_hook")]
|
#[cfg(feature = "console_error_panic_hook")]
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// display an alert with an error message passed as a JsValue.
|
||||||
pub fn alert_err(msg: JsValue) {
|
pub fn alert_err(msg: JsValue) {
|
||||||
let mut s = String::from("ERROR! --- ");
|
let mut s = String::from("ERROR! --- ");
|
||||||
s.push_str(&msg.as_string().unwrap_or_else(|| String::from("unknown")));
|
s.push_str(&msg.as_string().unwrap_or_else(|| String::from("unknown")));
|
||||||
window().unwrap().alert_with_message(&s).unwrap();
|
window().unwrap().alert_with_message(&s).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// helper function for using javascript's `window.requestAnimationFrame()`
|
||||||
pub fn request_animation_frame(f: &Closure<dyn FnMut(f32)>) {
|
pub fn request_animation_frame(f: &Closure<dyn FnMut(f32)>) {
|
||||||
window()
|
window()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|
Loading…
Reference in New Issue