Skip to main content

online/
online.rs

1//! A small online game: walk around a field, hear the other players and chat.
2//!
3//! One player picks "Host a game", the others type the host's address and pick "Join".
4//! To try it alone, start the example twice on the same computer and join `localhost`.
5//!
6//! * Arrows: walk (1 meter per step)
7//! * Enter: chat
8//! * Space: where are the others?
9//! * P: ping to the server
10//! * Escape: leave
11//!
12//! The host decides: clients only tell the host where they walked, and the host passes it
13//! on to everybody else. Run with `cargo run --example online` and use headphones.
14
15use std::collections::BTreeMap;
16
17use audio_code::prelude::*;
18
19struct Lobby {
20    name: String,
21    address: String,
22}
23
24impl Lobby {
25    fn new(ctx: &mut Context) -> Self {
26        Self {
27            name: ctx.storage.get_or("name", "Player".to_string()),
28            address: ctx.storage.get_or("address", "localhost".to_string()),
29        }
30    }
31
32    fn menu(&self) -> Menu {
33        Menu::new("lobby", "Online demo")
34            .text_field("name", "Your name", self.name.clone())
35            .button("host", "Host a game")
36            .text_field("address", "Server address", self.address.clone())
37            .button("join", "Join")
38            .button("quit", "Quit")
39            .closable(false)
40    }
41
42    fn start_game(ctx: &mut Context) {
43        ctx.ui.close();
44        let scene = Field::new(ctx);
45        ctx.switch_scene(scene);
46    }
47}
48
49impl Scene for Lobby {
50    fn start(&mut self, ctx: &mut Context) {
51        ctx.ui.open(self.menu());
52    }
53
54    fn on_ui(&mut self, event: &UiEvent, ctx: &mut Context) {
55        match event {
56            UiEvent::Changed { item, value, .. } if item == "name" => {
57                self.name = value.as_str().to_string();
58                ctx.storage.set("name", &self.name);
59            }
60            UiEvent::Changed { item, value, .. } if item == "address" => {
61                self.address = value.as_str().to_string();
62                ctx.storage.set("address", &self.address);
63            }
64            UiEvent::Activated { item, .. } => match item.as_str() {
65                "host" => {
66                    ctx.net.set_name(&self.name);
67                    match ctx.net.host(net::DEFAULT_PORT) {
68                        Ok(()) => {
69                            let address = Net::local_ip().map_or("unknown".to_string(), |ip| ip.to_string());
70                            ctx.speak(format!("Hosting. Your address is {address}"));
71                            Self::start_game(ctx);
72                        }
73                        Err(e) => ctx.ui.message("error", e.to_string()),
74                    }
75                }
76                "join" => {
77                    ctx.net.set_name(&self.name);
78                    ctx.net.join(&self.address);
79                    ctx.speak(format!("Connecting to {}", self.address));
80                }
81                "quit" => ctx.quit(),
82                _ => {}
83            },
84            _ => {}
85        }
86    }
87
88    fn on_net(&mut self, event: &NetEvent, ctx: &mut Context) {
89        match event {
90            NetEvent::Connected(_) => Self::start_game(ctx),
91            NetEvent::Disconnected(_, reason) => ctx.ui.message("error", ctx.lang.describe_reason(reason)),
92            NetEvent::Message(..) => {}
93        }
94    }
95
96    fn update(&mut self, _ctx: &mut Context) {}
97}
98
99/// Another player: where they are and the sound that marks them.
100struct Other {
101    name: String,
102    position: Vec3,
103    sound: Sound,
104}
105
106struct Field {
107    position: Vec3,
108    others: BTreeMap<PeerId, Other>,
109}
110
111impl Field {
112    fn new(ctx: &mut Context) -> Self {
113        ctx.audio.register("other", Synth::triangle(660.0).length(0.06).decay(40.0).then(Synth::silence(0.5)));
114        ctx.audio.register("step", Synth::noise().length(0.05).decay(80.0).volume(0.5));
115        ctx.audio.register("chat", Synth::sine(1200.0).length(0.05).volume(0.3));
116        Self { position: Vec3::ZERO, others: BTreeMap::new() }
117    }
118
119    fn add(&mut self, ctx: &mut Context, id: PeerId, name: &str, position: Vec3) {
120        let sound = ctx.audio.play_looped_at("other", position);
121        sound.set_pitch(1.0 + id.number() as f32 * 0.15);
122        self.others.insert(id, Other { name: name.to_string(), position, sound });
123    }
124
125    fn remove(&mut self, ctx: &mut Context, id: PeerId) {
126        if let Some(other) = self.others.remove(&id) {
127            other.sound.stop();
128            ctx.speak(format!("{} left", other.name));
129        }
130    }
131
132    fn moved(&mut self, id: PeerId, position: Vec3) {
133        if let Some(other) = self.others.get_mut(&id) {
134            other.position = position;
135            other.sound.set_position(position);
136        }
137    }
138
139    fn name(&self, ctx: &Context, id: PeerId) -> String {
140        if id == ctx.net.id() {
141            return ctx.net.name(id).to_string();
142        }
143        self.others.get(&id).map_or("Someone".to_string(), |o| o.name.clone())
144    }
145
146    /// The host's side of the protocol.
147    fn host_events(&mut self, event: &NetEvent, ctx: &mut Context) {
148        match event {
149            NetEvent::Connected(id) => {
150                let name = ctx.net.name(*id).to_string();
151                ctx.speak(format!("{name} joined"));
152                // Tell the newcomer about everybody, and everybody about the newcomer.
153                ctx.net.send(*id, Packet::new("player").with(PeerId::SERVER).with(ctx.net.name(PeerId::SERVER)).with(self.position));
154                for (other, o) in &self.others {
155                    ctx.net.send(*id, Packet::new("player").with(*other).with(&o.name).with(o.position));
156                }
157                ctx.net.send_all_except(*id, Packet::new("player").with(*id).with(&name).with(Vec3::ZERO));
158                self.add(ctx, *id, &name, Vec3::ZERO);
159            }
160            NetEvent::Message(from, packet) => match packet.kind() {
161                "move" => {
162                    self.moved(*from, packet.vec3(0));
163                    ctx.net.send_all_except(*from, Packet::new("pos").with(*from).with(packet.vec3(0)));
164                }
165                "chat" => {
166                    let text = packet.text(0);
167                    ctx.play("chat");
168                    ctx.speak(format!("{}: {text}", self.name(ctx, *from)));
169                    ctx.net.send_all_except(*from, Packet::new("chat").with(*from).with(text));
170                }
171                _ => {}
172            },
173            NetEvent::Disconnected(id, _) => {
174                self.remove(ctx, *id);
175                ctx.net.send_all(Packet::new("gone").with(*id));
176            }
177        }
178    }
179
180    /// The client's side of the protocol.
181    fn client_events(&mut self, event: &NetEvent, ctx: &mut Context) {
182        match event {
183            NetEvent::Message(_, packet) => match packet.kind() {
184                "player" => self.add(ctx, packet.peer(0), packet.text(1), packet.vec3(2)),
185                "pos" => self.moved(packet.peer(0), packet.vec3(1)),
186                "gone" => self.remove(ctx, packet.peer(0)),
187                "chat" => {
188                    ctx.play("chat");
189                    ctx.speak(format!("{}: {}", self.name(ctx, packet.peer(0)), packet.text(1)));
190                }
191                _ => {}
192            },
193            NetEvent::Disconnected(_, reason) => {
194                ctx.speak(ctx.lang.describe_reason(reason));
195                ctx.audio.stop_all();
196                let scene = Lobby::new(ctx);
197                ctx.switch_scene(scene);
198            }
199            NetEvent::Connected(_) => {}
200        }
201    }
202}
203
204impl Scene for Field {
205    fn start(&mut self, ctx: &mut Context) {
206        ctx.speech.speak_queued("Arrows walk, Enter chats, Space finds the others, Escape leaves.");
207    }
208
209    fn on_net(&mut self, event: &NetEvent, ctx: &mut Context) {
210        if ctx.net.is_host() {
211            self.host_events(event, ctx);
212        } else {
213            self.client_events(event, ctx);
214        }
215    }
216
217    fn on_ui(&mut self, event: &UiEvent, ctx: &mut Context) {
218        if let UiEvent::TextSubmitted { id, text } = event
219            && id == "chat"
220        {
221            // The host sends to everybody, a client sends to the host, who passes it on.
222            if ctx.net.is_host() {
223                ctx.net.send_all(Packet::new("chat").with(PeerId::SERVER).with(text));
224            } else {
225                ctx.net.send_all(Packet::new("chat").with(text));
226            }
227            ctx.play("chat");
228        }
229    }
230
231    fn update(&mut self, ctx: &mut Context) {
232        let step = vec3(
233            ctx.input.repeating(Key::Right, 0.25, 0.25) as i32 as f32 - ctx.input.repeating(Key::Left, 0.25, 0.25) as i32 as f32,
234            ctx.input.repeating(Key::Up, 0.25, 0.25) as i32 as f32 - ctx.input.repeating(Key::Down, 0.25, 0.25) as i32 as f32,
235            0.0,
236        );
237        if step != Vec3::ZERO {
238            self.position = (self.position + step).clamp(Vec3::splat(-20.0), Vec3::splat(20.0));
239            ctx.play("step");
240            if ctx.net.is_host() {
241                ctx.net.send_all(Packet::new("pos").with(PeerId::SERVER).with(self.position));
242            } else {
243                ctx.net.send_all(Packet::new("move").with(self.position));
244            }
245        }
246        ctx.audio.set_listener(self.position, 0.0);
247
248        if ctx.pressed(Key::Enter) {
249            ctx.ui.input("chat", "Message");
250        }
251        if ctx.pressed(Key::Space) {
252            let text = if self.others.is_empty() {
253                "Nobody else is here".to_string()
254            } else {
255                let places: Vec<String> = self
256                    .others
257                    .values()
258                    .map(|o| format!("{}: {}", o.name, ctx.lang.describe_position(self.position, 0.0, o.position)))
259                    .collect();
260                places.join(". ")
261            };
262            ctx.speak(text);
263        }
264        if ctx.pressed(Key::P) {
265            if ctx.net.is_host() {
266                ctx.speak(format!("You are the host, {} players connected", ctx.net.player_count()));
267            } else {
268                let ms = (ctx.net.ping(PeerId::SERVER) * 1000.0).round();
269                ctx.speak(format!("Ping {ms} milliseconds"));
270            }
271        }
272        if ctx.pressed(Key::Escape) {
273            ctx.net.close();
274            ctx.audio.stop_all();
275            ctx.speak("You left");
276            let scene = Lobby::new(ctx);
277            ctx.switch_scene(scene);
278        }
279    }
280}
281
282fn main() -> audio_code::Result<()> {
283    Engine::new("Online demo").run_with(Lobby::new)
284}