diff --git a/src/client/chat/systems.rs b/src/client/chat/systems.rs index b71de0c9..b2aa491f 100644 --- a/src/client/chat/systems.rs +++ b/src/client/chat/systems.rs @@ -55,21 +55,23 @@ pub fn chat_state_transition_system( mut chat_state: ResMut, ) { let current_state_value = current_state.get(); - let mut next_state_value = current_state_value.clone(); + + if *current_state_value == GameState::WaitingForServer || *current_state_value == GameState::LoadingSpawnArea { + // TODO: Introduce Chatting Substate + return; + } if keyboard_input.just_pressed(KeyCode::KeyT) { info!("Focusing chat via KeyT"); if *current_state_value == GameState::Playing { chat_state.just_focused = true; - next_state_value = GameState::Chatting; + next_state.set(GameState::Chatting); } } if keyboard_input.just_pressed(KeyCode::Escape) && *current_state_value == GameState::Chatting { info!("Unfocusing chat via Escape"); - next_state_value = GameState::Playing; + next_state.set(GameState::Playing); } - - next_state.set(next_state_value); } pub fn process_chat_input_system( diff --git a/src/client/collider/systems.rs b/src/client/collider/systems.rs index 3e0efeec..3f90566c 100644 --- a/src/client/collider/systems.rs +++ b/src/client/collider/systems.rs @@ -3,7 +3,7 @@ use terrain_util::client_block::block_properties; use crate::prelude::*; static COLLIDER_GRID_SIZE: u32 = 4; -static COLLIDER_RESTING_POSITION: Vec3 = Vec3::ZERO; +static COLLIDER_RESTING_POSITION: Vec3 = Vec3::MIN; static COLLIDER_CUBOID_WIDTH: f32 = 1.0; pub fn setup_coliders_system(mut commands: Commands) { @@ -11,7 +11,7 @@ pub fn setup_coliders_system(mut commands: Commands) { commands.spawn(( Collider::cuboid(256.0, 1.0, 256.0), - Transform::from_xyz(0.0, 0.0, 0.0), + Transform::from_translation(COLLIDER_RESTING_POSITION), )); for x in collider_range.clone() { diff --git a/src/client/gui/systems.rs b/src/client/gui/systems.rs index 6232b42d..2d55b673 100644 --- a/src/client/gui/systems.rs +++ b/src/client/gui/systems.rs @@ -25,7 +25,8 @@ pub fn handle_debug_state_transition_system( ) { if key_input.just_pressed(KeyCode::Tab) { match *current_state.get() { - GameState::WaitingForServer => {} + GameState::LoadingSpawnArea => {}, + GameState::WaitingForServer => {}, GameState::Playing => next_state.set(GameState::Debugging), GameState::Chatting => next_state.set(GameState::Debugging), GameState::Debugging => next_state.set(GameState::Playing), diff --git a/src/client/networking/systems.rs b/src/client/networking/systems.rs index 5ad0c00f..65e81c1f 100644 --- a/src/client/networking/systems.rs +++ b/src/client/networking/systems.rs @@ -27,7 +27,6 @@ pub fn receive_message_system( #[cfg(feature = "chat")] mut single_chat_events: ResMut< Messages, >, - mut spawn_area_loaded: ResMut, mut exit_events: MessageWriter, mut next_state: ResMut>, ) { @@ -40,7 +39,8 @@ pub fn receive_message_system( } NetworkingMessage::PlayerAccept(player_state) => { commands.insert_resource(player_resources::LocalPlayerSpawnState(player_state)); - next_state.set(GameState::Playing); + commands.insert_resource(terrain_resources::SpawnArea{ origin: player_state.position.as_ivec3() }); + next_state.set(GameState::LoadingSpawnArea); } NetworkingMessage::PlayerJoin(username) => { player_spawn_events.write(remote_player_events::RemotePlayerSpawnedEvent { @@ -102,11 +102,6 @@ pub fn receive_message_system( chunk_manager.insert_chunk(chunk); chunk_mesh_events .write(terrain_events::ChunkMeshUpdateEvent { chunk_position }); - - if chunk_position.eq(&IVec3::ZERO) { - info!("Spawn area loaded."); - spawn_area_loaded.0 = true; - } } } NetworkingMessage::PlayerSync(event) => { diff --git a/src/client/player/mod.rs b/src/client/player/mod.rs index 64429bbe..e424e75b 100644 --- a/src/client/player/mod.rs +++ b/src/client/player/mod.rs @@ -20,17 +20,12 @@ impl Plugin for PlayerPlugin { app.insert_resource(player_resources::PlayerSpawned(false)); app.insert_resource(player_resources::LastPlayerPosition::new()); app.add_systems( - Startup, + OnExit(GameState::LoadingSpawnArea), ( player_systems::setup_highlight_cube_system, player_systems::setup_player_camera, - ), - ); - app.add_systems( - Update, - (player_systems::setup_controller_on_area_ready_system,) - .run_if(terrain_resources::SpawnAreaLoaded::is_loaded) - .run_if(player_resources::PlayerSpawned::is_not_spawned), + player_systems::setup_controller_on_area_ready_system + ).chain(), ); app.add_systems( Update, @@ -38,6 +33,7 @@ impl Plugin for PlayerPlugin { player_systems::handle_controller_movement_system, player_systems::handle_player_collider_events_system, ) + .run_if(terrain_resources::SpawnAreaLoaded::is_loaded) // TODO: doublecheck .run_if(player_resources::PlayerSpawned::is_spawned), ); app.add_systems( diff --git a/src/client/player/resources.rs b/src/client/player/resources.rs index 861d7d8b..09684c11 100644 --- a/src/client/player/resources.rs +++ b/src/client/player/resources.rs @@ -35,7 +35,7 @@ impl BlockSelection { } #[derive(Resource)] -pub struct LastPlayerPosition(pub Vec3); +pub struct LastPlayerPosition(pub IVec3); impl Default for LastPlayerPosition { fn default() -> Self { @@ -45,7 +45,19 @@ impl Default for LastPlayerPosition { impl LastPlayerPosition { pub fn new() -> Self { - Self(Vec3::ZERO) + Self(IVec3::ZERO) + } + + pub fn chunk_position(&self) -> IVec3 { + Self::chunk_pos(self.0) + } + + pub fn has_same_chunk_position_as(&self, other_world_position: IVec3) -> bool { + Self::chunk_pos(self.0) == Self::chunk_pos(other_world_position) + } + + fn chunk_pos(world_pos: IVec3) -> IVec3 { + ChunkManager::world_position_to_chunk_position(world_pos) } } diff --git a/src/client/player/systems/controller.rs b/src/client/player/systems/controller.rs index ac3ce9a2..39e7f4ff 100644 --- a/src/client/player/systems/controller.rs +++ b/src/client/player/systems/controller.rs @@ -97,15 +97,31 @@ pub fn handle_controller_movement_system( query: Query<(Entity, &FpsControllerInput, &Transform)>, mut last_position: ResMut, mut collider_events: MessageWriter, + mut terrain_events: MessageWriter, ) { for (_entity, _input, transform) in &mut query.iter() { - let controller_position = transform.translation; - if last_position.0.floor() != controller_position.floor() { + let controller_position: IVec3 = transform.translation.as_ivec3(); + + if last_position.0 != controller_position { collider_events.write(collider_events::ColliderUpdateEvent { - grid_center_position: controller_position.floor().into(), + grid_center_position: [ + // TODO: refactor colliders to use integers over floats + controller_position.x as f32, + controller_position.y as f32, + controller_position.z as f32, + ], }); + + if !last_position.has_same_chunk_position_as(controller_position) { + info!("Player moved out of chunk, rerequesting chunks for: {controller_position}"); + terrain_events.write(terrain_events::RerequestChunks { + center_chunk_position: ChunkManager::world_position_to_chunk_position( + controller_position, + ), + }); + } } - last_position.0 = controller_position.floor(); + last_position.0 = controller_position; } } diff --git a/src/client/player/systems/selection.rs b/src/client/player/systems/selection.rs index 824bce37..0001c91c 100644 --- a/src/client/player/systems/selection.rs +++ b/src/client/player/systems/selection.rs @@ -1,7 +1,7 @@ use crate::prelude::*; const RAY_DIST: f32 = 20.0; -const HIGHLIGHT_CUBE_ORIGIN: Vec3 = Vec3::new(0.0, 2.0, 0.0); +const HIGHLIGHT_CUBE_ORIGIN: Vec3 = Vec3::MIN; pub fn setup_highlight_cube_system( mut commands: Commands, diff --git a/src/client/states.rs b/src/client/states.rs index ffb0abcc..afb71bc6 100644 --- a/src/client/states.rs +++ b/src/client/states.rs @@ -1,8 +1,10 @@ use bevy::prelude::States; -#[derive(States, Debug, Clone, PartialEq, Eq, Hash)] +#[derive(States, Debug, Clone, PartialEq, Eq, Hash, Default)] pub enum GameState { + #[default] WaitingForServer, + LoadingSpawnArea, Chatting, Debugging, Playing, diff --git a/src/client/terrain/events.rs b/src/client/terrain/events.rs index 668b2828..f02b3a89 100644 --- a/src/client/terrain/events.rs +++ b/src/client/terrain/events.rs @@ -5,6 +5,16 @@ pub struct ChunkMeshUpdateEvent { pub chunk_position: IVec3, } +#[derive(Message)] +pub struct RerequestChunks { + pub center_chunk_position: IVec3, +} + +#[derive(Message)] +pub struct RequestChunkBatch { + pub positions: Vec, +} + #[derive(Message)] pub struct BlockUpdateEvent { pub position: IVec3, diff --git a/src/client/terrain/mod.rs b/src/client/terrain/mod.rs index 3e3f674c..7fb8264d 100644 --- a/src/client/terrain/mod.rs +++ b/src/client/terrain/mod.rs @@ -16,9 +16,13 @@ impl Plugin for TerrainPlugin { app.insert_resource(resources::RenderMaterials::new()); app.insert_resource(resources::MesherTasks::default()); app.insert_resource(resources::ChunkEntityMap::default()); + app.insert_resource(resources::RequestedChunks::default()); + app.insert_resource(resources::LastChunkRequestOrigin::default()); app.add_message::(); app.add_message::(); app.add_message::(); + app.add_message::(); + app.add_message::(); app.add_systems(Startup, terrain_systems::prepare_mesher_materials_system); #[cfg(feature = "skip_terrain")] { @@ -30,12 +34,13 @@ impl Plugin for TerrainPlugin { app.insert_resource(terrain_resources::SpawnAreaLoaded(false)); app.add_systems( - OnExit(GameState::WaitingForServer), - terrain_systems::prepare_spawn_area_system, + OnEnter(GameState::LoadingSpawnArea), + terrain_systems::generate_world_system, ); app.add_systems( - OnExit(GameState::WaitingForServer), - terrain_systems::generate_world_system, + Update, + (terrain_systems::check_if_spawn_area_is_loaded_system) + .run_if(in_state(GameState::LoadingSpawnArea)), ); app.add_systems( Update, @@ -46,6 +51,12 @@ impl Plugin for TerrainPlugin { terrain_systems::handle_terrain_regeneration_events_system, ); app.add_systems(Update, terrain_systems::handle_chunk_tasks_system); + app.add_systems(Update, terrain_systems::handle_chunk_rerequests_system); + app.add_systems( + Update, + terrain_systems::handle_chunk_request_chunk_batch_event_system, + ); + app.add_systems(Update, terrain_systems::cleanup_chunk_entities_system); } } } diff --git a/src/client/terrain/resources.rs b/src/client/terrain/resources.rs index 0e37ec18..273b34a5 100644 --- a/src/client/terrain/resources.rs +++ b/src/client/terrain/resources.rs @@ -1,3 +1,5 @@ +use std::{collections::{HashSet, hash_map::ExtractIf}, option::Iter}; + use bevy::tasks::Task; use crate::prelude::*; @@ -11,6 +13,15 @@ impl SpawnAreaLoaded { } } +#[derive(Resource, Default)] +pub struct RequestedChunks { + pub previous_chunks: HashSet, +} +#[derive(Resource, Default)] +pub struct LastChunkRequestOrigin { + pub position: IVec3 +} + #[derive(Eq, Hash, Clone, PartialEq)] pub enum MeshType { Solid, @@ -38,7 +49,16 @@ pub struct ChunkEntityMap { map: HashMap>, } +#[derive(Resource, Default)] +pub struct SpawnArea { + pub origin: IVec3 +} + impl ChunkEntityMap { + pub fn count(&self) -> usize { + return self.map.iter().count() + } + pub fn add(&mut self, chunk_position: IVec3, entity: Entity) { self.map.entry(chunk_position).or_default().push(entity); } @@ -46,6 +66,18 @@ impl ChunkEntityMap { pub fn remove(&mut self, chunk_position: IVec3) -> Option> { self.map.remove(&chunk_position) } + + pub fn extract_within_distance(&mut self, origin: &IVec3, distance: &IVec3) -> Vec<(IVec3, Vec)> { + let extracted: HashMap> = self.map.extract_if(|k, _v| { + (k.x - origin.x) > distance.x + || (k.y - origin.y) > distance.y + || (k.z - origin.z) > distance.z + }).collect(); + + extracted.into_iter().map(|(key, entities)| { + (key, entities) + }).collect() + } } #[derive(Resource)] diff --git a/src/client/terrain/systems.rs b/src/client/terrain/systems.rs index 5c000e37..4d1b76cc 100644 --- a/src/client/terrain/systems.rs +++ b/src/client/terrain/systems.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use bevy::tasks::{futures_lite::future, AsyncComputeTaskPool}; use terrain_components::ChunkMesh; use terrain_resources::{ @@ -6,6 +8,9 @@ use terrain_resources::{ use crate::prelude::*; +const RENDER_DISTANCE: IVec3 = IVec3::new(4, 4, 4); +const MIN_SPAWN_AREA_DISTANCE: IVec3 = IVec3::new(1,1,1); + pub fn prepare_mesher_materials_system( mut render_materials: ResMut, mut materials: ResMut>, @@ -34,37 +39,38 @@ pub fn generate_simple_ground_system( )); } -pub fn prepare_spawn_area_system(mut client: ResMut) { - info!("Sending chunk requests for spawn area"); - - let chunks = ChunkManager::instantiate_chunks(IVec3::ZERO, IVec3::ONE); +pub fn generate_world_system( + chunk_manager: Res, + spawn_area: Res, + mut batch_events: MessageWriter, +) { + let origin = spawn_area.origin; + let positions = chunk_manager.sorted_new_chunk_positions(origin, RENDER_DISTANCE); - let positions: Vec = chunks.into_iter().map(|chunk| chunk.position).collect(); - let message = bincode::serialize(&NetworkingMessage::ChunkBatchRequest(positions)); - info!("requesting world"); - client.send_message(DefaultChannel::ReliableUnordered, message.unwrap()); + batch_events.write(terrain_events::RequestChunkBatch { positions }); } -pub fn generate_world_system( +pub fn handle_chunk_request_chunk_batch_event_system( mut client: ResMut, - mut chunk_manager: ResMut, + mut batch_events: MessageReader, + mut all_requests: ResMut, ) { - let render_distance = IVec3::new(4, 4, 4); - - info!("Sending chunk requests for chunks"); + if batch_events.is_empty() { + return; + } - let origin = IVec3::ZERO; - let chunks = chunk_manager.instantiate_new_chunks(origin, render_distance); + let mut new_positions: HashSet = HashSet::new(); + for batch_event in batch_events.read() { + batch_event.positions.iter().for_each(|position| { + new_positions.insert(*position); + }); + } - let mut positions: Vec = chunks.into_iter().map(|chunk| chunk.position).collect(); - positions.sort_by(|a, b| { - (a - origin) - .length_squared() - .cmp(&(b - origin).length_squared()) - }); + let old_positions = &all_requests.previous_chunks; + let diff: HashSet<&IVec3> = new_positions.difference(old_positions).collect(); + let diff: Vec = diff.into_iter().copied().collect(); - let batched_positions = positions.chunks(32); - assert!(batched_positions.len() > 0, "Batched positions is empty"); + let batched_positions = diff.chunks(32); batched_positions.enumerate().for_each(|(index, batch)| { let request_positions = batch.to_vec(); @@ -76,6 +82,10 @@ pub fn generate_world_system( info!("requesting chunks #{}", index); client.send_message(DefaultChannel::ReliableUnordered, message.unwrap()); }); + + diff.iter().for_each(|position| { + all_requests.previous_chunks.insert(*position); + }) } pub fn handle_chunk_mesh_update_events_system( @@ -104,6 +114,22 @@ pub fn handle_chunk_mesh_update_events_system( } } +pub fn handle_chunk_rerequests_system( + chunk_manager: Res, + mut terrain_events: MessageReader, + mut batch_events: MessageWriter, + mut last_chunk_request_origin: ResMut, +) { + for event in terrain_events.read() { + info!("Sending chunk requests for chunks"); + + let origin = event.center_chunk_position; + last_chunk_request_origin.position = origin; + let positions = chunk_manager.sorted_new_chunk_positions(origin, RENDER_DISTANCE); + batch_events.write(terrain_events::RequestChunkBatch { positions }); + } +} + fn create_mesh_task(chunk: &Chunk, texture_manager: &terrain_util::TextureManager) -> MeshTask { let task_pool = AsyncComputeTaskPool::get(); let chunk = *chunk; @@ -178,6 +204,41 @@ pub fn handle_chunk_tasks_system( }); } +pub fn cleanup_chunk_entities_system( + mut commands: Commands, + mut chunk_entities: ResMut, + origin: Res, +) { + if chunk_entities.count() as i32 > RENDER_DISTANCE.x * RENDER_DISTANCE.y * RENDER_DISTANCE.z * 5 + { + chunk_entities + .extract_within_distance(&origin.position, &RENDER_DISTANCE) + .iter() + .for_each(|(_position, entities)| { + entities + .iter() + .for_each(|entity| commands.entity(*entity).despawn()) + }); + } +} + + +pub fn check_if_spawn_area_is_loaded_system( + chunk_manager: Res, + mut spawn_area_loaded: ResMut, + mut next_state: ResMut>, +) { + let len = chunk_manager.get_all_chunk_positions().len(); + // let expected_len = ((MIN_SPAWN_AREA_DISTANCE.x * 2) * (MIN_SPAWN_AREA_DISTANCE.y * 2) * (MIN_SPAWN_AREA_DISTANCE.z * 2)) as usize; + let expected_len = 3; + warn!("{} {}", expected_len, len); + if len >= expected_len { + warn!("All chunks for spawn area have been received, proceeding with GameState::Playing"); + next_state.set(GameState::Playing); + spawn_area_loaded.0 = true; + } +} + fn create_chunk_bundle( mesh_handle: Handle, chunk_position: Vec3, diff --git a/src/server/networking/systems.rs b/src/server/networking/systems.rs index 7de9ce32..8310b50d 100644 --- a/src/server/networking/systems.rs +++ b/src/server/networking/systems.rs @@ -135,7 +135,7 @@ pub fn handle_events_system( *client_id, DefaultChannel::ReliableOrdered, bincode::serialize(&NetworkingMessage::PlayerReject(String::from( - "Another Client is already connected with that Username.", + "Another Client is already connected with that Username. Wait 15 seconds before trying again.", ))) .expect("Message should always be sendable"), ); diff --git a/src/server/terrain/util/generator.rs b/src/server/terrain/util/generator.rs index d514364c..22df77d8 100644 --- a/src/server/terrain/util/generator.rs +++ b/src/server/terrain/util/generator.rs @@ -43,10 +43,6 @@ impl Generator { } pub fn generate_chunk(&self, chunk: &mut Chunk) { - if chunk.position.y < 0 { - return; - } - for_each_chunk_coordinate!(chunk, |x, y, z, world_position| { let block = self.generate_block(world_position); chunk.set_unpadded(x, y, z, block); diff --git a/src/shared/chunk/manager.rs b/src/shared/chunk/manager.rs index 7425055b..44e5715a 100644 --- a/src/shared/chunk/manager.rs +++ b/src/shared/chunk/manager.rs @@ -51,23 +51,40 @@ impl ChunkManager { chunks } - pub fn instantiate_new_chunks( - &mut self, - position: IVec3, - render_distance: IVec3, - ) -> Vec { - let chunks = Self::instantiate_chunks(position, render_distance); - - chunks + pub fn sorted_new_chunk_positions(&self, origin: IVec3, distance: IVec3) -> Vec { + let all_positions = Self::get_sorted_chunk_positions_in_range(origin, distance); + all_positions .into_iter() - .filter(|chunk| { - let chunk_position = chunk.position; - let chunk = self.get_chunk_mut(&chunk_position); - chunk.is_none() - }) + .filter(|position| self.get_chunk(position).is_none()) .collect() } + pub fn get_sorted_chunk_positions_in_range(origin: IVec3, distance: IVec3) -> Vec { + let distance_x = distance.x; + let distance_y = distance.y; + let distance_z = distance.z; + + let mut positions: Vec = + Vec::with_capacity((distance_x * 2 * distance_y * 2 * distance_z * 2) as usize); + + for x in -distance_x..distance_x { + for y in -distance_y..distance_y { + for z in -distance_z..distance_z { + let chunk_position = IVec3::new(x + origin.x, y + origin.y, z + origin.z); + positions.push(chunk_position); + } + } + } + + positions.sort_by(|a, b| { + (a - origin) + .length_squared() + .cmp(&(b - origin).length_squared()) + }); + + positions + } + pub fn insert_chunk(&mut self, chunk: Chunk) { self.chunks.insert(chunk.position, chunk); } @@ -171,12 +188,16 @@ impl ChunkManager { out } - fn chunk_at_position(&mut self, position: IVec3) -> Option<&mut Chunk> { - let chunk_position = IVec3 { + pub fn world_position_to_chunk_position(position: IVec3) -> IVec3 { + IVec3 { x: position.x.div_euclid(CHUNK_SIZE as i32), y: position.y.div_euclid(CHUNK_SIZE as i32), z: position.z.div_euclid(CHUNK_SIZE as i32), - }; + } + } + + fn chunk_at_position(&mut self, position: IVec3) -> Option<&mut Chunk> { + let chunk_position = Self::world_position_to_chunk_position(position); self.get_chunk_mut(&chunk_position) }