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 }