From 576eb9267da4fcc0b2c659b9b79405cebfa3838b Mon Sep 17 00:00:00 2001 From: mvlabat Date: Sun, 25 Apr 2021 18:37:07 +0300 Subject: [PATCH] Implement pausing client if connection is lost, add overlay UI --- libs/client_lib/src/lib.rs | 67 ++++++++++++++++++++++++++-- libs/client_lib/src/net.rs | 7 +++ libs/client_lib/src/ui/mod.rs | 1 + libs/client_lib/src/ui/overlay_ui.rs | 41 +++++++++++++++++ libs/server_lib/src/net.rs | 1 - libs/shared_lib/src/lib.rs | 24 +++++++--- 6 files changed, 132 insertions(+), 9 deletions(-) create mode 100644 libs/client_lib/src/ui/overlay_ui.rs diff --git a/libs/client_lib/src/lib.rs b/libs/client_lib/src/lib.rs index f730818b..e92ecdee 100644 --- a/libs/client_lib/src/lib.rs +++ b/libs/client_lib/src/lib.rs @@ -12,10 +12,11 @@ use bevy::{ component::ComponentId, entity::Entity, query::Access, - schedule::{ShouldRun, SystemStage}, + schedule::{ShouldRun, State, StateError, SystemStage}, system::{Commands, IntoSystem, Local, Res, ResMut, System, SystemId, SystemParam}, world::World, }, + log, math::Vec3, pbr::{Light, LightBundle}, render::entity::PerspectiveCameraBundle, @@ -24,8 +25,11 @@ use bevy::{ use bevy_egui::EguiPlugin; use bevy_networking_turbulence::LinkConditionerConfig; use mr_shared_lib::{ - framebuffer::FrameNumber, messages::PlayerNetId, net::ConnectionState, GameTime, - MuddleSharedPlugin, SimulationTime, SIMULATIONS_PER_SECOND, + framebuffer::FrameNumber, + messages::PlayerNetId, + net::{ConnectionState, ConnectionStatus}, + GameState, GameTime, MuddleSharedPlugin, SimulationTime, COMPONENT_FRAMEBUFFER_LIMIT, + SIMULATIONS_PER_SECOND, }; use std::{borrow::Cow, time::Instant}; @@ -50,6 +54,7 @@ impl Plugin for MuddleClientPlugin { let broadcast_updates_stage = SystemStage::parallel().with_system(send_network_updates.system()); let post_tick_stage = SystemStage::single_threaded() + .with_system(pause_simulation.system()) .with_system(control_ticking_speed.system()) .with_system(update_debug_ui_state.system()); @@ -59,6 +64,7 @@ impl Plugin for MuddleClientPlugin { .init_resource::() .init_resource::() // Startup systems. + .add_startup_system(init_state.system()) .add_startup_system(basic_scene.system()) // Game. .add_plugin(MuddleSharedPlugin::new( @@ -77,6 +83,7 @@ impl Plugin for MuddleClientPlugin { // Egui. .add_system(ui::debug_ui::update_ui_scale_factor.system()) .add_system(ui::debug_ui::debug_ui.system()) + .add_system(ui::overlay_ui::connection_status_overlay.system()) .add_system(ui::debug_ui::inspect_object.system()); let world = builder.world_mut(); @@ -125,6 +132,8 @@ impl InitialRtt { } #[derive(Default)] +/// This resource is used for adjusting game speed. +/// If the estimated `frame_number` falls too much behind, the game is paused. pub struct EstimatedServerTime { pub updated_at: FrameNumber, pub frame_number: FrameNumber, @@ -158,6 +167,58 @@ pub struct CurrentPlayerNetId(pub Option); pub struct MainCameraEntity(pub Entity); +fn init_state(mut game_state: ResMut>) { + log::info!("Pausing the game"); + game_state.push(GameState::Paused).unwrap(); +} + +fn pause_simulation( + mut game_state: ResMut>, + connection_state: Res, + game_time: Res, + estimated_server_time: Res, +) { + let is_connected = matches!(connection_state.status(), ConnectionStatus::Connected); + + let has_server_updates = game_time + .frame_number + .value() + .saturating_sub(estimated_server_time.frame_number.value()) + < COMPONENT_FRAMEBUFFER_LIMIT / 2; + + // We always assume that `GameState::Playing` is the initial state and `GameState::Paused` + // is pushed to the top of the stack. + if let GameState::Paused = game_state.current() { + if is_connected && has_server_updates { + log::info!("Unpausing the game"); + game_state.pop().unwrap(); + return; + } + } + + // We always assume that `GameState::Playing` is the initial state and `GameState::Paused` + // is pushed to the top of the stack. + if let GameState::Paused = game_state.current() { + if is_connected && has_server_updates { + log::info!("Unpausing the game"); + game_state.pop().unwrap(); + } + } + + if !is_connected || !has_server_updates { + let result = game_state.push(GameState::Paused); + match result { + Ok(()) => { + log::info!("Pausing the game"); + } + Err(StateError::AlreadyInState) | Err(StateError::StateAlreadyQueued) => { + // It's ok. Bevy won't let us push duplicate values - that's what we rely on. + } + Err(StateError::StackEmpty) => unreachable!(), + } + } +} + fn basic_scene(mut commands: Commands) { // Add entities to the scene. commands.spawn_bundle(LightBundle { diff --git a/libs/client_lib/src/net.rs b/libs/client_lib/src/net.rs index 3c4ae3a5..be6f485f 100644 --- a/libs/client_lib/src/net.rs +++ b/libs/client_lib/src/net.rs @@ -345,6 +345,13 @@ pub fn send_network_updates( player_registry: Res>, player_update_params: PlayerUpdateParams, ) { + if !matches!( + network_params.connection_state.status(), + ConnectionStatus::Connected + ) { + return; + } + log::trace!("Broadcast updates for frame {}", time.frame_number); let (connection_handle, address) = match network_params.net.connections.iter_mut().next() { Some((&handle, connection)) => (handle, connection.remote_address()), diff --git a/libs/client_lib/src/ui/mod.rs b/libs/client_lib/src/ui/mod.rs index 6ec47f12..357add9c 100644 --- a/libs/client_lib/src/ui/mod.rs +++ b/libs/client_lib/src/ui/mod.rs @@ -2,6 +2,7 @@ use bevy_egui::egui::{self, Ui}; use mr_shared_lib::game::components::{PlayerDirection, Position}; pub mod debug_ui; +pub mod overlay_ui; pub trait MuddleInspectable { fn inspect(&self, ui: &mut Ui); diff --git a/libs/client_lib/src/ui/overlay_ui.rs b/libs/client_lib/src/ui/overlay_ui.rs new file mode 100644 index 00000000..a33d10e4 --- /dev/null +++ b/libs/client_lib/src/ui/overlay_ui.rs @@ -0,0 +1,41 @@ +use bevy::{ + ecs::system::{Res, ResMut}, + window::Windows, +}; +use bevy_egui::{egui, EguiContext}; +use mr_shared_lib::net::{ConnectionState, ConnectionStatus}; + +pub fn connection_status_overlay( + egui_context: ResMut, + connection_state: Res, + windows: Res, +) { + if let ConnectionStatus::Connected = connection_state.status() { + return; + } + + let primary_window = windows.get_primary().unwrap(); + let window_width = 200.0; + let window_height = 100.0; + + let ctx = egui_context.ctx(); + egui::CentralPanel::default() + .frame(egui::Frame::none().fill(egui::Color32::from_black_alpha(200))) + .show(ctx, |ui| { + egui::Window::new("connection status") + .title_bar(false) + .collapsible(false) + .resizable(false) + .fixed_pos(egui::Pos2::new( + (primary_window.physical_width() as f32 - window_width) / 2.0, + (primary_window.physical_height() as f32 - window_height) / 2.0, + )) + .fixed_size(egui::Vec2::new(window_width, window_height)) + .show(ui.ctx(), |ui| { + ui.centered_and_justified(|ui| { + ui.style_mut().body_text_style = egui::TextStyle::Heading; + ui.label(format!("{:?}", connection_state.status())); + }); + }); + }); +} diff --git a/libs/server_lib/src/net.rs b/libs/server_lib/src/net.rs index be5248c4..fec84465 100644 --- a/libs/server_lib/src/net.rs +++ b/libs/server_lib/src/net.rs @@ -397,7 +397,6 @@ pub fn send_network_updates( player_entities: Query<(Entity, &Position, &PlayerDirection, &Spawned)>, players_registry: Res>, ) { - // TODO! remove players from Res> when they are despawned. log::trace!("Sending network updates (frame: {})", time.frame_number); broadcast_start_game_messages( diff --git a/libs/shared_lib/src/lib.rs b/libs/shared_lib/src/lib.rs index 6428ecb3..ecaae67e 100644 --- a/libs/shared_lib/src/lib.rs +++ b/libs/shared_lib/src/lib.rs @@ -200,7 +200,10 @@ impl> Plugin for MuddleSharedPlugin { builder.add_stage_before( stage::MAIN_SCHEDULE, stage::READ_INPUT_UPDATES, - SystemStage::parallel().with_system(read_movement_updates.system()), + SystemStage::parallel().with_system_set( + SystemSet::on_update(GameState::Playing) + .with_system(read_movement_updates.system()), + ), ); builder.add_stage_before( stage::READ_INPUT_UPDATES, @@ -210,6 +213,10 @@ impl> Plugin for MuddleSharedPlugin { .expect("Can't initialize the plugin more than once"), ); + // Is `GameState::Paused` for client (see `init_state`). + builder.add_state(GameState::Playing); + builder.add_state_to_stage(stage::READ_INPUT_UPDATES, GameState::Playing); + builder.add_startup_system(network_setup.system()); let resources = builder.world_mut(); @@ -254,9 +261,12 @@ impl Plugin for RapierResourcesPlugin { } } -// TODO: split into two resources for simulation and game frames to live separately? -// This will probably help with avoiding bugs where we mistakenly use game frame -// instead of simulation frame. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum GameState { + Paused, + Playing, +} + #[derive(Default, Clone, PartialEq, Debug)] pub struct GameTime { pub generation: usize, @@ -413,6 +423,7 @@ impl Default for SimulationTickRunCriteria { impl SimulationTickRunCriteria { fn prepare_system( mut state: Local, + game_state: Res>, game_time: Res, simulation_time: Res, ) -> ShouldRun { @@ -422,6 +433,7 @@ impl SimulationTickRunCriteria { state.last_game_frame = Some(game_time.frame_number); } else if state.last_player_frame == simulation_time.player_frame && state.last_server_frame == simulation_time.server_frame + && game_state.current() == &GameState::Playing { panic!( "Simulation frame hasn't advanced: {}, {}", @@ -431,7 +443,9 @@ impl SimulationTickRunCriteria { state.last_player_frame = simulation_time.player_frame; state.last_server_frame = simulation_time.server_frame; - if state.last_player_frame <= game_time.frame_number { + if state.last_player_frame <= game_time.frame_number + && game_state.current() == &GameState::Playing + { trace!( "Run and loop a simulation schedule (simulation: {}, game {})", simulation_time.player_frame,