Skip to content

Commit

Permalink
Implement loading level objects on game start
Browse files Browse the repository at this point in the history
  • Loading branch information
vladbat00 committed Jan 23, 2023
1 parent 8882126 commit 9179769
Show file tree
Hide file tree
Showing 15 changed files with 337 additions and 179 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

96 changes: 43 additions & 53 deletions libs/client_lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use bevy::{
diagnostic::FrameTimeDiagnosticsPlugin,
ecs::{
entity::Entity,
schedule::{IntoSystemDescriptor, ShouldRun, State, StateError, SystemStage},
schedule::{IntoSystemDescriptor, ShouldRun, SystemStage},
system::{Commands, IntoSystem, Local, Res, ResMut, Resource, SystemParam},
},
hierarchy::BuildChildren,
Expand All @@ -52,8 +52,8 @@ use mr_shared_lib::{
game::client_factories::VisibilitySettings,
messages::{EntityNetId, PlayerNetId},
net::{ConnectionState, ConnectionStatus, MessageId},
GameState, GameTime, MuddleSharedPlugin, SimulationTime, COMPONENT_FRAMEBUFFER_LIMIT,
SIMULATIONS_PER_SECOND, TICKS_PER_NETWORK_BROADCAST,
AppState, GameSessionState, GameTime, MuddleSharedPlugin, SimulationTime,
COMPONENT_FRAMEBUFFER_LIMIT, SIMULATIONS_PER_SECOND, TICKS_PER_NETWORK_BROADCAST,
};
use std::{marker::PhantomData, net::SocketAddr};
use url::Url;
Expand All @@ -77,9 +77,9 @@ pub struct MuddleClientPlugin;
impl Plugin for MuddleClientPlugin {
fn build(&self, app: &mut App) {
let input_stage = SystemStage::single_threaded()
// Processing network events should happen before tracking input
// because we reset current's player inputs on each delta update.
.with_system(maintain_connection_system)
// Processing network events should happen before tracking input:
// we rely on resetting current's player inputs on each delta update message (event).
.with_system(process_network_events_system.after(maintain_connection_system))
.with_system(input::track_input_events_system.after(process_network_events_system))
.with_system(input::cast_mouse_ray_system.after(input::track_input_events_system));
Expand Down Expand Up @@ -144,28 +144,30 @@ impl Plugin for MuddleClientPlugin {
.add_system(process_control_points_input_system.after("builder_system_set"))
.add_system(spawn_control_points_system.after("builder_system_set"));

let world = &mut app.world;
world
// There's also `GameSessionState`, which is added by `MuddleSharedPlugin`.
app.add_state(AppState::Loading);

app.world
.get_resource_mut::<WorldInspectorParams>()
.unwrap()
.enabled = false;
world.get_resource_or_insert_with(InitialRtt::default);
world.get_resource_or_insert_with(EstimatedServerTime::default);
world.get_resource_or_insert_with(GameTicksPerSecond::default);
world.get_resource_or_insert_with(TargetFramesAhead::default);
world.get_resource_or_insert_with(DelayServerTime::default);
world.get_resource_or_insert_with(ui::debug_ui::DebugUiState::default);
world.get_resource_or_insert_with(CurrentPlayerNetId::default);
world.get_resource_or_insert_with(ConnectionState::default);
world.get_resource_or_insert_with(PlayerRequestsQueue::default);
world.get_resource_or_insert_with(EditedLevelObject::default);
world.get_resource_or_insert_with(LevelObjectRequestsQueue::default);
world.get_resource_or_insert_with(LevelObjectCorrelations::default);
world.get_resource_or_insert_with(MouseRay::default);
world.get_resource_or_insert_with(MouseWorldPosition::default);
world.get_resource_or_insert_with(VisibilitySettings::default);
world.get_resource_or_insert_with(ServerToConnect::default);
world.get_resource_or_insert_with(OfflineAuthConfig::default);
app.init_resource::<InitialRtt>();
app.init_resource::<EstimatedServerTime>();
app.init_resource::<GameTicksPerSecond>();
app.init_resource::<TargetFramesAhead>();
app.init_resource::<DelayServerTime>();
app.init_resource::<ui::debug_ui::DebugUiState>();
app.init_resource::<CurrentPlayerNetId>();
app.init_resource::<ConnectionState>();
app.init_resource::<PlayerRequestsQueue>();
app.init_resource::<EditedLevelObject>();
app.init_resource::<LevelObjectRequestsQueue>();
app.init_resource::<LevelObjectCorrelations>();
app.init_resource::<MouseRay>();
app.init_resource::<MouseWorldPosition>();
app.init_resource::<VisibilitySettings>();
app.init_resource::<ServerToConnect>();
app.init_resource::<OfflineAuthConfig>();
}
}

Expand Down Expand Up @@ -297,7 +299,8 @@ pub struct MainCameraPivotEntity(pub Entity);
pub struct MainCameraEntity(pub Entity);

fn pause_simulation_system(
mut game_state: ResMut<State<GameState>>,
mut commands: Commands,
game_state: Res<CurrentState<GameSessionState>>,
connection_state: Res<ConnectionState>,
game_time: Res<GameTime>,
estimated_server_time: Res<EstimatedServerTime>,
Expand All @@ -312,37 +315,24 @@ fn pause_simulation_system(
.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 let GameSessionState::Paused = game_state.0 {
if is_connected && has_server_updates {
let result = game_state.pop();
match result {
Ok(()) => {
log::info!("Unpausing the game");
}
Err(StateError::StateAlreadyQueued) => {
// TODO: investigate why this runs more than once before
// changing the state sometimes.
}
Err(StateError::StackEmpty | StateError::AlreadyInState) => unreachable!(),
}
log::info!(
"Changing the game session state to {:?}",
GameSessionState::Playing
);
commands.insert_resource(NextState(GameSessionState::Playing));
return;
}
}

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!(),
}
// We can pause the game only when we are actually playing (not loading).
if matches!(game_state.0, GameSessionState::Playing) && (!is_connected || !has_server_updates) {
log::info!(
"Changing the game session state to {:?}",
GameSessionState::Paused
);
commands.insert_resource(NextState(GameSessionState::Paused));
}
}

Expand Down Expand Up @@ -498,7 +488,7 @@ fn net_adaptive_run_criteria(
mut state: Local<NetAdaptiveRunCriteriaState>,
time: Res<Time>,
game_ticks_per_second: Res<GameTicksPerSecond>,
game_state: Res<State<GameState>>,
game_state: Res<CurrentState<GameSessionState>>,
) -> ShouldRun {
#[cfg(feature = "profiler")]
puffin::profile_function!();
Expand All @@ -515,7 +505,7 @@ fn net_adaptive_run_criteria(

// In the scenario when a client was frozen (minimized, for example) and it got
// disconnected, we don't want to replay all the accumulated frames.
if game_state.current() != &GameState::Playing {
if game_state.0 != GameSessionState::Playing {
state.accumulator = state.accumulator.min(1.0);
}

Expand Down
70 changes: 47 additions & 23 deletions libs/client_lib/src/net/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ use auth::{AuthMessage, AuthRequest};
use bevy::{ecs::system::SystemParam, log, prelude::*, utils::Instant};
use bevy_disturbulence::{NetworkEvent, NetworkResource};
use futures::{select, FutureExt};
use iyes_loopless::state::NextState;
use mr_messages_lib::{MatchmakerMessage, MatchmakerRequest, Server};
use mr_shared_lib::{
framebuffer::{FrameNumber, Framebuffer},
game::{
commands::{
DeferredQueue, DespawnLevelObject, DespawnPlayer, DespawnReason, RestartGame,
SpawnPlayer, SwitchPlayerRole, UpdateLevelObject,
DeferredQueue, DespawnLevelObject, DespawnPlayer, DespawnReason, SpawnPlayer,
SwitchPlayerRole, UpdateLevelObject,
},
components::{PlayerDirection, Spawned},
},
Expand All @@ -35,7 +36,8 @@ use mr_shared_lib::{
},
player::{Player, PlayerDirectionUpdate, PlayerRole, PlayerUpdates, Players},
registry::EntityRegistry,
GameTime, SimulationTime, COMPONENT_FRAMEBUFFER_LIMIT, SIMULATIONS_PER_SECOND,
AppState, GameSessionState, GameTime, LevelObjectsToSpawnToLoad, SimulationTime,
COMPONENT_FRAMEBUFFER_LIMIT, SIMULATIONS_PER_SECOND,
};
use std::{
future::Future,
Expand Down Expand Up @@ -69,7 +71,6 @@ pub struct UpdateParams<'w, 's> {
delay_server_time: ResMut<'w, DelayServerTime>,
initial_rtt: ResMut<'w, InitialRtt>,
player_updates: ResMut<'w, PlayerUpdates>,
restart_game_commands: ResMut<'w, DeferredQueue<RestartGame>>,
level_object_correlations: ResMut<'w, LevelObjectCorrelations>,
spawn_level_object_commands: ResMut<'w, DeferredQueue<UpdateLevelObject>>,
despawn_level_object_commands: ResMut<'w, DeferredQueue<DespawnLevelObject>>,
Expand Down Expand Up @@ -281,12 +282,13 @@ where
pub struct MatchmakerParams<'w, 's> {
matchmaker_state: Option<ResMut<'w, MatchmakerState>>,
server_to_connect: ResMut<'w, ServerToConnect>,
main_menu_ui_channels: Option<Res<'w, MainMenuUiChannels>>,
main_menu_ui_channels: Option<ResMut<'w, MainMenuUiChannels>>,
#[system_param(ignore)]
marker: PhantomData<&'s ()>,
}

pub fn process_network_events_system(
mut commands: Commands,
mut network_params: NetworkParams,
mut network_events: EventReader<NetworkEvent>,
mut current_player_net_id: ResMut<CurrentPlayerNetId>,
Expand Down Expand Up @@ -399,14 +401,26 @@ pub fn process_network_events_system(
},
));

// This seems to be the most reliable place to do this. `StartGame` might come
// after the first `DeltaUpdate`, so it's not super reliable to restart a game
// there. `Handshake`, on the contrary, always comes before both `DeltaUpdate`
// and `StartGame`. Restarting on disconnect might work just fine too, but I
current_player_net_id.0 = None;
// This seems to be the most reliable place to switch the sate. `StartGame`
// might come after the first `DeltaUpdate`, so it's not
// super reliable to reset the game world there (which is implied by entering
// the `GameSessionState::Loading` state. `Handshake`, on
// the contrary, always comes before both `DeltaUpdate`
// and `StartGame`. Resetting on disconnect might work just fine too, but I
// thought that `Handshake` probably comes with less edge-cases, since we
// always get it before starting the game.
current_player_net_id.0 = None;
update_params.restart_game_commands.push(RestartGame);
log::info!("Changing the app state to {:?}", AppState::Playing);
commands.insert_resource(NextState(AppState::Playing));
// On game start, `GameSessionState::Loading` is likely current state as
// well, we rely on `iyes_loopless` behaviour to run the
// enter stage regardless of whether current state
// equals next one.
log::info!(
"Changing the game session state to {:?}",
GameSessionState::Loading
);
commands.insert_resource(NextState(GameSessionState::Loading));
}
UnreliableServerMessage::DeltaUpdate(update) => {
let mut skip_update = false;
Expand Down Expand Up @@ -548,6 +562,10 @@ pub fn process_network_events_system(
.connection_state
.set_status(ConnectionStatus::Connecting);
}
ReliableServerMessage::Loading => {
// TODO: reflect this message in the UI.
log::info!("The server is loading...");
}
ReliableServerMessage::StartGame(start_game) => {
let expected_handshake_id =
network_params.connection_state.handshake_id - MessageId::new(1);
Expand All @@ -569,6 +587,7 @@ pub fn process_network_events_system(
start_game.game_state.frame_number
);
process_start_game_message(
&mut commands,
start_game,
&mut network_params.connection_state,
&mut current_player_net_id,
Expand Down Expand Up @@ -701,9 +720,7 @@ pub fn process_network_events_system(
pub fn maintain_connection_system(
time: Res<GameTime>,
client_config: Res<MuddleClientConfig>,
matchmaker_state: Option<ResMut<MatchmakerState>>,
matchmaker_channels: Option<ResMut<MainMenuUiChannels>>,
mut server_to_connect: ResMut<ServerToConnect>,
mut matchmaker_params: MatchmakerParams,
mut network_params: NetworkParams,
mut initial_rtt: ResMut<InitialRtt>,
) {
Expand All @@ -713,18 +730,20 @@ pub fn maintain_connection_system(
if matches!(
network_params.connection_state.status(),
ConnectionStatus::Connected
) && server_to_connect.is_some()
) && matchmaker_params.server_to_connect.is_some()
{
**server_to_connect = None;
if let Some(matchmaker_channels) = matchmaker_channels.as_ref() {
**matchmaker_params.server_to_connect = None;
if let Some(matchmaker_channels) = matchmaker_params.main_menu_ui_channels.as_ref() {
matchmaker_channels
.connection_request_tx
.send(false)
.expect("Failed to write to a channel (matchmaker connection request)");
}
}

let mut matchmaker = matchmaker_state.zip(matchmaker_channels);
let mut matchmaker = matchmaker_params
.matchmaker_state
.zip(matchmaker_params.main_menu_ui_channels);
if let Some((matchmaker_state, matchmaker_channels)) = matchmaker.as_mut() {
loop {
match matchmaker_channels.status_rx.try_recv() {
Expand All @@ -746,15 +765,15 @@ pub fn maintain_connection_system(
);

// TODO: if a client isn't getting any updates, we may also want to pause the
// game and wait for some time for a server to respond.
// game and wait for some time for a server to respond.

let connection_timeout = Instant::now().duration_since(
let connection_has_timed_out = Instant::now().duration_since(
network_params
.connection_state
.last_valid_message_received_at,
) > Duration::from_millis(CONNECTION_TIMEOUT_MILLIS);

if connection_timeout && !connection_is_uninitialized {
if connection_has_timed_out && !connection_is_uninitialized {
log::warn!("Connection timeout, resetting");
}

Expand All @@ -779,7 +798,7 @@ pub fn maintain_connection_system(
);
}

if !connection_is_uninitialized && connection_timeout
if !connection_is_uninitialized && connection_has_timed_out
|| is_falling_behind
|| matches!(
network_params.connection_state.status(),
Expand All @@ -793,6 +812,9 @@ pub fn maintain_connection_system(
.set_status(ConnectionStatus::Uninitialized);
}

// TODO! so I left off after just finishing with the loading states, need to
// sort out the menu app state and running conditions

if network_params.net.connections.is_empty() {
let addr = if let Some((matchmaker_state, matchmaker_channels)) = matchmaker.as_mut() {
if matches!(matchmaker_state.status, TcpConnectionStatus::Disconnected) {
Expand All @@ -804,7 +826,7 @@ pub fn maintain_connection_system(
return;
}

let Some(server) = &**server_to_connect else {
let Some(server) = &**matchmaker_params.server_to_connect else {
return;
};
log::info!("Connecting to {}: {}", server.name, server.addr);
Expand Down Expand Up @@ -1250,6 +1272,7 @@ fn sync_clock(
}

fn process_start_game_message(
commands: &mut Commands,
start_game: StartGame,
connection_state: &mut ConnectionState,
current_player_net_id: &mut CurrentPlayerNetId,
Expand Down Expand Up @@ -1349,6 +1372,7 @@ fn process_start_game_message(
);
}
}
commands.insert_resource(LevelObjectsToSpawnToLoad(start_game.objects.len()));
for spawn_level_object in start_game.objects {
update_params
.spawn_level_object_commands
Expand Down
Loading

0 comments on commit 9179769

Please sign in to comment.