placeholder-project

Temp project
git clone git://archive.git.mtrnord.blog/MTRNord/placeholder-project.git
Log | Files | Refs

client.rs (7974B)


      1 use std::net::SocketAddr;
      2 
      3 use bevy::app::PluginGroupBuilder;
      4 use bevy::prelude::*;
      5 use bevy::utils::Duration;
      6 
      7 use bevy_ecs_ldtk::LdtkWorldBundle;
      8 use lightyear::prelude::client::*;
      9 use lightyear::prelude::*;
     10 
     11 use crate::player::{AnimationIndices, AnimationTimer, PlayerBundle};
     12 
     13 use super::protocol::{
     14     protocol, ClientMut, Components, Inputs, MatrixRPGGameProto, PlayerId, PlayerPosition,
     15 };
     16 use super::{shared_config, shared_movement_behaviour, SharedSettings};
     17 
     18 pub struct ClientPluginGroup {
     19     lightyear: ClientPlugin<MatrixRPGGameProto>,
     20 }
     21 
     22 impl ClientPluginGroup {
     23     pub(crate) fn new(
     24         client_id: u64,
     25         server_addr: SocketAddr,
     26         transport_config: TransportConfig,
     27         shared_settings: SharedSettings,
     28     ) -> ClientPluginGroup {
     29         let auth = Authentication::Manual {
     30             server_addr,
     31             client_id,
     32             private_key: shared_settings.private_key,
     33             protocol_id: shared_settings.protocol_id,
     34         };
     35         let link_conditioner = LinkConditionerConfig {
     36             incoming_latency: Duration::from_millis(200),
     37             incoming_jitter: Duration::from_millis(20),
     38             incoming_loss: 0.05,
     39         };
     40         let config = ClientConfig {
     41             shared: shared_config(),
     42             net: NetConfig::Netcode {
     43                 auth,
     44                 config: NetcodeConfig::default(),
     45                 io: IoConfig::from_transport(transport_config).with_conditioner(link_conditioner),
     46             },
     47             interpolation: InterpolationConfig {
     48                 delay: InterpolationDelay::default(),
     49                 custom_interpolation_logic: false,
     50             },
     51             ..default()
     52         };
     53         let plugin_config = PluginConfig::new(config, protocol());
     54         ClientPluginGroup {
     55             lightyear: ClientPlugin::new(plugin_config),
     56         }
     57     }
     58 }
     59 
     60 impl PluginGroup for ClientPluginGroup {
     61     fn build(self) -> PluginGroupBuilder {
     62         PluginGroupBuilder::start::<Self>()
     63             .add(self.lightyear)
     64             .add(MatrixRPGClientPlugin)
     65             .add(super::SharedPlugin)
     66     }
     67 }
     68 
     69 pub struct MatrixRPGClientPlugin;
     70 
     71 impl Plugin for MatrixRPGClientPlugin {
     72     fn build(&self, app: &mut App) {
     73         app.add_systems(Startup, init);
     74         app.add_systems(PreUpdate, handle_connection.after(MainSet::ReceiveFlush));
     75         // Inputs have to be buffered in the FixedPreUpdate schedule
     76         app.add_systems(
     77             FixedPreUpdate,
     78             buffer_input.in_set(InputSystemSet::BufferInputs),
     79         );
     80         app.add_systems(FixedUpdate, player_movement);
     81         app.add_systems(Update, spawn_player);
     82     }
     83 }
     84 
     85 // Startup system for the client
     86 pub(crate) fn init(mut commands: Commands, mut client: ClientMut, asset_server: Res<AssetServer>) {
     87     let mut camera = Camera2dBundle::default();
     88     camera.projection.scale = 0.5;
     89     camera.transform.translation.x += 1920.0 / 4.0;
     90     camera.transform.translation.y += 1080.0 / 4.0;
     91     commands.spawn(camera);
     92 
     93     commands.spawn(LdtkWorldBundle {
     94         ldtk_handle: asset_server.load("matrix_office.ldtk"),
     95         ..Default::default()
     96     });
     97 
     98     let _ = client.connect();
     99 }
    100 
    101 pub(crate) fn handle_connection(mut commands: Commands, metadata: Res<GlobalMetadata>) {
    102     // the `GlobalMetadata` resource holds metadata related to the client
    103     // once the connection is established.
    104     if metadata.is_changed() {
    105         if let Some(client_id) = metadata.client_id {
    106             commands.spawn(TextBundle::from_section(
    107                 format!("Client {}", client_id),
    108                 TextStyle {
    109                     font_size: 30.0,
    110                     color: Color::WHITE,
    111                     ..default()
    112                 },
    113             ));
    114         }
    115     }
    116 }
    117 
    118 // System that reads from peripherals and adds inputs to the buffer
    119 pub(crate) fn buffer_input(mut client: ClientMut, keypress: Res<ButtonInput<KeyCode>>) {
    120     let mut direction = super::protocol::Direction {
    121         up: false,
    122         down: false,
    123         left: false,
    124         right: false,
    125     };
    126     if keypress.pressed(KeyCode::KeyW) || keypress.pressed(KeyCode::ArrowUp) {
    127         direction.up = true;
    128     }
    129     if keypress.pressed(KeyCode::KeyS) || keypress.pressed(KeyCode::ArrowDown) {
    130         direction.down = true;
    131     }
    132     if keypress.pressed(KeyCode::KeyA) || keypress.pressed(KeyCode::ArrowLeft) {
    133         direction.left = true;
    134     }
    135     if keypress.pressed(KeyCode::KeyD) || keypress.pressed(KeyCode::ArrowRight) {
    136         direction.right = true;
    137     }
    138     if !direction.is_none() {
    139         return client.add_input(Inputs::Direction(direction));
    140     }
    141     if keypress.pressed(KeyCode::Space) {
    142         return client.add_input(Inputs::Spawn);
    143     }
    144     // info!("Sending input: {:?} on tick: {:?}", &input, client.tick());
    145     client.add_input(Inputs::None)
    146 }
    147 
    148 // The client input only gets applied to predicted entities that we own
    149 // This works because we only predict the user's controlled entity.
    150 // If we were predicting more entities, we would have to only apply movement to the player owned one.
    151 #[allow(clippy::type_complexity)]
    152 fn player_movement(
    153     mut position_query: Query<
    154         (&mut Transform, &mut PlayerPosition),
    155         (With<Predicted>, With<PlayerId>, Without<Camera>),
    156     >,
    157     mut cameras: Query<&mut Transform, With<Camera>>,
    158     mut input_reader: EventReader<InputEvent<Inputs>>,
    159 ) {
    160     if <Components as SyncMetadata<PlayerPosition>>::mode() != ComponentSyncMode::Full {
    161         return;
    162     }
    163     for input in input_reader.read() {
    164         if let Some(input) = input.input() {
    165             for (mut transform, position) in position_query.iter_mut() {
    166                 // NOTE: be careful to directly pass Mut<PlayerPosition>
    167                 // getting a mutable reference triggers change detection, unless you use `as_deref_mut()`
    168                 transform.translation = Vec3::new(position.x, position.y, transform.translation.z);
    169                 let pos = transform.translation;
    170                 for mut transform in &mut cameras {
    171                     transform.translation.x = pos.x;
    172                     transform.translation.y = pos.y;
    173                 }
    174                 shared_movement_behaviour(position, input);
    175             }
    176         }
    177     }
    178 }
    179 
    180 /// Spawn a player when the space command is pressed
    181 fn spawn_player(
    182     mut commands: Commands,
    183     players: Query<&PlayerId, With<PlayerPosition>>,
    184     metadata: Res<GlobalMetadata>,
    185     asset_server: Res<AssetServer>,
    186     mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
    187 ) {
    188     // return early if we still don't have access to the client id
    189     let Some(client_id) = metadata.client_id else {
    190         return;
    191     };
    192 
    193     for player_id in players.iter() {
    194         if player_id.0 == client_id {
    195             return;
    196         }
    197     }
    198     info!("got spawn input");
    199 
    200     let texture = asset_server.load("tilesets/user.png");
    201     let layout = TextureAtlasLayout::from_grid(Vec2::new(16.0, 16.0), 8, 8, None, None);
    202     let texture_atlas_layout = texture_atlas_layouts.add(layout);
    203     // Use only the subset of sprites in the sheet that make up the run animation
    204     let animation_indices = AnimationIndices { first: 0, last: 3 };
    205     let atlas = TextureAtlas {
    206         layout: texture_atlas_layout.clone(),
    207         index: animation_indices.first,
    208     };
    209     commands.spawn((
    210         PlayerBundle::new(client_id, Vec2::ZERO),
    211         AnimationTimer(Timer::from_seconds(0.3, TimerMode::Repeating)),
    212         animation_indices,
    213         SpriteSheetBundle {
    214             transform: Transform::from_xyz(0., 0., 17.).with_scale(Vec3::splat(2.0)),
    215             texture: texture.clone(),
    216             atlas,
    217             ..default()
    218         },
    219         // IMPORTANT: this lets the server know that the entity is pre-predicted
    220         // when the server replicates this entity; we will get a Confirmed entity which will use this entity
    221         // as the Predicted version
    222         ShouldBePredicted::default(),
    223     ));
    224 }