Add block selection UI

main
copygirl 3 weeks ago
parent 8700c23893
commit f15421a461
  1. 10
      client/src/assets/block_visuals.rs
  2. 9
      client/src/input/client_inputs.rs
  3. 3
      client/src/main.rs
  4. 150
      client/src/ui/block_selection.rs
  5. 7
      client/src/ui/mod.rs
  6. 3
      common/src/block.rs

@ -23,16 +23,16 @@ struct BuiltinBlockMeshes {
#[derive(Asset, TypePath)] #[derive(Asset, TypePath)]
pub struct BlockVisuals { pub struct BlockVisuals {
_id: Identifier<Block>, id: Identifier<Block>,
color: Color, color: Color,
material: Handle<StandardMaterial>, material: Handle<StandardMaterial>,
// mesh: Handle<Mesh>, // mesh: Handle<Mesh>,
} }
impl BlockVisuals { impl BlockVisuals {
// pub fn id(&self) -> &Identifier<Block> { pub fn id(&self) -> &Identifier<Block> {
// &self.id &self.id
// } }
pub fn color(&self) -> Color { pub fn color(&self) -> Color {
self.color self.color
@ -81,7 +81,7 @@ impl AssetLoader for BlockVisualsLoader {
let id = load_context.path().try_into()?; let id = load_context.path().try_into()?;
Ok(BlockVisuals { Ok(BlockVisuals {
_id: id, id,
color, color,
material, material,
}) })

@ -8,6 +8,8 @@ use lightyear::prelude::input::native::*;
use bevy::window::{CursorGrabMode, CursorOptions}; use bevy::window::{CursorGrabMode, CursorOptions};
use crate::ui::block_selection::SelectedBlock;
pub(super) fn plugin(app: &mut App) { pub(super) fn plugin(app: &mut App) {
app.init_resource::<CurrentAction>(); app.init_resource::<CurrentAction>();
app.add_systems( app.add_systems(
@ -28,10 +30,14 @@ struct CurrentAction(Action);
fn place_or_break_blocks( fn place_or_break_blocks(
mut event: On<Pointer<Press>>, mut event: On<Pointer<Press>>,
selected_block: Option<Res<SelectedBlock>>,
mut current_action: ResMut<CurrentAction>, mut current_action: ResMut<CurrentAction>,
cursor: Single<&CursorOptions>, cursor: Single<&CursorOptions>,
blocks: Blocks, blocks: Blocks,
) { ) {
let Some(selected_block) = selected_block else {
return; // No block selected.
};
let is_place = match event.button { let is_place = match event.button {
PointerButton::Primary => false, // left-click PointerButton::Primary => false, // left-click
PointerButton::Secondary => true, // right-click PointerButton::Secondary => true, // right-click
@ -50,8 +56,7 @@ fn place_or_break_blocks(
current_action.0 = if is_place { current_action.0 = if is_place {
// FIXME: This only works for axis-aligned normals. // FIXME: This only works for axis-aligned normals.
let offset = normal.normalize().round().as_ivec3(); let offset = normal.normalize().round().as_ivec3();
// TODO: Don't hardcode block type. Action::PlaceBlock(block_pos + offset, selected_block.clone())
Action::PlaceBlock(block_pos + offset, Block::DEFAULT)
} else { } else {
Action::BreakBlock(block_pos) Action::BreakBlock(block_pos)
}; };

@ -104,10 +104,11 @@ fn start_server_or_connect(args: Res<Args>, mut commands: Commands) -> Result {
/// When the server is started, spawn the initial blocks the world is made of. /// When the server is started, spawn the initial blocks the world is made of.
fn spawn_initial_blocks(_event: On<Add, Started>, mut blocks: Blocks) { fn spawn_initial_blocks(_event: On<Add, Started>, mut blocks: Blocks) {
const PLATFORM: Identifier<Block> = Identifier::new_const("vampire_black");
for x in -8..8 { for x in -8..8 {
for z in -8..8 { for z in -8..8 {
let pos = BlockPos::new(x, 0, z); let pos = BlockPos::new(x, 0, z);
blocks.spawn(pos, Block::PLATFORM).unwrap(); blocks.spawn(pos, PLATFORM.clone()).unwrap();
} }
} }
} }

@ -0,0 +1,150 @@
use bevy::color::palettes::css::*;
use bevy::prelude::*;
use common::prelude::*;
use bevy::window::{CursorGrabMode, CursorOptions};
use crate::assets::block_visuals::BlockVisuals;
const SCALE: i32 = 4;
pub(super) fn plugin(app: &mut App) {
let blocks_changed = Manifest::<BlockVisuals>::changed;
app.add_systems(PostUpdate, setup_selection_ui.run_if(blocks_changed));
app.add_systems(Update, update_selection_visibility);
let selection_changed = resource_exists_and_changed::<SelectedBlock>;
app.add_systems(Update, update_selected_cell.run_if(selection_changed));
}
#[derive(Resource, Deref, DerefMut)]
pub struct SelectedBlock(Identifier<Block>);
#[derive(Component)]
struct BlockSelectionLayout;
#[derive(Component)]
#[require(Button, Pickable)]
struct BlockSelectionCell {
id: Identifier<Block>,
}
fn setup_selection_ui(
existing: Option<Single<Entity, With<BlockSelectionLayout>>>,
blocks: Manifest<BlockVisuals>,
mut commands: Commands,
) {
// Despawn previous block selection UI, if present.
if let Some(existing) = existing {
commands.entity(*existing).despawn();
}
commands
.spawn((
BlockSelectionLayout,
Node {
width: percent(100),
height: percent(100),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
))
.with_children(|builder| {
builder
.spawn((
Pickable::default(),
Node {
display: Display::Grid,
padding: UiRect::all(px(4 * SCALE)),
grid_template_columns: RepeatedGridTrack::auto(9),
overflow: Overflow::scroll_y(),
..default()
},
BackgroundColor(BLACK.with_alpha(0.4).into()),
))
.observe(|mut event: On<Pointer<Click>>| {
// Ensure the event doesn't propagate up to the
// `Window`, which would grab the mouse cursor.
event.propagate(false);
})
.with_children(|builder| {
for block in blocks.iter() {
spawn_block_cell(builder, block);
}
});
});
}
fn spawn_block_cell(builder: &mut ChildSpawnerCommands, block: &BlockVisuals) {
let id = block.id().clone();
builder
.spawn((
BlockSelectionCell { id },
Node {
width: px(16 * SCALE),
height: px(16 * SCALE),
margin: UiRect::all(px(2 * SCALE)),
padding: UiRect::all(px(2 * SCALE)),
border: UiRect::all(px(2 * SCALE)),
..default()
},
BackgroundColor(block.color()),
BorderColor::all(BLACK),
))
.observe(on_cell_click)
.observe(on_cell_over)
.observe(on_cell_out);
}
// TODO: Ideally visibility should be toggled when pressing `E`, but this works for now.
fn update_selection_visibility(
cursor: Single<&CursorOptions, Changed<CursorOptions>>,
crosshair: Single<&mut Visibility, With<BlockSelectionLayout>>,
) {
let is_grabbed = cursor.grab_mode != CursorGrabMode::None;
let mut selection_visibility = crosshair.into_inner();
*selection_visibility = is_grabbed.then_some(Visibility::Hidden).unwrap_or_default();
}
fn update_selected_cell(
cells: Query<(&BlockSelectionCell, &mut BorderColor)>,
selected_block: Res<SelectedBlock>,
) {
for (cell, mut color) in cells {
let is_selected = cell.id == **selected_block;
*color = if is_selected { WHITE } else { BLACK }.into();
}
}
fn on_cell_click(
event: On<Pointer<Click>>,
mut cells: Query<&BlockSelectionCell>,
mut commands: Commands,
) {
// SAFETY: Entity is known to have this component.
let cell = cells.get_mut(event.entity).unwrap();
// Resource mightn't've been added yet, so let's just insert / replace it.
commands.insert_resource(SelectedBlock(cell.id.clone()));
}
fn on_cell_over(
event: On<Pointer<Over>>,
mut cells: Query<(&BlockSelectionCell, &mut BorderColor)>,
selected_block: Option<Res<SelectedBlock>>,
) {
// SAFETY: Entity is known to have these components.
let (cell, mut color) = cells.get_mut(event.entity).unwrap();
let is_selected = selected_block.is_some_and(|s| cell.id == **s);
*color = if is_selected { WHITE } else { GRAY }.into();
}
fn on_cell_out(
event: On<Pointer<Out>>,
mut cells: Query<(&BlockSelectionCell, &mut BorderColor)>,
selected_block: Option<Res<SelectedBlock>>,
) {
// SAFETY: Entity is known to have these components.
let (cell, mut color) = cells.get_mut(event.entity).unwrap();
let is_selected = selected_block.is_some_and(|s| cell.id == **s);
*color = if is_selected { WHITE } else { BLACK }.into();
}

@ -1,10 +1,15 @@
use bevy::prelude::*; use bevy::prelude::*;
pub mod block_selection;
pub mod crosshair; pub mod crosshair;
pub mod loading_screen; pub mod loading_screen;
pub(super) fn plugin(app: &mut App) { pub(super) fn plugin(app: &mut App) {
app.add_plugins((crosshair::plugin, loading_screen::plugin)); app.add_plugins((
block_selection::plugin,
crosshair::plugin,
loading_screen::plugin,
));
// Make entities require the `Pickable` component if // Make entities require the `Pickable` component if
// they should be considered for the picking system. // they should be considered for the picking system.

@ -24,9 +24,6 @@ pub struct Block {
} }
impl Block { impl Block {
pub const PLATFORM: Identifier<Block> = Identifier::new_const("vampire_black");
pub const DEFAULT: Identifier<Block> = Identifier::new_const("default_cube_gray");
pub fn pos(&self) -> BlockPos { pub fn pos(&self) -> BlockPos {
self.pos self.pos
} }

Loading…
Cancel
Save