Skip to main content

asteroids/
asteroids.rs

1use macroquad::prelude::*;
2
3const SHIP_HEIGHT: f32 = 25.;
4const SHIP_BASE: f32 = 22.;
5struct Ship {
6    pos: Vec2,
7    rot: f32,
8    vel: Vec2,
9}
10
11struct Bullet {
12    pos: Vec2,
13    vel: Vec2,
14    shot_at: f64,
15    collided: bool,
16}
17
18struct Asteroid {
19    pos: Vec2,
20    vel: Vec2,
21    rot: f32,
22    rot_speed: f32,
23    size: f32,
24    sides: u8,
25    collided: bool,
26}
27
28fn wrap_around(v: &Vec2) -> Vec2 {
29    let mut vr = Vec2::new(v.x, v.y);
30    if vr.x > screen_width() {
31        vr.x = 0.;
32    }
33    if vr.x < 0. {
34        vr.x = screen_width()
35    }
36    if vr.y > screen_height() {
37        vr.y = 0.;
38    }
39    if vr.y < 0. {
40        vr.y = screen_height()
41    }
42    vr
43}
44
45#[macroquad::main("Asteroids")]
46async fn main() {
47    let mut ship = Ship {
48        pos: Vec2::new(screen_width() / 2., screen_height() / 2.),
49        rot: 0.,
50        vel: Vec2::new(0., 0.),
51    };
52
53    let mut bullets = Vec::new();
54    let mut last_shot = get_time();
55    let mut asteroids = Vec::new();
56    let mut gameover = false;
57
58    let mut screen_center;
59
60    loop {
61        if gameover {
62            clear_background(LIGHTGRAY);
63            let mut text = "You Win!. Press [enter] to play again.";
64            let font_size = 30.;
65
66            if asteroids.len() > 0 {
67                text = "Game Over. Press [enter] to play again.";
68            }
69            let text_size = measure_text(text, None, font_size as _, 1.0);
70            draw_text(
71                text,
72                screen_width() / 2. - text_size.width / 2.,
73                screen_height() / 2. - text_size.height / 2.,
74                font_size,
75                DARKGRAY,
76            );
77            if is_key_down(KeyCode::Enter) {
78                ship = Ship {
79                    pos: Vec2::new(screen_width() / 2., screen_height() / 2.),
80                    rot: 0.,
81                    vel: Vec2::new(0., 0.),
82                };
83                bullets = Vec::new();
84                asteroids = Vec::new();
85                gameover = false;
86                screen_center = Vec2::new(screen_width() / 2., screen_height() / 2.);
87                for _ in 0..10 {
88                    asteroids.push(Asteroid {
89                        pos: screen_center
90                            + Vec2::new(rand::gen_range(-1., 1.), rand::gen_range(-1., 1.))
91                                .normalize()
92                                * screen_width().min(screen_height())
93                                / 2.,
94                        vel: Vec2::new(rand::gen_range(-1., 1.), rand::gen_range(-1., 1.)),
95                        rot: 0.,
96                        rot_speed: rand::gen_range(-2., 2.),
97                        size: screen_width().min(screen_height()) / 10.,
98                        sides: rand::gen_range(3, 8),
99                        collided: false,
100                    })
101                }
102            }
103            next_frame().await;
104            continue;
105        }
106        let frame_t = get_time();
107        let rotation = ship.rot.to_radians();
108
109        let mut acc = -ship.vel / 100.; // Friction
110
111        // Forward
112        if is_key_down(KeyCode::Up) {
113            acc = Vec2::new(rotation.sin(), -rotation.cos()) / 3.;
114        }
115
116        // Shot
117        if is_key_down(KeyCode::Space) && frame_t - last_shot > 0.5 {
118            let rot_vec = Vec2::new(rotation.sin(), -rotation.cos());
119            bullets.push(Bullet {
120                pos: ship.pos + rot_vec * SHIP_HEIGHT / 2.,
121                vel: rot_vec * 7.,
122                shot_at: frame_t,
123                collided: false,
124            });
125            last_shot = frame_t;
126        }
127
128        // Steer
129        if is_key_down(KeyCode::Right) {
130            ship.rot += 5.;
131        } else if is_key_down(KeyCode::Left) {
132            ship.rot -= 5.;
133        }
134
135        // Euler integration
136        ship.vel += acc;
137        if ship.vel.length() > 5. {
138            ship.vel = ship.vel.normalize() * 5.;
139        }
140        ship.pos += ship.vel;
141        ship.pos = wrap_around(&ship.pos);
142
143        // Move each bullet
144        for bullet in bullets.iter_mut() {
145            bullet.pos += bullet.vel;
146        }
147
148        // Move each asteroid
149        for asteroid in asteroids.iter_mut() {
150            asteroid.pos += asteroid.vel;
151            asteroid.pos = wrap_around(&asteroid.pos);
152            asteroid.rot += asteroid.rot_speed;
153        }
154
155        // Bullet lifetime
156        bullets.retain(|bullet| bullet.shot_at + 1.5 > frame_t);
157
158        let mut new_asteroids = Vec::new();
159        for asteroid in asteroids.iter_mut() {
160            // Asteroid/ship collision
161            if (asteroid.pos - ship.pos).length() < asteroid.size + SHIP_HEIGHT / 3. {
162                gameover = true;
163                break;
164            }
165
166            // Asteroid/bullet collision
167            for bullet in bullets.iter_mut() {
168                if (asteroid.pos - bullet.pos).length() < asteroid.size {
169                    asteroid.collided = true;
170                    bullet.collided = true;
171
172                    // Break the asteroid
173                    if asteroid.sides > 3 {
174                        new_asteroids.push(Asteroid {
175                            pos: asteroid.pos,
176                            vel: Vec2::new(bullet.vel.y, -bullet.vel.x).normalize()
177                                * rand::gen_range(1., 3.),
178                            rot: rand::gen_range(0., 360.),
179                            rot_speed: rand::gen_range(-2., 2.),
180                            size: asteroid.size * 0.8,
181                            sides: asteroid.sides - 1,
182                            collided: false,
183                        });
184                        new_asteroids.push(Asteroid {
185                            pos: asteroid.pos,
186                            vel: Vec2::new(-bullet.vel.y, bullet.vel.x).normalize()
187                                * rand::gen_range(1., 3.),
188                            rot: rand::gen_range(0., 360.),
189                            rot_speed: rand::gen_range(-2., 2.),
190                            size: asteroid.size * 0.8,
191                            sides: asteroid.sides - 1,
192                            collided: false,
193                        })
194                    }
195                    break;
196                }
197            }
198        }
199
200        // Remove the collided objects
201        bullets.retain(|bullet| bullet.shot_at + 1.5 > frame_t && !bullet.collided);
202        asteroids.retain(|asteroid| !asteroid.collided);
203        asteroids.append(&mut new_asteroids);
204
205        // You win?
206        if asteroids.len() == 0 {
207            gameover = true;
208        }
209
210        if gameover {
211            continue;
212        }
213
214        clear_background(LIGHTGRAY);
215
216        for bullet in bullets.iter() {
217            draw_circle(bullet.pos.x, bullet.pos.y, 2., BLACK);
218        }
219
220        for asteroid in asteroids.iter() {
221            draw_poly_lines(
222                asteroid.pos.x,
223                asteroid.pos.y,
224                asteroid.sides,
225                asteroid.size,
226                asteroid.rot,
227                2.,
228                BLACK,
229            )
230        }
231
232        let v1 = Vec2::new(
233            ship.pos.x + rotation.sin() * SHIP_HEIGHT / 2.,
234            ship.pos.y - rotation.cos() * SHIP_HEIGHT / 2.,
235        );
236        let v2 = Vec2::new(
237            ship.pos.x - rotation.cos() * SHIP_BASE / 2. - rotation.sin() * SHIP_HEIGHT / 2.,
238            ship.pos.y - rotation.sin() * SHIP_BASE / 2. + rotation.cos() * SHIP_HEIGHT / 2.,
239        );
240        let v3 = Vec2::new(
241            ship.pos.x + rotation.cos() * SHIP_BASE / 2. - rotation.sin() * SHIP_HEIGHT / 2.,
242            ship.pos.y + rotation.sin() * SHIP_BASE / 2. + rotation.cos() * SHIP_HEIGHT / 2.,
243        );
244        draw_triangle_lines(v1, v2, v3, 2., BLACK);
245
246        next_frame().await
247    }
248}