use ggez::audio;
use ggez::audio::SoundSource;
use ggez::conf;
use ggez::event::{self, EventHandler, KeyCode, KeyMods};
use ggez::graphics::{self, Color};
use ggez::timer;
use ggez::{Context, ContextBuilder, GameResult};
use glam::*;
use oorandom::Rand32;
use std::env;
use std::path;
type Point2 = Vec2;
type Vector2 = Vec2;
fn vec_from_angle(angle: f32) -> Vector2 {
let vx = angle.sin();
let vy = angle.cos();
Vector2::new(vx, vy)
}
fn random_vec(rng: &mut Rand32, max_magnitude: f32) -> Vector2 {
let angle = rng.rand_float() * 2.0 * std::f32::consts::PI;
let mag = rng.rand_float() * max_magnitude;
vec_from_angle(angle) * (mag)
}
#[derive(Debug)]
enum ActorType {
Player,
Rock,
Shot,
}
#[derive(Debug)]
struct Actor {
tag: ActorType,
pos: Point2,
facing: f32,
velocity: Vector2,
ang_vel: f32,
bbox_size: f32,
life: f32,
}
const PLAYER_LIFE: f32 = 1.0;
const SHOT_LIFE: f32 = 2.0;
const ROCK_LIFE: f32 = 1.0;
const PLAYER_BBOX: f32 = 12.0;
const ROCK_BBOX: f32 = 12.0;
const SHOT_BBOX: f32 = 6.0;
const MAX_ROCK_VEL: f32 = 50.0;
fn create_player() -> Actor {
Actor {
tag: ActorType::Player,
pos: Point2::ZERO,
facing: 0.,
velocity: Vector2::ZERO,
ang_vel: 0.,
bbox_size: PLAYER_BBOX,
life: PLAYER_LIFE,
}
}
fn create_rock() -> Actor {
Actor {
tag: ActorType::Rock,
pos: Point2::ZERO,
facing: 0.,
velocity: Vector2::ZERO,
ang_vel: 0.,
bbox_size: ROCK_BBOX,
life: ROCK_LIFE,
}
}
fn create_shot() -> Actor {
Actor {
tag: ActorType::Shot,
pos: Point2::ZERO,
facing: 0.,
velocity: Vector2::ZERO,
ang_vel: SHOT_ANG_VEL,
bbox_size: SHOT_BBOX,
life: SHOT_LIFE,
}
}
fn create_rocks(
rng: &mut Rand32,
num: i32,
exclusion: Point2,
min_radius: f32,
max_radius: f32,
) -> Vec<Actor> {
assert!(max_radius > min_radius);
let new_rock = |_| {
let mut rock = create_rock();
let r_angle = rng.rand_float() * 2.0 * std::f32::consts::PI;
let r_distance = rng.rand_float() * (max_radius - min_radius) + min_radius;
rock.pos = exclusion + vec_from_angle(r_angle) * r_distance;
rock.velocity = random_vec(rng, MAX_ROCK_VEL);
rock
};
(0..num).map(new_rock).collect()
}
const SHOT_SPEED: f32 = 200.0;
const SHOT_ANG_VEL: f32 = 0.1;
const PLAYER_THRUST: f32 = 100.0;
const PLAYER_TURN_RATE: f32 = 3.0;
const PLAYER_SHOT_TIME: f32 = 0.5;
fn player_handle_input(actor: &mut Actor, input: &InputState, dt: f32) {
actor.facing += dt * PLAYER_TURN_RATE * input.xaxis;
if input.yaxis > 0.0 {
player_thrust(actor, dt);
}
}
fn player_thrust(actor: &mut Actor, dt: f32) {
let direction_vector = vec_from_angle(actor.facing);
let thrust_vector = direction_vector * (PLAYER_THRUST);
actor.velocity += thrust_vector * (dt);
}
const MAX_PHYSICS_VEL: f32 = 250.0;
fn update_actor_position(actor: &mut Actor, dt: f32) {
let norm_sq = actor.velocity.length_squared();
if norm_sq > MAX_PHYSICS_VEL.powi(2) {
actor.velocity = actor.velocity / norm_sq.sqrt() * MAX_PHYSICS_VEL;
}
let dv = actor.velocity * dt;
actor.pos += dv;
actor.facing += actor.ang_vel;
}
fn wrap_actor_position(actor: &mut Actor, sx: f32, sy: f32) {
let screen_x_bounds = sx / 2.0;
let screen_y_bounds = sy / 2.0;
if actor.pos.x > screen_x_bounds {
actor.pos -= Vec2::new(sx, 0.0);
} else if actor.pos.x < -screen_x_bounds {
actor.pos += Vec2::new(sx, 0.0);
};
if actor.pos.y > screen_y_bounds {
actor.pos -= Vec2::new(0.0, sy);
} else if actor.pos.y < -screen_y_bounds {
actor.pos += Vec2::new(0.0, sy);
}
}
fn handle_timed_life(actor: &mut Actor, dt: f32) {
actor.life -= dt;
}
fn world_to_screen_coords(screen_width: f32, screen_height: f32, point: Point2) -> Point2 {
let x = point.x + screen_width / 2.0;
let y = screen_height - (point.y + screen_height / 2.0);
Point2::new(x, y)
}
struct Assets {
player_image: graphics::Image,
shot_image: graphics::Image,
rock_image: graphics::Image,
font: graphics::Font,
shot_sound: audio::Source,
hit_sound: audio::Source,
}
impl Assets {
fn new(ctx: &mut Context) -> GameResult<Assets> {
let player_image = graphics::Image::new(ctx, "/player.png")?;
let shot_image = graphics::Image::new(ctx, "/shot.png")?;
let rock_image = graphics::Image::new(ctx, "/rock.png")?;
let font = graphics::Font::new(ctx, "/LiberationMono-Regular.ttf")?;
let shot_sound = audio::Source::new(ctx, "/pew.ogg")?;
let hit_sound = audio::Source::new(ctx, "/boom.ogg")?;
Ok(Assets {
player_image,
shot_image,
rock_image,
font,
shot_sound,
hit_sound,
})
}
fn actor_image(&mut self, actor: &Actor) -> &mut graphics::Image {
match actor.tag {
ActorType::Player => &mut self.player_image,
ActorType::Rock => &mut self.rock_image,
ActorType::Shot => &mut self.shot_image,
}
}
}
#[derive(Debug)]
struct InputState {
xaxis: f32,
yaxis: f32,
fire: bool,
}
impl Default for InputState {
fn default() -> Self {
InputState {
xaxis: 0.0,
yaxis: 0.0,
fire: false,
}
}
}
struct MainState {
player: Actor,
shots: Vec<Actor>,
rocks: Vec<Actor>,
level: i32,
score: i32,
assets: Assets,
screen_width: f32,
screen_height: f32,
input: InputState,
player_shot_timeout: f32,
rng: Rand32,
}
impl MainState {
fn new(ctx: &mut Context) -> GameResult<MainState> {
println!("Game resource path: {:?}", ctx.filesystem);
print_instructions();
let mut seed: [u8; 8] = [0; 8];
getrandom::getrandom(&mut seed[..]).expect("Could not create RNG seed");
let mut rng = Rand32::new(u64::from_ne_bytes(seed));
let assets = Assets::new(ctx)?;
let player = create_player();
let rocks = create_rocks(&mut rng, 5, player.pos, 100.0, 250.0);
let (width, height) = graphics::drawable_size(ctx);
let s = MainState {
player,
shots: Vec::new(),
rocks,
level: 0,
score: 0,
assets,
screen_width: width,
screen_height: height,
input: InputState::default(),
player_shot_timeout: 0.0,
rng,
};
Ok(s)
}
fn fire_player_shot(&mut self, ctx: &Context) {
self.player_shot_timeout = PLAYER_SHOT_TIME;
let player = &self.player;
let mut shot = create_shot();
shot.pos = player.pos;
shot.facing = player.facing;
let direction = vec_from_angle(shot.facing);
shot.velocity = SHOT_SPEED * direction;
self.shots.push(shot);
let _ = self.assets.shot_sound.play(ctx);
}
fn clear_dead_stuff(&mut self) {
self.shots.retain(|s| s.life > 0.0);
self.rocks.retain(|r| r.life > 0.0);
}
fn handle_collisions(&mut self, ctx: &Context) {
for rock in &mut self.rocks {
let pdistance = rock.pos - self.player.pos;
if pdistance.length() < (self.player.bbox_size + rock.bbox_size) {
self.player.life = 0.0;
}
for shot in &mut self.shots {
let distance = shot.pos - rock.pos;
if distance.length() < (shot.bbox_size + rock.bbox_size) {
shot.life = 0.0;
rock.life = 0.0;
self.score += 1;
let _ = self.assets.hit_sound.play(ctx);
}
}
}
}
fn check_for_level_respawn(&mut self) {
if self.rocks.is_empty() {
self.level += 1;
let r = create_rocks(&mut self.rng, self.level + 5, self.player.pos, 100.0, 250.0);
self.rocks.extend(r);
}
}
}
fn print_instructions() {
println!();
println!("Welcome to ASTROBLASTO!");
println!();
println!("How to play:");
println!("L/R arrow keys rotate your ship, up thrusts, space bar fires");
println!();
}
fn draw_actor(
assets: &mut Assets,
ctx: &mut Context,
actor: &Actor,
world_coords: (f32, f32),
) -> GameResult {
let (screen_w, screen_h) = world_coords;
let pos = world_to_screen_coords(screen_w, screen_h, actor.pos);
let image = assets.actor_image(actor);
let drawparams = graphics::DrawParam::new()
.dest(pos)
.rotation(actor.facing as f32)
.offset(Point2::new(0.5, 0.5));
graphics::draw(ctx, image, drawparams)
}
impl EventHandler<ggez::GameError> for MainState {
fn update(&mut self, ctx: &mut Context) -> GameResult {
const DESIRED_FPS: u32 = 60;
while timer::check_update_time(ctx, DESIRED_FPS) {
let seconds = 1.0 / (DESIRED_FPS as f32);
player_handle_input(&mut self.player, &self.input, seconds);
self.player_shot_timeout -= seconds;
if self.input.fire && self.player_shot_timeout < 0.0 {
self.fire_player_shot(ctx);
}
update_actor_position(&mut self.player, seconds);
wrap_actor_position(
&mut self.player,
self.screen_width as f32,
self.screen_height as f32,
);
for act in &mut self.shots {
update_actor_position(act, seconds);
wrap_actor_position(act, self.screen_width as f32, self.screen_height as f32);
handle_timed_life(act, seconds);
}
for act in &mut self.rocks {
update_actor_position(act, seconds);
wrap_actor_position(act, self.screen_width as f32, self.screen_height as f32);
}
self.handle_collisions(ctx);
self.clear_dead_stuff();
self.check_for_level_respawn();
if self.player.life <= 0.0 {
println!("Game over!");
let _ = event::quit(ctx);
}
}
Ok(())
}
fn draw(&mut self, ctx: &mut Context) -> GameResult {
graphics::clear(ctx, Color::BLACK);
{
let assets = &mut self.assets;
let coords = (self.screen_width, self.screen_height);
let p = &self.player;
draw_actor(assets, ctx, p, coords)?;
for s in &self.shots {
draw_actor(assets, ctx, s, coords)?;
}
for r in &self.rocks {
draw_actor(assets, ctx, r, coords)?;
}
}
let level_dest = Point2::new(10.0, 10.0);
let score_dest = Point2::new(200.0, 10.0);
let level_str = format!("Level: {}", self.level);
let score_str = format!("Score: {}", self.score);
let level_display = graphics::Text::new((level_str, self.assets.font, 32.0));
let score_display = graphics::Text::new((score_str, self.assets.font, 32.0));
graphics::draw(ctx, &level_display, (level_dest, 0.0, Color::WHITE))?;
graphics::draw(ctx, &score_display, (score_dest, 0.0, Color::WHITE))?;
graphics::present(ctx)?;
timer::yield_now();
Ok(())
}
fn key_down_event(
&mut self,
ctx: &mut Context,
keycode: KeyCode,
_keymod: KeyMods,
_repeat: bool,
) {
match keycode {
KeyCode::Up => {
self.input.yaxis = 1.0;
}
KeyCode::Left => {
self.input.xaxis = -1.0;
}
KeyCode::Right => {
self.input.xaxis = 1.0;
}
KeyCode::Space => {
self.input.fire = true;
}
KeyCode::P => {
let img = graphics::screenshot(ctx).expect("Could not take screenshot");
img.encode(ctx, graphics::ImageFormat::Png, "/screenshot.png")
.expect("Could not save screenshot");
}
KeyCode::Escape => event::quit(ctx),
_ => (), }
}
fn key_up_event(&mut self, _ctx: &mut Context, keycode: KeyCode, _keymod: KeyMods) {
match keycode {
KeyCode::Up => {
self.input.yaxis = 0.0;
}
KeyCode::Left | KeyCode::Right => {
self.input.xaxis = 0.0;
}
KeyCode::Space => {
self.input.fire = false;
}
_ => (), }
}
}
pub fn main() -> GameResult {
let resource_dir = if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
let mut path = path::PathBuf::from(manifest_dir);
path.push("resources");
path
} else {
path::PathBuf::from("./resources")
};
let cb = ContextBuilder::new("astroblasto", "ggez")
.window_setup(conf::WindowSetup::default().title("Astroblasto!"))
.window_mode(conf::WindowMode::default().dimensions(640.0, 480.0))
.add_resource_path(resource_dir);
let (mut ctx, events_loop) = cb.build()?;
let game = MainState::new(&mut ctx)?;
event::run(ctx, events_loop, game)
}