Delay gameplay until assets loaded

- Steal `asset_loading` from `bevy_new_2d`
- Rename `BlockResources` to `BlockAssets`
- Remove `common::blocks::*` re-exports
- Turn `Args` into a resource and insert it
- Add `Screen` state to see when loading finishes
- Run gameplay systems only in `Gameplay` state
- Start server / connect when loading finishes
copygirl 1 month ago
parent 2c7d27311b
commit 9b77a07aa5
  1. 3
      client/src/args.rs
  2. 31
      client/src/block.rs
  3. 41
      client/src/block_assets.rs
  4. 20
      client/src/loading.rs
  5. 136
      client/src/main.rs
  6. 4
      client/src/placement.rs
  7. 72
      common/src/asset_loading.rs
  8. 1
      common/src/lib.rs

@ -1,7 +1,8 @@
use bevy::ecs::resource::Resource;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use common::network::{DEFAULT_ADDRESS, DEFAULT_PORT}; use common::network::{DEFAULT_ADDRESS, DEFAULT_PORT};
#[derive(Parser, Default, Debug)] #[derive(Resource, Parser, Default, Debug)]
#[command(version, about)] #[command(version, about)]
pub struct Args { pub struct Args {
#[command(subcommand)] #[command(subcommand)]

@ -1,31 +0,0 @@
use bevy::prelude::*;
pub use common::block::*;
#[derive(Resource)]
pub struct BlockResources {
mesh: Handle<Mesh>,
material: Handle<StandardMaterial>,
}
pub fn setup_blocks(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
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<Add, Block>,
mut commands: Commands,
resources: Res<BlockResources>,
) {
commands.entity(add.entity).insert((
Mesh3d(resources.mesh.clone()),
MeshMaterial3d(resources.material.clone()),
));
}

@ -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::<BlockAssets>();
}
#[derive(Resource, Asset, Reflect, Clone)]
#[reflect(Resource)]
pub struct BlockAssets {
#[dependency]
mesh: Handle<Mesh>,
#[dependency]
material: Handle<StandardMaterial>,
}
impl FromWorld for BlockAssets {
fn from_world(world: &mut World) -> Self {
let assets = world.resource::<AssetServer>();
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<Add, Block>,
mut commands: Commands,
block_assets: Res<BlockAssets>,
) {
commands.entity(add.entity).insert((
Mesh3d(block_assets.mesh.clone()),
MeshMaterial3d(block_assets.material.clone()),
));
}

@ -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<NextState<Screen>>) {
next_screen.set(Screen::Gameplay);
}
fn all_assets_loaded(resource_handles: Res<ResourceHandles>) -> bool {
resource_handles.is_all_done()
}

@ -3,75 +3,93 @@ use common::network::*;
use bevy::asset::AssetMetaCheck; use bevy::asset::AssetMetaCheck;
use bevy::window::WindowResolution; use bevy::window::WindowResolution;
use common::block::Blocks;
use lightyear::prelude::server::Started; use lightyear::prelude::server::Started;
mod args; mod args;
mod block; mod block_assets;
mod camera; mod camera;
mod loading;
mod placement; mod placement;
use args::*; use args::*;
use block::*;
use camera::*; use camera::*;
use placement::*; use placement::*;
#[rustfmt::skip] #[derive(States, Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum Screen {
Loading,
Gameplay,
}
fn main() -> Result { fn main() -> Result {
let cli = Args::parse();
let mut app = App::new(); let mut app = App::new();
app.add_plugins(DefaultPlugins app.add_plugins(
.set(WindowPlugin { DefaultPlugins
primary_window: Some(Window { .set(WindowPlugin {
title: "bevy-bloxel-classic".into(), primary_window: Some(Window {
// Steam Deck: DPI appears pretty high, causing everything to be scaled up. title: "bevy-bloxel-classic".into(),
// Setting `scale_factor_override` prevents this from happening. // Steam Deck: DPI appears pretty high, causing everything to be scaled up.
resolution: WindowResolution::new(1280, 720).with_scale_factor_override(1.0), // Setting `scale_factor_override` prevents this from happening.
// WEB: Fit canvas to parent element, so `Window` resizes automatically. resolution: WindowResolution::new(1280, 720).with_scale_factor_override(1.0),
fit_canvas_to_parent: true, // WEB: Fit canvas to parent element, so `Window` resizes automatically.
// WEB: Don't override default event handling like browser hotkeys while focused. fit_canvas_to_parent: true,
prevent_default_event_handling: false, // 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()
}), }),
..default() );
})
.set(AssetPlugin { app.add_plugins((
// WEB: Don't check for `.meta` files since we don't use them. bevy_fix_cursor_unlock_web::FixPointerUnlockPlugin,
meta_check: AssetMetaCheck::Never, ServerPlugin,
..default() ClientPlugin,
})); common::asset_loading::plugin,
block_assets::plugin,
// Fixes issue on web where the cursor isn't ungrabbed properly. loading::plugin,
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_systems(Update, cursor_grab); app.insert_resource(Args::parse());
app.add_systems(Update, update_crosshair_visibility.after(cursor_grab)); app.insert_state(Screen::Loading);
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.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`. // `place_break_blocks` requires the camera's `GlobalTransform`.
// For a most up-to-date value, run it after that's been updated. // 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(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(); app.run();
Ok(()) Ok(())
@ -101,3 +119,23 @@ fn spawn_initial_blocks(_event: On<Add, Started>, mut blocks: Blocks) {
} }
} }
} }
fn start_server_or_connect(args: Res<Args>, 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(())
}

@ -1,7 +1,9 @@
use bevy::prelude::*; use bevy::prelude::*;
use bevy::window::{CursorGrabMode, CursorOptions}; 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( pub fn place_break_blocks(
mut commands: Commands, mut commands: Commands,

@ -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::<ResourceHandles>();
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<T: Resource + Asset + Clone + FromWorld>(&mut self) -> &mut Self;
}
impl LoadResource for App {
fn load_resource<T: Resource + Asset + Clone + FromWorld>(&mut self) -> &mut Self {
self.init_asset::<T>();
let world = self.world_mut();
let value = T::from_world(world);
let assets = world.resource::<AssetServer>();
let handle = assets.add(value);
let mut handles = world.resource_mut::<ResourceHandles>();
handles
.waiting
.push_back((handle.untyped(), |world, handle| {
let assets = world.resource::<Assets<T>>();
if let Some(value) = assets.get(handle.id().typed::<T>()) {
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<UntypedHandle>,
}
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<ResourceHandles>| {
world.resource_scope(|world, assets: Mut<AssetServer>| {
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));
}
}
});
});
}

@ -1,2 +1,3 @@
pub mod asset_loading;
pub mod block; pub mod block;
pub mod network; pub mod network;

Loading…
Cancel
Save