diff --git a/client/src/args.rs b/client/src/args.rs index 49e8007..37a8491 100644 --- a/client/src/args.rs +++ b/client/src/args.rs @@ -1,7 +1,8 @@ +use bevy::ecs::resource::Resource; use clap::{Parser, Subcommand}; use common::network::{DEFAULT_ADDRESS, DEFAULT_PORT}; -#[derive(Parser, Default, Debug)] +#[derive(Resource, Parser, Default, Debug)] #[command(version, about)] pub struct Args { #[command(subcommand)] diff --git a/client/src/block.rs b/client/src/block.rs deleted file mode 100644 index c64ceb0..0000000 --- a/client/src/block.rs +++ /dev/null @@ -1,31 +0,0 @@ -use bevy::prelude::*; - -pub use common::block::*; - -#[derive(Resource)] -pub struct BlockResources { - mesh: Handle, - material: Handle, -} - -pub fn setup_blocks( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { - commands.insert_resource(BlockResources { - mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)), - material: materials.add(Color::srgb_u8(124, 144, 255)), - }); -} - -pub fn add_block_visuals( - add: On, - mut commands: Commands, - resources: Res, -) { - commands.entity(add.entity).insert(( - Mesh3d(resources.mesh.clone()), - MeshMaterial3d(resources.material.clone()), - )); -} diff --git a/client/src/block_assets.rs b/client/src/block_assets.rs new file mode 100644 index 0000000..e1f3b7e --- /dev/null +++ b/client/src/block_assets.rs @@ -0,0 +1,41 @@ +use bevy::prelude::*; + +use common::asset_loading::LoadResource; +use common::block::Block; + +pub fn plugin(app: &mut App) { + app.add_observer(insert_block_visuals); + app.load_resource::(); +} + +#[derive(Resource, Asset, Reflect, Clone)] +#[reflect(Resource)] +pub struct BlockAssets { + #[dependency] + mesh: Handle, + #[dependency] + material: Handle, +} + +impl FromWorld for BlockAssets { + fn from_world(world: &mut World) -> Self { + let assets = world.resource::(); + Self { + mesh: assets.add(Cuboid::new(1.0, 1.0, 1.0).into()), + material: assets.add(Color::srgb_u8(124, 144, 255).into()), + } + } +} + +/// Observer which automatically inserts block visuals (mesh and +/// material) when an entity has the `Block` component added to it. +fn insert_block_visuals( + add: On, + mut commands: Commands, + block_assets: Res, +) { + commands.entity(add.entity).insert(( + Mesh3d(block_assets.mesh.clone()), + MeshMaterial3d(block_assets.material.clone()), + )); +} diff --git a/client/src/loading.rs b/client/src/loading.rs new file mode 100644 index 0000000..a25d4f9 --- /dev/null +++ b/client/src/loading.rs @@ -0,0 +1,20 @@ +use bevy::prelude::*; + +use common::asset_loading::ResourceHandles; + +use crate::Screen; + +pub fn plugin(app: &mut App) { + app.add_systems( + Update, + enter_gameplay_screen.run_if(in_state(Screen::Loading).and(all_assets_loaded)), + ); +} + +fn enter_gameplay_screen(mut next_screen: ResMut>) { + next_screen.set(Screen::Gameplay); +} + +fn all_assets_loaded(resource_handles: Res) -> bool { + resource_handles.is_all_done() +} diff --git a/client/src/main.rs b/client/src/main.rs index 1dd6fb6..99361b4 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -3,75 +3,93 @@ use common::network::*; use bevy::asset::AssetMetaCheck; use bevy::window::WindowResolution; +use common::block::Blocks; use lightyear::prelude::server::Started; mod args; -mod block; +mod block_assets; mod camera; +mod loading; mod placement; use args::*; -use block::*; use camera::*; use placement::*; -#[rustfmt::skip] +#[derive(States, Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum Screen { + Loading, + Gameplay, +} + fn main() -> Result { - let cli = Args::parse(); let mut app = App::new(); - app.add_plugins(DefaultPlugins - .set(WindowPlugin { - primary_window: Some(Window { - title: "bevy-bloxel-classic".into(), - // Steam Deck: DPI appears pretty high, causing everything to be scaled up. - // Setting `scale_factor_override` prevents this from happening. - resolution: WindowResolution::new(1280, 720).with_scale_factor_override(1.0), - // WEB: Fit canvas to parent element, so `Window` resizes automatically. - fit_canvas_to_parent: true, - // WEB: Don't override default event handling like browser hotkeys while focused. - prevent_default_event_handling: false, + app.add_plugins( + DefaultPlugins + .set(WindowPlugin { + primary_window: Some(Window { + title: "bevy-bloxel-classic".into(), + // Steam Deck: DPI appears pretty high, causing everything to be scaled up. + // Setting `scale_factor_override` prevents this from happening. + resolution: WindowResolution::new(1280, 720).with_scale_factor_override(1.0), + // WEB: Fit canvas to parent element, so `Window` resizes automatically. + fit_canvas_to_parent: true, + // WEB: Don't override default event handling like browser hotkeys while focused. + prevent_default_event_handling: false, + ..default() + }), + ..default() + }) + .set(AssetPlugin { + // WEB: Don't check for `.meta` files since we don't use them. + meta_check: AssetMetaCheck::Never, ..default() }), - ..default() - }) - .set(AssetPlugin { - // WEB: Don't check for `.meta` files since we don't use them. - meta_check: AssetMetaCheck::Never, - ..default() - })); - - // Fixes issue on web where the cursor isn't ungrabbed properly. - app.add_plugins(bevy_fix_cursor_unlock_web::FixPointerUnlockPlugin); - - app.add_plugins(common::network::ServerPlugin); - app.add_plugins(common::network::ClientPlugin); - - app.add_systems(Startup, setup_crosshair); - app.add_systems(Startup, setup_blocks); - app.add_systems(Startup, setup_scene.after(setup_blocks)); + ); + + app.add_plugins(( + bevy_fix_cursor_unlock_web::FixPointerUnlockPlugin, + ServerPlugin, + ClientPlugin, + common::asset_loading::plugin, + block_assets::plugin, + loading::plugin, + )); - app.add_systems(Update, cursor_grab); - app.add_systems(Update, update_crosshair_visibility.after(cursor_grab)); - app.add_systems(Update, camera_look.after(cursor_grab).run_if(is_cursor_grabbed)); - app.add_systems(Update, noclip_controller.after(camera_look).run_if(is_cursor_grabbed)); + app.insert_resource(Args::parse()); + app.insert_state(Screen::Loading); + + app.add_systems( + OnEnter(Screen::Gameplay), + (setup_crosshair, setup_scene, start_server_or_connect), + ); + + // TODO: Create and configure `SystemSet`s for gameplay, input, etc. + app.add_systems( + Update, + ( + cursor_grab, + update_crosshair_visibility.after(cursor_grab), + camera_look.after(cursor_grab).run_if(is_cursor_grabbed), + noclip_controller + .after(camera_look) + .run_if(is_cursor_grabbed), + ) + .run_if(in_state(Screen::Gameplay)), + ); // `place_break_blocks` requires the camera's `GlobalTransform`. // For a most up-to-date value, run it after that's been updated. - app.add_systems(PostUpdate, place_break_blocks.after(TransformSystems::Propagate)); - + app.add_systems( + PostUpdate, + place_break_blocks + .after(TransformSystems::Propagate) + .run_if(in_state(Screen::Gameplay).and(is_cursor_grabbed)), + ); + + // TODO: Move this to a more general world generation module. app.add_observer(spawn_initial_blocks); - app.add_observer(add_block_visuals); - - let mut commands = app.world_mut().commands(); - match cli.mode.unwrap_or_default() { - Mode::Local => commands.queue(StartLocalServerCommand), - #[cfg(not(target_family = "wasm"))] - Mode::Host { port } => commands.queue(StartWebTransportServerCommand::new(port)), - Mode::Connect { address, digest } => commands.queue(ConnectWebTransportCommand::new(&address, digest)?), - } - // NOTE: When setting up a local server, a host-client is automatically - // connected to it thanks to the `autoconnect_host_client` observer. app.run(); Ok(()) @@ -101,3 +119,23 @@ fn spawn_initial_blocks(_event: On, mut blocks: Blocks) { } } } + +fn start_server_or_connect(args: Res, mut commands: Commands) -> Result { + let mode = args.mode.as_ref(); + let default = Mode::default(); + match mode.unwrap_or(&default) { + Mode::Local => { + commands.queue(StartLocalServerCommand); + } + #[cfg(not(target_family = "wasm"))] + Mode::Host { port } => { + commands.queue(StartWebTransportServerCommand::new(*port)); + } + Mode::Connect { address, digest } => { + commands.queue(ConnectWebTransportCommand::new(&address, digest.clone())?); + } + } + // NOTE: When setting up a local server, a host-client is automatically + // connected to it thanks to the `autoconnect_host_client` observer. + Ok(()) +} diff --git a/client/src/placement.rs b/client/src/placement.rs index b603880..8a9538a 100644 --- a/client/src/placement.rs +++ b/client/src/placement.rs @@ -1,7 +1,9 @@ use bevy::prelude::*; use bevy::window::{CursorGrabMode, CursorOptions}; -use crate::block::*; +use common::block::Blocks; + +// TODO: Use picking system instead of manually raycasting. pub fn place_break_blocks( mut commands: Commands, diff --git a/common/src/asset_loading.rs b/common/src/asset_loading.rs new file mode 100644 index 0000000..a366a5c --- /dev/null +++ b/common/src/asset_loading.rs @@ -0,0 +1,72 @@ +//! A high-level way to load collections of asset handles as resources. +// Taken from: https://github.com/TheBevyFlock/bevy_new_2d/blob/main/src/asset_tracking.rs + +use std::collections::VecDeque; + +use bevy::prelude::*; + +pub fn plugin(app: &mut App) { + app.init_resource::(); + app.add_systems(PreUpdate, load_resource_assets); +} + +pub trait LoadResource { + /// This will load the [`Resource`] as an [`Asset`]. When all of its asset dependencies + /// have been loaded, it will be inserted as a resource. This ensures that the resource only + /// exists when the assets are ready. + fn load_resource(&mut self) -> &mut Self; +} + +impl LoadResource for App { + fn load_resource(&mut self) -> &mut Self { + self.init_asset::(); + let world = self.world_mut(); + let value = T::from_world(world); + let assets = world.resource::(); + let handle = assets.add(value); + let mut handles = world.resource_mut::(); + handles + .waiting + .push_back((handle.untyped(), |world, handle| { + let assets = world.resource::>(); + if let Some(value) = assets.get(handle.id().typed::()) { + world.insert_resource(value.clone()); + } + })); + self + } +} + +/// A function that inserts a loaded resource. +type InsertLoadedResource = fn(&mut World, &UntypedHandle); + +#[derive(Resource, Default)] +pub struct ResourceHandles { + // Use a queue for waiting assets so they can be cycled through and moved to + // `finished` one at a time. + waiting: VecDeque<(UntypedHandle, InsertLoadedResource)>, + finished: Vec, +} + +impl ResourceHandles { + /// Returns true if all requested [`Asset`]s have finished loading and are available as [`Resource`]s. + pub fn is_all_done(&self) -> bool { + self.waiting.is_empty() + } +} + +fn load_resource_assets(world: &mut World) { + world.resource_scope(|world, mut resource_handles: Mut| { + world.resource_scope(|world, assets: Mut| { + for _ in 0..resource_handles.waiting.len() { + let (handle, insert_fn) = resource_handles.waiting.pop_front().unwrap(); + if assets.is_loaded_with_dependencies(&handle) { + insert_fn(world, &handle); + resource_handles.finished.push(handle); + } else { + resource_handles.waiting.push_back((handle, insert_fn)); + } + } + }); + }); +} diff --git a/common/src/lib.rs b/common/src/lib.rs index 9b09d8b..69000e3 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,2 +1,3 @@ +pub mod asset_loading; pub mod block; pub mod network;