Skip to main content

client_server/
client_server.rs

1use quaso::{
2    GameLauncher,
3    assets::{make_directory_database, shader::ShaderAsset},
4    config::Config,
5    context::GameContext,
6    game::{GameInstance, GameState, GameStateChange},
7    game_state_custom_event,
8    multiplayer::{
9        GameMultiplayerChange, GameNetwork,
10        client_server::ClientServerMultiplayer,
11        tcp::{TcpClientConnection, TcpServerConnection},
12        universal::UniversalMultiplayerGameState,
13    },
14    third_party::{
15        fontdue::layout::{HorizontalAlign, VerticalAlign},
16        raui_core::{
17            layout::CoordsMappingScaling,
18            widget::{
19                component::{
20                    image_box::ImageBoxProps, interactive::navigation::NavItemActive,
21                    text_box::TextBoxProps,
22                },
23                unit::text::{TextBoxFont, TextBoxHorizontalAlign, TextBoxVerticalAlign},
24                utils::Color,
25            },
26        },
27        raui_immediate_widgets::core::{
28            containers::{content_box, nav_horizontal_box},
29            image_box,
30            interactive::{ImmediateButton, button},
31            text_box,
32        },
33        spitfire_draw::{
34            context::DrawContext,
35            sprite::{Sprite, SpriteTexture},
36            text::Text,
37            utils::{Drawable, ShaderRef, TextureRef, Vertex},
38        },
39        spitfire_glow::{
40            graphics::{CameraScaling, GraphicsTarget, Shader},
41            renderer::GlowTextureFiltering,
42        },
43        spitfire_input::{
44            ArrayInputCombinator, CardinalInputCombinator, InputActionRef, InputAxisRef,
45            InputConsume, InputMapping, MouseButton, VirtualAction, VirtualAxis,
46        },
47        tehuti::{
48            channel::{ChannelId, ChannelMode},
49            codec::postcard::PostcardCodec,
50            event::{Receiver, unbounded},
51            peer::{Peer, PeerBuilder, PeerId, PeerInfo, PeerRoleId, TypedPeer, TypedPeerRole},
52            replica::{
53                Replica, ReplicaApplyChanges, ReplicaCollectChanges, ReplicaId, ReplicationBuffer,
54            },
55            replication::{HashReplicated, primitives::RepF32},
56        },
57        tehuti_client_server::{
58            authority::PureAuthority,
59            controller::{Controller, ControllerEvent},
60            puppet::{Puppet, Puppetable},
61        },
62        tehuti_timeline::time::TimeStamp,
63        tracing::{debug, level_filters::LevelFilter},
64        tracing_subscriber::{
65            Layer, fmt::layer, layer::SubscriberExt, registry, util::SubscriberInitExt,
66        },
67        vek::{Rgba, Vec2},
68        windowing::event::VirtualKeyCode,
69    },
70};
71use std::{collections::HashMap, error::Error, sync::Arc};
72
73const ADDRESS: &str = "127.0.0.1:12345";
74const PLAYER_ROLE: PeerRoleId = PeerRoleId::new(1);
75const PLAYER_EVENT_CHANNEL: ChannelId = ChannelId::new(0);
76const PLAYER_CHANGE_CHANNEL: ChannelId = ChannelId::new(1);
77const SPEED: f32 = 100.0;
78const COLOR_WHITE: Color = Color {
79    r: 1.0,
80    g: 1.0,
81    b: 1.0,
82    a: 1.0,
83};
84const COLOR_BLACK: Color = Color {
85    r: 0.0,
86    g: 0.0,
87    b: 0.0,
88    a: 1.0,
89};
90
91fn main() -> Result<(), Box<dyn Error>> {
92    registry()
93        .with(
94            layer()
95                .with_writer(std::io::stdout)
96                .with_filter(LevelFilter::DEBUG),
97        )
98        .init();
99
100    GameLauncher::new(GameInstance::new(Preloader).setup_assets(|assets| {
101        *assets = make_directory_database("./resources/").unwrap();
102    }))
103    .title("Netcode: Replicated Client-Server")
104    .config(Config::load_from_file("./resources/GameConfig.toml")?)
105    .run();
106    Ok(())
107}
108
109#[derive(Default)]
110struct Preloader;
111
112impl GameState for Preloader {
113    fn enter(&mut self, context: GameContext) {
114        context.graphics.state.color = [0.2, 0.2, 0.2, 1.0];
115        context.graphics.state.main_camera.screen_alignment = 0.5.into();
116        context.graphics.state.main_camera.scaling = CameraScaling::FitVertical(500.0);
117        context.gui.coords_map_scaling = CoordsMappingScaling::FitVertical(500.0);
118
119        context
120            .assets
121            .spawn(
122                "shader://color",
123                (ShaderAsset::new(
124                    Shader::COLORED_VERTEX_2D,
125                    Shader::PASS_FRAGMENT,
126                ),),
127            )
128            .unwrap();
129        context
130            .assets
131            .spawn(
132                "shader://image",
133                (ShaderAsset::new(
134                    Shader::TEXTURED_VERTEX_2D,
135                    Shader::TEXTURED_FRAGMENT,
136                ),),
137            )
138            .unwrap();
139        context
140            .assets
141            .spawn(
142                "shader://text",
143                (ShaderAsset::new(Shader::TEXT_VERTEX, Shader::TEXT_FRAGMENT),),
144            )
145            .unwrap();
146
147        context.assets.ensure("font://roboto.ttf").unwrap();
148        context.assets.ensure("texture://ferris.png").unwrap();
149    }
150
151    fn update(&mut self, context: GameContext, _: f32) {
152        if !context.assets.is_busy() {
153            *context.state_change = GameStateChange::Swap(Box::new(Lobby::default()));
154        }
155    }
156}
157
158#[derive(Default)]
159struct Lobby {
160    exit: InputActionRef,
161}
162
163impl GameState for Lobby {
164    fn enter(&mut self, context: GameContext) {
165        let pointer_x = InputAxisRef::default();
166        let pointer_y = InputAxisRef::default();
167        let pointer_trigger = InputActionRef::default();
168        self.exit = InputActionRef::default();
169
170        context.gui.interactions.inputs.pointer_position =
171            ArrayInputCombinator::new([pointer_x.clone(), pointer_y.clone()]);
172        context.gui.interactions.inputs.pointer_trigger = pointer_trigger.clone();
173
174        context.input.push_mapping(
175            InputMapping::default()
176                .consume(InputConsume::Hit)
177                .axis(VirtualAxis::MousePositionX, pointer_x)
178                .axis(VirtualAxis::MousePositionY, pointer_y)
179                .action(
180                    VirtualAction::MouseButton(MouseButton::Left),
181                    pointer_trigger,
182                )
183                .action(
184                    VirtualAction::KeyButton(VirtualKeyCode::Escape),
185                    self.exit.clone(),
186                ),
187        );
188    }
189
190    fn exit(&mut self, context: GameContext) {
191        context.input.pop_mapping();
192    }
193
194    fn update(&mut self, context: GameContext, _delta_time: f32) {
195        if let Some(multiplayer) = context.multiplayer::<ClientServerMultiplayer>()
196            && multiplayer.is_initialized()
197        {
198            *context.state_change = GameStateChange::Swap(Box::new(State::default()));
199        }
200    }
201
202    fn fixed_update(&mut self, context: GameContext, _delta_time: f32) {
203        if self.exit.get().is_pressed() {
204            *context.state_change = GameStateChange::Pop;
205        }
206    }
207
208    fn draw_gui(&mut self, context: GameContext) {
209        nav_horizontal_box(NavItemActive, || {
210            if context.multiplayer::<ClientServerMultiplayer>().is_some() {
211                text_box(TextBoxProps {
212                    text: "ESTABLISHING CONNECTION...".to_owned(),
213                    font: TextBoxFont {
214                        name: "roboto.ttf".to_owned(),
215                        size: 24.0,
216                    },
217                    color: COLOR_WHITE,
218                    horizontal_align: TextBoxHorizontalAlign::Center,
219                    vertical_align: TextBoxVerticalAlign::Middle,
220                    ..Default::default()
221                });
222            } else {
223                let host = lobby_button("HOST");
224                let join = lobby_button("JOIN");
225
226                let is_server = if host.trigger_start() {
227                    Some(true)
228                } else if join.trigger_start() {
229                    Some(false)
230                } else {
231                    None
232                };
233
234                if let Some(is_server) = is_server {
235                    debug!(
236                        "Starting {}...",
237                        if is_server { "server" } else { "client" }
238                    );
239
240                    context.network.clear_connections();
241                    let mut network = GameNetwork::new(Arc::new(
242                        PureAuthority::peer_factory(is_server)
243                            .unwrap()
244                            .with_typed::<PlayerController>(),
245                    ));
246                    if is_server {
247                        network.add_connection(TcpServerConnection::listen(ADDRESS).unwrap());
248                    } else {
249                        network.add_connection(TcpClientConnection::connect(ADDRESS).unwrap());
250                    }
251                    if let Some(multiplayer) = ClientServerMultiplayer::new(&network) {
252                        debug!("{} started", if is_server { "Server" } else { "Client" });
253
254                        *context.network = network;
255                        *context.multiplayer_change = GameMultiplayerChange::Set(Box::new(
256                            multiplayer.with_ticks_per_second(30),
257                        ));
258                    }
259                }
260            }
261        });
262    }
263}
264
265fn lobby_button(label: &str) -> ImmediateButton {
266    button(NavItemActive, |state| {
267        let (bg_color, text_color) = if state.state.selected {
268            (COLOR_WHITE, COLOR_BLACK)
269        } else {
270            (COLOR_BLACK, COLOR_WHITE)
271        };
272
273        content_box((), || {
274            image_box(ImageBoxProps::colored(bg_color));
275
276            text_box(TextBoxProps {
277                text: label.to_owned(),
278                font: TextBoxFont {
279                    name: "roboto.ttf".to_owned(),
280                    size: 24.0,
281                },
282                color: text_color,
283                horizontal_align: TextBoxHorizontalAlign::Center,
284                vertical_align: TextBoxVerticalAlign::Middle,
285                ..Default::default()
286            });
287        });
288    })
289}
290
291#[derive(Default)]
292struct State {
293    players: HashMap<PeerId, Player>,
294    movement: CardinalInputCombinator,
295    exit: InputActionRef,
296}
297
298impl GameState for State {
299    fn enter(&mut self, mut context: GameContext) {
300        let move_left = InputActionRef::default();
301        let move_right = InputActionRef::default();
302        let move_up = InputActionRef::default();
303        let move_down = InputActionRef::default();
304        self.movement = CardinalInputCombinator::new(
305            move_left.clone(),
306            move_right.clone(),
307            move_up.clone(),
308            move_down.clone(),
309        );
310        context.input.push_mapping(
311            InputMapping::default()
312                .consume(InputConsume::Hit)
313                .action(
314                    VirtualAction::KeyButton(VirtualKeyCode::A),
315                    move_left.clone(),
316                )
317                .action(
318                    VirtualAction::KeyButton(VirtualKeyCode::D),
319                    move_right.clone(),
320                )
321                .action(VirtualAction::KeyButton(VirtualKeyCode::W), move_up.clone())
322                .action(
323                    VirtualAction::KeyButton(VirtualKeyCode::S),
324                    move_down.clone(),
325                )
326                .action(VirtualAction::KeyButton(VirtualKeyCode::Left), move_left)
327                .action(VirtualAction::KeyButton(VirtualKeyCode::Right), move_right)
328                .action(VirtualAction::KeyButton(VirtualKeyCode::Up), move_up)
329                .action(VirtualAction::KeyButton(VirtualKeyCode::Down), move_down)
330                .action(
331                    VirtualAction::KeyButton(VirtualKeyCode::Escape),
332                    self.exit.clone(),
333                ),
334        );
335
336        if let Some(multiplayer) = context.multiplayer_mut::<ClientServerMultiplayer>() {
337            multiplayer.process_lifecycle_events = true;
338            let _ = multiplayer.authority.create_peer(PlayerController::ROLE_ID);
339        } else {
340            *context.state_change = GameStateChange::Swap(Box::new(Lobby::default()));
341        }
342    }
343
344    fn exit(&mut self, context: GameContext) {
345        context.input.pop_mapping();
346
347        *context.network = GameNetwork::default();
348        *context.multiplayer_change = GameMultiplayerChange::Reset;
349    }
350
351    fn fixed_update(&mut self, context: GameContext, _delta_time: f32) {
352        if self.exit.get().is_pressed() {
353            *context.state_change = GameStateChange::Swap(Box::new(Lobby::default()));
354        }
355    }
356
357    fn draw(&mut self, context: GameContext) {
358        for player in self.players.values() {
359            if let Some(character) = &player.character {
360                character.draw(context.draw, context.graphics);
361            }
362        }
363    }
364
365    fn multiplayer_peer_added(&mut self, mut context: GameContext, peer: Peer) {
366        let (replica_added_sender, replica_added_receiver) = unbounded();
367        let (replica_removed_sender, replica_removed_receiver) = unbounded();
368
369        let mut controller = Controller::new(
370            peer,
371            PLAYER_EVENT_CHANNEL,
372            Some(PLAYER_CHANGE_CHANNEL),
373            None,
374            replica_added_sender,
375            replica_removed_sender,
376        )
377        .unwrap();
378
379        if let Some(multiplayer) = context.multiplayer_mut::<ClientServerMultiplayer>() {
380            let _ = controller.create_replica(&mut multiplayer.authority);
381        }
382
383        for player in self.players.values_mut() {
384            if let Some(character) = &mut player.character {
385                character.request_full_snapshot();
386            }
387        }
388
389        self.players.insert(
390            controller.info().peer_id,
391            Player {
392                controller,
393                character: None,
394                replica_added_receiver,
395                replica_removed_receiver,
396            },
397        );
398    }
399
400    fn multiplayer_peer_removed(&mut self, _context: GameContext, peer_id: PeerId) {
401        self.players.remove(&peer_id);
402    }
403
404    game_state_custom_event! {
405        trait(UniversalMultiplayerGameState)
406    }
407}
408
409impl UniversalMultiplayerGameState for State {
410    fn handle_inputs(&mut self, _context: GameContext, _current_tick: TimeStamp) {
411        let [x, y] = self.movement.get();
412        let movement = Vec2::new(x, y).try_normalized().unwrap_or_default() * SPEED;
413
414        for player in self.players.values_mut() {
415            if !player.controller.info().remote
416                && let Some(character) = &mut player.character
417            {
418                character.velocity_x.0 = movement.x;
419                character.velocity_y.0 = movement.y;
420            }
421        }
422    }
423
424    fn tick(
425        &mut self,
426        _context: GameContext,
427        _current_tick: TimeStamp,
428        delta_time: f32,
429        _resimulating: bool,
430    ) {
431        for player in self.players.values_mut() {
432            player.tick(delta_time);
433        }
434    }
435}
436
437struct Character {
438    peer_info: PeerInfo,
439    sprite: Sprite,
440    position_x: HashReplicated<RepF32>,
441    position_y: HashReplicated<RepF32>,
442    velocity_x: HashReplicated<RepF32>,
443    velocity_y: HashReplicated<RepF32>,
444}
445
446impl Character {
447    fn new(peer_info: PeerInfo, position: Vec2<f32>) -> Self {
448        Self {
449            peer_info,
450            sprite: Sprite::single(SpriteTexture {
451                sampler: "u_image".into(),
452                texture: TextureRef::name("ferris.png"),
453                filtering: GlowTextureFiltering::Linear,
454            })
455            .pivot(0.5.into())
456            .position(position)
457            .scale(0.25.into()),
458            position_x: HashReplicated::new(RepF32(position.x)),
459            position_y: HashReplicated::new(RepF32(position.y)),
460            velocity_x: Default::default(),
461            velocity_y: Default::default(),
462        }
463    }
464
465    fn tick_before(&mut self, delta_time: f32) {
466        self.position_x.0 += self.velocity_x.0 * delta_time;
467        self.position_y.0 += self.velocity_y.0 * delta_time;
468    }
469
470    fn tick_after(&mut self) {
471        self.sprite.transform.position.x = self.position_x.0;
472        self.sprite.transform.position.y = self.position_y.0;
473    }
474}
475
476impl Drawable for Character {
477    fn draw(&self, context: &mut DrawContext, graphics: &mut dyn GraphicsTarget<Vertex>) {
478        self.sprite.draw(context, graphics);
479
480        Text::new(ShaderRef::name("text"))
481            .text(self.peer_info.peer_id.id().to_string())
482            .font("roboto.ttf")
483            .size(20.0)
484            .position(Vec2::from(self.sprite.transform.position) - Vec2::new(0.0, 60.0))
485            .tint(if self.peer_info.remote {
486                Rgba::red()
487            } else {
488                Rgba::white()
489            })
490            .horizontal_align(HorizontalAlign::Center)
491            .vertical_align(VerticalAlign::Bottom)
492            .draw(context, graphics);
493    }
494}
495
496impl Puppetable for Character {
497    fn collect_changes(
498        &mut self,
499        mut collector: ReplicaCollectChanges,
500        full_snapshot: bool,
501    ) -> Result<(), Box<dyn Error>> {
502        if full_snapshot {
503            HashReplicated::mark_changed(&mut self.position_x);
504            HashReplicated::mark_changed(&mut self.position_y);
505        }
506
507        collector
508            .scope()
509            .maybe_collect_replicated::<0, _, _>(&self.position_x)?;
510        collector
511            .scope()
512            .maybe_collect_replicated::<1, _, _>(&self.position_y)?;
513        collector
514            .scope()
515            .maybe_collect_replicated::<2, _, _>(&self.velocity_x)?;
516        collector
517            .scope()
518            .maybe_collect_replicated::<3, _, _>(&self.velocity_y)?;
519
520        Ok(())
521    }
522
523    fn apply_changes(&mut self, mut applicator: ReplicaApplyChanges) -> Result<(), Box<dyn Error>> {
524        applicator
525            .scope()
526            .maybe_apply_replicated::<0, _, _>(&mut self.position_x)?;
527        applicator
528            .scope()
529            .maybe_apply_replicated::<1, _, _>(&mut self.position_y)?;
530        applicator
531            .scope()
532            .maybe_apply_replicated::<2, _, _>(&mut self.velocity_x)?;
533        applicator
534            .scope()
535            .maybe_apply_replicated::<3, _, _>(&mut self.velocity_y)?;
536
537        self.sprite.transform.position.x = self.position_x.0;
538        self.sprite.transform.position.y = self.position_y.0;
539
540        Ok(())
541    }
542}
543
544struct Player {
545    controller: Controller,
546    character: Option<Puppet<Character>>,
547    replica_added_receiver: Receiver<Replica>,
548    replica_removed_receiver: Receiver<ReplicaId>,
549}
550
551impl Player {
552    fn tick(&mut self, delta_time: f32) {
553        let _ = self.controller.maintain();
554
555        for replica in self.replica_added_receiver.iter() {
556            self.character = Some(Puppet::new(
557                replica,
558                Character::new(*self.controller.info(), 0.0.into()),
559            ));
560        }
561
562        for _ in self.replica_removed_receiver.iter() {
563            self.character = None;
564        }
565
566        if let Some(character) = &mut self.character {
567            character.tick_before(delta_time);
568        }
569
570        if let Some(puppet) = &mut self.character {
571            let _ = puppet.replicate();
572        }
573
574        if let Some(character) = &mut self.character {
575            character.tick_after();
576        }
577    }
578}
579
580struct PlayerController;
581
582impl TypedPeerRole for PlayerController {
583    const ROLE_ID: PeerRoleId = PLAYER_ROLE;
584}
585
586impl TypedPeer for PlayerController {
587    fn builder(builder: PeerBuilder) -> Result<PeerBuilder, Box<dyn Error>> {
588        if builder.info().remote {
589            Ok(builder
590                .bind_read_write::<PostcardCodec<ControllerEvent>, ControllerEvent>(
591                    PLAYER_EVENT_CHANNEL,
592                    ChannelMode::ReliableOrdered,
593                    None,
594                )
595                .bind_read::<ReplicationBuffer, ReplicationBuffer>(
596                    PLAYER_CHANGE_CHANNEL,
597                    ChannelMode::ReliableOrdered,
598                    None,
599                ))
600        } else {
601            Ok(builder
602                .bind_read_write::<PostcardCodec<ControllerEvent>, ControllerEvent>(
603                    PLAYER_EVENT_CHANNEL,
604                    ChannelMode::ReliableOrdered,
605                    None,
606                )
607                .bind_write::<ReplicationBuffer, ReplicationBuffer>(
608                    PLAYER_CHANGE_CHANNEL,
609                    ChannelMode::ReliableOrdered,
610                    None,
611                ))
612        }
613    }
614}