Skip to main content

flight/
flight.rs

1//! Fly a drone through a big hall in full 3D and find the birds, which sit at different
2//! heights. Sounds above you are clear, sounds below a bit dull, sounds behind you muffled.
3//!
4//! * Up / Down: fly forward / backward in the direction you look
5//! * Left / Right: turn 30 degrees
6//! * Page Up / Page Down: look 30 degrees up / down
7//! * E / Q: rise / sink straight up or down
8//! * C: where is the next bird?  F: where am I looking?  H: how high am I?
9//! * Escape: quit
10//!
11//! All sounds are generated with the synth. Run with `cargo run --example flight` and use
12//! headphones.
13
14use audio_code::prelude::*;
15
16const BIRDS: [Vec3; 4] = [
17    vec3(10.0, 15.0, 8.0),  // high up in the north east
18    vec3(-12.0, 5.0, 1.0),  // just above the floor in the west
19    vec3(0.0, -14.0, 14.0), // under the roof in the south
20    vec3(14.0, -10.0, 4.0),
21];
22
23struct Flight {
24    drone: BodyId,
25    birds: usize,
26    /// Whether the last move was blocked, so the bump plays once and not every frame.
27    blocked: bool,
28}
29
30impl Flight {
31    fn new(ctx: &mut Context) -> Self {
32        ctx.audio.register("bird", Synth::sine(2200.0).sweep_to(3200.0).length(0.07).then(Synth::silence(0.05)).then(Synth::sine(2600.0).length(0.06)).then(Synth::silence(0.7)));
33        ctx.audio.register("hum", Synth::saw(70.0).length(1.0).volume(0.08));
34        ctx.audio.register("bump", Synth::sine(80.0).length(0.15).decay(25.0).volume(0.8));
35        ctx.audio.register("found", Synth::triangle(660.0).sweep_to(1320.0).length(0.4).decay(6.0).volume(0.3));
36
37        // A hall of 40 x 40 meters and 16 meters high: floor, roof and four walls.
38        // A box is given by its center and its full width (x), depth (y) and height (z).
39        let physics = &mut ctx.physics;
40        physics.add(Body::fixed(vec3(0.0, 0.0, -0.5)).with_box(40.0, 40.0, 1.0).with_tag("floor"));
41        physics.add(Body::fixed(vec3(0.0, 0.0, 16.5)).with_box(40.0, 40.0, 1.0).with_tag("roof"));
42        physics.add(Body::fixed(vec3(0.0, 20.5, 8.0)).with_box(40.0, 1.0, 16.0).with_tag("north wall"));
43        physics.add(Body::fixed(vec3(0.0, -20.5, 8.0)).with_box(40.0, 1.0, 16.0).with_tag("south wall"));
44        physics.add(Body::fixed(vec3(20.5, 0.0, 8.0)).with_box(1.0, 40.0, 16.0).with_tag("east wall"));
45        physics.add(Body::fixed(vec3(-20.5, 0.0, 8.0)).with_box(1.0, 40.0, 16.0).with_tag("west wall"));
46
47        // The drone starts in the middle, 2 meters above the floor. No gravity: it hovers.
48        let drone = ctx.physics.add(Body::kinematic(vec3(0.0, 0.0, 2.0)).with_sphere(0.5).with_tag("drone"));
49        ctx.follow(drone); // the ears use the drone's position, facing and pitch
50        ctx.audio.play_looped("hum");
51
52        for spot in BIRDS {
53            let bird = ctx.physics.add(Body::sensor(spot).with_sphere(1.0).with_tag("bird"));
54            ctx.loop_on(bird, "bird");
55        }
56        Self { drone, birds: BIRDS.len(), blocked: false }
57    }
58
59    fn fly(&mut self, ctx: &mut Context, result: MoveResult) {
60        if result.blocked && !self.blocked
61            && let Some(&hit) = result.hits.first()
62        {
63            ctx.play("bump");
64            ctx.speech.speak(ctx.physics.tag(hit));
65        }
66        self.blocked = result.blocked;
67    }
68}
69
70impl Scene for Flight {
71    fn start(&mut self, ctx: &mut Context) {
72        ctx.speak("Find the four birds. Arrows fly and turn, page up and down look up and down, E and Q rise and sink. C locates the nearest bird.");
73    }
74
75    fn update(&mut self, ctx: &mut Context) {
76        if ctx.pressed(Key::Escape) {
77            ctx.quit();
78        }
79        if ctx.pressed(Key::Left) || ctx.pressed(Key::Right) {
80            let facing = ctx.physics.turn(self.drone, if ctx.pressed(Key::Left) { -30.0 } else { 30.0 });
81            ctx.speak(math::direction_name(facing));
82        }
83        if ctx.pressed(Key::PageUp) || ctx.pressed(Key::PageDown) {
84            let pitch = ctx.physics.tilt(self.drone, if ctx.pressed(Key::PageUp) { 30.0 } else { -30.0 });
85            ctx.speak(describe_pitch(pitch));
86        }
87
88        // Smooth flight: 4 meters per second while a key is held.
89        let speed = 4.0 * ctx.time.delta();
90        let forward = ctx.input.axis(Key::Down, Key::Up);
91        if forward != 0.0 {
92            let result = ctx.physics.move_forward(self.drone, forward * speed);
93            self.fly(ctx, result);
94        }
95        let rise = ctx.input.axis(Key::Q, Key::E);
96        if rise != 0.0 {
97            let result = ctx.physics.move_up(self.drone, rise * speed);
98            self.fly(ctx, result);
99        }
100
101        if ctx.pressed(Key::C) {
102            let position = ctx.physics.position(self.drone);
103            match ctx.physics.nearest_with_tag(position, "bird") {
104                Some(bird) => {
105                    let text = ctx.physics.describe(self.drone, bird); // e.g. "2 o'clock, 9 meters, 6 meters above"
106                    ctx.speak(text);
107                }
108                None => ctx.speak("No birds left"),
109            }
110        }
111        if ctx.pressed(Key::F) {
112            let (facing, pitch) = (ctx.physics.facing(self.drone), ctx.physics.pitch(self.drone));
113            ctx.speak(format!("Facing {}, {}", math::direction_name(facing), describe_pitch(pitch)));
114        }
115        if ctx.pressed(Key::H) {
116            let height = ctx.physics.position(self.drone).z;
117            ctx.speak(format!("{height:.0} meters high"));
118        }
119
120        if let Some(bird) = ctx.physics.entered_tag(self.drone, "bird") {
121            ctx.physics.remove(bird);
122            self.birds -= 1;
123            ctx.play("found");
124            if self.birds == 0 {
125                ctx.ui.message("done", "You found every bird!");
126            } else {
127                ctx.speak(format!("Bird found! {} left.", self.birds));
128            }
129        }
130        if ctx.ui.closed("done") {
131            ctx.quit();
132        }
133    }
134}
135
136fn describe_pitch(pitch: f32) -> String {
137    match pitch.round() as i32 {
138        0 => "looking straight ahead".to_string(),
139        90 => "looking straight up".to_string(),
140        -90 => "looking straight down".to_string(),
141        p if p > 0 => format!("looking {p} degrees up"),
142        p => format!("looking {} degrees down", -p),
143    }
144}
145
146fn main() -> audio_code::Result<()> {
147    Engine::new("Flight").echo_speech(true).run_with(Flight::new)
148}