Skip to content

Instantly share code, notes, and snippets.

@teki
Last active September 30, 2018 11:31
Show Gist options
  • Save teki/85a24b60ecb5a2fbba18ea457a051709 to your computer and use it in GitHub Desktop.
Save teki/85a24b60ecb5a2fbba18ea457a051709 to your computer and use it in GitHub Desktop.
asteroids game
/* Cargo.toml
[package]
name = "asteroids"
version = "0.1.0"
authors = ["me <[email protected]>"]
edition = "2018"
[dependencies]
rand = "0.5"
[dependencies.sdl2]
version = "0.31"
default-features = false
features = ["gfx"]
*/
extern crate rand;
extern crate sdl2;
// std
use std::time::{Duration, Instant};
// sdl2
use sdl2::event::Event;
//use sdl2::rect::Rect;
use sdl2::keyboard::Keycode;
use sdl2::keyboard::Scancode;
use sdl2::pixels::Color;
use sdl2::render::{Canvas, RenderTarget};
use sdl2::EventPump;
// gfx
use sdl2::gfx::primitives::DrawRenderer;
// rand
use rand::Rng;
const UPDATE_RATE: u32 = 30;
const UPDATE_LENGTH_NS: u32 = 1_000_000_000 / UPDATE_RATE;
const UPDATE_MAX_SKIP: u32 = 10;
const UPDATE_DT: f32 = 1.0 / (UPDATE_RATE as f32);
const TWO_PI: f32 = std::f32::consts::PI * 2.0;
const BULLET_RADIUS: f32 = 5.0;
const ASTEROID_STAGES: [(f32, f32); 4] = [(120.0, 15.0), (70.0, 30.0), (50.0, 50.0), (20.0, 80.0)];
const ASTEROID_STAGES_DEAD: usize = 1234;
struct Ship {
x: f32,
y: f32,
radius: f32,
angle: f32,
speed_x: f32,
speed_y: f32,
}
struct Bullet {
x: f32,
y: f32,
angle: f32,
time_left: f32,
}
struct Asteroid {
x: f32,
y: f32,
angle: f32,
stage: usize,
}
struct Game<'a> {
rng: &'a mut rand::ThreadRng,
name: &'static str,
arena_w: f32,
arena_h: f32,
ship: Ship,
key_right: bool,
key_left: bool,
key_up: bool,
key_fire: bool,
bullets: Vec<Bullet>,
bullet_timer: f32,
asteroids: Vec<Asteroid>,
}
fn draw_strings<T: RenderTarget>(canvas: &mut Canvas<T>, lines: &[&str]) {
let mut string_y = 0;
for &line in lines.iter() {
let _ = canvas.string(0, string_y, line, Color::RGB(255, 255, 255));
string_y += 8;
}
}
fn are_circles_intersecting(
a_x: f32,
a_y: f32,
a_radius: f32,
b_x: f32,
b_y: f32,
b_radius: f32,
) -> bool {
(a_x - b_x).powi(2) + (a_y - b_y).powi(2) <= (a_radius + b_radius).powi(2)
}
fn wrap_val<T>(val: T, limit: T) -> T
where
T: Copy,
T: std::ops::Add<Output = T> + std::ops::Rem<Output = T>,
{
(limit + val) % limit
}
impl<'a> Game<'a> {
fn new(arena_w: u32, arena_h: u32, rng: &'a mut rand::ThreadRng) -> Game<'a> {
let mut g = Game {
rng,
name: "Asteroids",
arena_w: arena_w as f32,
arena_h: arena_h as f32,
ship: Ship {
x: 0.0,
y: 0.0,
radius: 30.0,
angle: 0.0,
speed_x: 0.0,
speed_y: 0.0,
},
key_right: false,
key_left: false,
key_up: false,
key_fire: false,
bullets: vec![],
bullet_timer: 0.0,
asteroids: vec![],
};
g.reset();
g
}
fn reset(&mut self) {
self.ship.x = self.arena_w / 2.0;
self.ship.y = self.arena_h / 2.0;
self.ship.angle = 0.0;
self.ship.speed_x = 0.0;
self.ship.speed_y = 0.0;
self.bullets.clear();
self.bullet_timer = 0.5;
self.asteroids.clear();
self.asteroids.push(Asteroid {
x: 100.0,
y: 100.0,
angle: self.rng.gen_range(0.0, 1.0),
stage: ASTEROID_STAGES.len() - 1,
});
self.asteroids.push(Asteroid {
x: self.arena_w - 100.0,
y: 100.0,
angle: self.rng.gen_range(0.0, 1.0),
stage: ASTEROID_STAGES.len() - 1,
});
self.asteroids.push(Asteroid {
x: self.arena_w / 2.0,
y: self.arena_h - 100.0,
angle: self.rng.gen_range(0.0, 1.0),
stage: ASTEROID_STAGES.len() - 1,
});
}
fn input(&mut self, event_source: &mut EventPump) -> bool {
// process evetns
for event in event_source.poll_iter() {
match event {
Event::Quit { .. }
| Event::KeyDown {
keycode: Some(Keycode::Escape),
..
} => return false,
_ => {}
}
}
// update keyboard state
let keys = event_source.keyboard_state();
self.key_right = keys.is_scancode_pressed(Scancode::Right);
self.key_left = keys.is_scancode_pressed(Scancode::Left);
self.key_up = keys.is_scancode_pressed(Scancode::Up);
self.key_fire = keys.is_scancode_pressed(Scancode::S);
// continue
return true;
}
fn update(&mut self) {
// move ship
if self.key_right {
self.ship.angle = wrap_val(self.ship.angle + 10.0 * UPDATE_DT, TWO_PI);
}
if self.key_left {
self.ship.angle = wrap_val(self.ship.angle - 10.0 * UPDATE_DT, TWO_PI);
}
if self.key_up {
let ship_speed = 100.0;
self.ship.speed_x = self.ship.speed_x + self.ship.angle.cos() * ship_speed * UPDATE_DT;
self.ship.speed_y = self.ship.speed_y + self.ship.angle.sin() * ship_speed * UPDATE_DT;
}
self.ship.x = wrap_val(self.ship.x + self.ship.speed_x * UPDATE_DT, self.arena_w);
self.ship.y = wrap_val(self.ship.y + self.ship.speed_y * UPDATE_DT, self.arena_h);
// shoot
self.bullet_timer += UPDATE_DT;
if self.key_fire {
if self.bullet_timer >= 0.5 {
self.bullet_timer = 0.0;
self.bullets.push(Bullet {
x: self.ship.x + self.ship.angle.cos() * self.ship.radius,
y: self.ship.y + self.ship.angle.sin() * self.ship.radius,
angle: self.ship.angle,
time_left: 4.0,
});
}
}
// bullets
let mut new_asteroids: Vec<Asteroid> = vec![];
for (_idx, b) in self.bullets.iter_mut().enumerate() {
let bullet_speed = 500.0;
b.x = wrap_val(b.x + b.angle.cos() * bullet_speed * UPDATE_DT, self.arena_w);
b.y = wrap_val(b.y + b.angle.sin() * bullet_speed * UPDATE_DT, self.arena_h);
b.time_left -= UPDATE_DT;
// collide with asteroid
for (_idx, a) in self.asteroids.iter_mut().enumerate() {
if are_circles_intersecting(
b.x,
b.y,
BULLET_RADIUS,
a.x,
a.y,
ASTEROID_STAGES[a.stage].1,
) {
// kill bullet and asteroid
b.time_left = 0.0;
// add 2 new asteroids
if a.stage > 1 {
new_asteroids.push(Asteroid {
x: a.x,
y: a.y,
angle: TWO_PI * self.rng.gen_range(0.0, 1.0),
stage: a.stage - 1,
});
new_asteroids.push(Asteroid {
x: a.x,
y: a.y,
angle: wrap_val(
new_asteroids.last().unwrap().angle - std::f32::consts::PI,
TWO_PI,
),
stage: a.stage - 1,
});
}
a.stage = ASTEROID_STAGES_DEAD;
}
}
self.asteroids.retain(|a| a.stage < ASTEROID_STAGES_DEAD);
}
self.bullets.retain(|b| b.time_left > 0.0);
self.asteroids.append(&mut new_asteroids);
if self.asteroids.is_empty() {
self.reset();
return;
}
// asteroids
for (_idx, a) in self.asteroids.iter_mut().enumerate() {
let asteroid_speed = ASTEROID_STAGES[a.stage].0;
a.x = wrap_val(
a.x + a.angle.cos() * asteroid_speed * UPDATE_DT,
self.arena_w,
);
a.y = wrap_val(
a.y + a.angle.sin() * asteroid_speed * UPDATE_DT,
self.arena_h,
);
// collosion
if are_circles_intersecting(
self.ship.x,
self.ship.y,
self.ship.radius,
a.x,
a.y,
ASTEROID_STAGES[a.stage].1,
) {
// restart game
self.reset();
return;
}
}
}
fn draw_objects<T: RenderTarget>(&self, canvas: &mut Canvas<T>, offset_x: i16, offset_y: i16) {
// ship
let _ = canvas.filled_circle(
offset_x + self.ship.x as i16,
offset_y + self.ship.y as i16,
self.ship.radius as i16,
Color::RGB(0, 0, 255),
);
// ship cockpit
let ship_circle_distance = 20.0;
let _ = canvas.filled_circle(
offset_x + (self.ship.x + self.ship.angle.cos() * ship_circle_distance) as i16,
offset_y + (self.ship.y + self.ship.angle.sin() * ship_circle_distance) as i16,
5,
Color::RGB(0, 255, 255),
);
// bullets
for (_idx, b) in self.bullets.iter().enumerate() {
let _ = canvas.filled_circle(
offset_x + b.x as i16,
offset_y + b.y as i16,
BULLET_RADIUS as i16,
Color::RGB(0, 255, 0),
);
}
// asteroids
for (_idx, a) in self.asteroids.iter().enumerate() {
let _ = canvas.filled_circle(
offset_x + a.x as i16,
offset_y + a.y as i16,
ASTEROID_STAGES[a.stage].1 as i16,
Color::RGB(255, 255, 0),
);
}
}
fn draw<T: RenderTarget>(&self, canvas: &mut Canvas<T>) {
canvas.set_draw_color(Color::RGB(0, 0, 0));
canvas.clear();
for y in -1..2 {
for x in -1..2 {
self.draw_objects(canvas, x * (self.arena_w as i16), y * (self.arena_h as i16));
}
}
/*
draw_strings(
canvas,
&[
&format!("ship.angle: {}", self.ship.angle),
&format!("ship.x: {}", self.ship.x),
&format!("ship.y: {}", self.ship.x),
&format!("ship.speed_x: {}", self.ship.speed_x),
&format!("ship.speed_y: {}", self.ship.speed_x),
],
);
*/
}
}
pub fn main() {
let arena_w = 800;
let arena_h = 600;
let mut rng = rand::thread_rng();
let mut game = Game::new(arena_w, arena_h, &mut rng);
let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap();
let window = video_subsystem
.window(game.name, arena_w, arena_h)
.position_centered()
.build()
.unwrap();
let mut canvas = window
.into_canvas()
.accelerated()
.present_vsync()
.build()
.unwrap();
let mut event_source = sdl_context.event_pump().unwrap();
let update_length = Duration::new(0, UPDATE_LENGTH_NS);
let mut next_update = Instant::now();
'running: loop {
let mut loop_cnt = 0;
while (Instant::now() > next_update) && (loop_cnt < UPDATE_MAX_SKIP) {
if !game.input(&mut event_source) {
break 'running;
}
game.update();
next_update += update_length;
loop_cnt += 1;
}
game.draw(&mut canvas);
canvas.present();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment