Skip to content

Commit

Permalink
Implement pausing client if connection is lost, add overlay UI
Browse files Browse the repository at this point in the history
  • Loading branch information
vladbat00 committed Apr 25, 2021
1 parent f037095 commit 576eb92
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 9 deletions.
67 changes: 64 additions & 3 deletions libs/client_lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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};

Expand All @@ -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());

Expand All @@ -59,6 +64,7 @@ impl Plugin for MuddleClientPlugin {
.init_resource::<WindowInnerSize>()
.init_resource::<input::MousePosition>()
// Startup systems.
.add_startup_system(init_state.system())
.add_startup_system(basic_scene.system())
// Game.
.add_plugin(MuddleSharedPlugin::new(
Expand All @@ -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();
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -158,6 +167,58 @@ pub struct CurrentPlayerNetId(pub Option<PlayerNetId>);

pub struct MainCameraEntity(pub Entity);

fn init_state(mut game_state: ResMut<State<GameState>>) {
log::info!("Pausing the game");
game_state.push(GameState::Paused).unwrap();
}

fn pause_simulation(
mut game_state: ResMut<State<GameState>>,
connection_state: Res<ConnectionState>,
game_time: Res<GameTime>,
estimated_server_time: Res<EstimatedServerTime>,
) {
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 {
Expand Down
7 changes: 7 additions & 0 deletions libs/client_lib/src/net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,13 @@ pub fn send_network_updates(
player_registry: Res<EntityRegistry<PlayerNetId>>,
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()),
Expand Down
1 change: 1 addition & 0 deletions libs/client_lib/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
41 changes: 41 additions & 0 deletions libs/client_lib/src/ui/overlay_ui.rs
Original file line number Diff line number Diff line change
@@ -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<EguiContext>,
connection_state: Res<ConnectionState>,
windows: Res<Windows>,
) {
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()));
});
});
});
}
1 change: 0 additions & 1 deletion libs/server_lib/src/net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,6 @@ pub fn send_network_updates(
player_entities: Query<(Entity, &Position, &PlayerDirection, &Spawned)>,
players_registry: Res<EntityRegistry<PlayerNetId>>,
) {
// TODO! remove players from Res<HashMap<PlayerNetId, Player>> when they are despawned.
log::trace!("Sending network updates (frame: {})", time.frame_number);

broadcast_start_game_messages(
Expand Down
24 changes: 19 additions & 5 deletions libs/shared_lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,10 @@ impl<S: System<In = (), Out = ShouldRun>> Plugin for MuddleSharedPlugin<S> {
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,
Expand All @@ -210,6 +213,10 @@ impl<S: System<In = (), Out = ShouldRun>> Plugin for MuddleSharedPlugin<S> {
.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();
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -413,6 +423,7 @@ impl Default for SimulationTickRunCriteria {
impl SimulationTickRunCriteria {
fn prepare_system(
mut state: Local<SimulationTickRunCriteriaState>,
game_state: Res<State<GameState>>,
game_time: Res<GameTime>,
simulation_time: Res<SimulationTime>,
) -> ShouldRun {
Expand All @@ -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: {}, {}",
Expand All @@ -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,
Expand Down

0 comments on commit 576eb92

Please sign in to comment.