UI picking example

This commit is contained in:
Rich Churcher 2024-11-17 17:16:01 +13:00
parent 423ece13c9
commit 6c58666ed2
2 changed files with 219 additions and 87 deletions

View file

@ -3764,7 +3764,17 @@ name = "Sprite Picking"
description = "Demonstrates picking sprites and sprite atlases" description = "Demonstrates picking sprites and sprite atlases"
category = "Picking" category = "Picking"
wasm = true wasm = true
required-features = ["bevy_sprite_picking_backend"]
[[example]]
name = "ui_picking"
path = "examples/picking/ui_picking.rs"
doc-scrape-examples = true
[package.metadata.example.ui_picking]
name = "UI Picking"
description = "Demonstrates picking in a UI context"
category = "Picking"
wasm = true
[[example]] [[example]]
name = "animation_masks" name = "animation_masks"

View file

@ -1,37 +1,53 @@
//! Demonstrates the use of picking in Bevy's UI. //! Demonstrates the use of picking in Bevy's UI. Also shows use of one-shot systems to spawn
//! boilerplate UI on-demand.
//! //!
//! This example displays a simple inventory system and an animated sprite. Items can be dragged //! This example displays a simple inventory. Items can be dragged and dropped within the
//! and dropped within the inventory, or onto the sprite with differing effects depending on the //! inventory.
//! item.
//!
//! The implementation is not intended for serious use as a comprehensive inventory system!
use bevy::prelude::*; use bevy::{ecs::system::RunSystemOnce, prelude::*, window::PrimaryWindow};
use std::fmt::Debug;
fn main() { fn main() {
App::new() App::new()
.init_resource::<DragDetails>()
.add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
.add_systems(Startup, (setup, spawn_text)) .add_systems(Startup, (setup, spawn_text))
.add_systems(Update, drag_inventory_items)
.run(); .run();
} }
/// Set up some UI to manipulate. #[derive(Resource, Copy, Clone, Default)]
fn setup(asset_server: Res<AssetServer>, mut commands: Commands) { struct DragDetails {
// We need a simple camera to display our UI. pub drag_origin: Option<Entity>,
commands.spawn((Name::new("Camera"), Camera2d)); pub drop_target: Option<Entity>,
}
let inventory_node = Node { /// Marker component for the root inventory UI `Node`.
margin: UiRect::all(Val::Px(5.)), #[derive(Component)]
height: Val::Px(50.), struct Inventory;
width: Val::Px(50.),
padding: UiRect::all(Val::Px(5.)), /// Marker component for an individual slot within the Inventory.
..default() #[derive(Component)]
}; struct InventorySlot;
// Spawn some arbitrarily-positioned inventory slots as UI nodes.
commands /// Marker component for entities which can be dragged.
.spawn(( #[derive(Component)]
struct Dragable;
/// Marker component for entities which are currently being dragged, and should follow the pointer.
#[derive(Component)]
struct Dragging;
/// Set up some UI to manipulate.
///
/// Systems that accept a &mut World argument implement the `Command` trait. We use this rather
/// than a system accepting `Commands`, because we want access to one-shot systems below.
fn setup(world: &mut World) {
// We need a simple camera to display our UI.
world.spawn((Name::new("Camera"), Camera2d));
world.spawn((
Name::new("Inventory"), Name::new("Inventory"),
Inventory,
// This first node acts like a container. You can think of it as the box in which // This first node acts like a container. You can think of it as the box in which
// inventory slots are arranged. It's useful to remember that picking events can // inventory slots are arranged. It's useful to remember that picking events can
// "bubble" up through layers of UI, so if required this parent node can act on events // "bubble" up through layers of UI, so if required this parent node can act on events
@ -46,60 +62,176 @@ fn setup(asset_server: Res<AssetServer>, mut commands: Commands) {
..default() ..default()
}, },
BackgroundColor(Color::WHITE.with_alpha(0.1)), BackgroundColor(Color::WHITE.with_alpha(0.1)),
))
.observe(|t: Trigger<Pointer<Over>>| {
dbg!(t);
})
.with_children(|parent| {
parent
.spawn((
Name::new("Inventory Slot A"),
inventory_node.clone(),
BackgroundColor(Color::WHITE.with_alpha(0.5)),
))
.with_children(|parent| {
parent.spawn(ImageNode::new(
asset_server.load("textures/rpg/props/generic-rpg-loot01.png"),
)); ));
})
.observe(drag_handler::<Pointer<DragEnd>>());
// To reduce boilerplate, let's spawn the inventory slots using one-shot systems. The slots
// are each identical, and the UI Layout engine takes care of arranging them for us in the
// above container. The `.unwrap()` is for simplicity in this example, whereas you might
// use `.expect()`.
world.run_system_once(spawn_inventory_slot).unwrap();
world.run_system_once(spawn_inventory_slot).unwrap();
world.run_system_once(spawn_inventory_slot).unwrap();
world.run_system_once(spawn_inventory_slot).unwrap();
// Now we need to add some items to the inventory.
world.run_system_once(add_items_to_inventory).unwrap();
}
/// Spawn individual inventory slots, using the `Inventory` root UI node as its parent.
fn spawn_inventory_slot(mut commands: Commands, inventory: Single<Entity, With<Inventory>>) {
commands.entity(*inventory).with_children(|parent| {
parent parent
.spawn(( .spawn((
Name::new("Inventory Slot B"), Name::new("Inventory Slot"),
inventory_node.clone(), Node {
margin: UiRect::all(Val::Px(5.)),
height: Val::Px(50.),
width: Val::Px(50.),
padding: UiRect::all(Val::Px(5.)),
..default()
},
InventorySlot,
BackgroundColor(Color::WHITE.with_alpha(0.5)), BackgroundColor(Color::WHITE.with_alpha(0.5)),
)) ))
.observe(drag_handler::<Pointer<DragStart>>()) // Update the `DragDetails` as the UI `Node`s receive picking events.
.with_children(|parent| { .observe(set_drop_target())
parent.spawn((ImageNode::new( .observe(clear_drop_target());
asset_server.load("textures/rpg/props/generic-rpg-loot02.png"),
),));
}); });
}
/// This observer detects the beginning of a drag-and-drop motion and takes care of tasks such as
/// detaching the target from its parent and allowing the node to be moved.
fn drag_start() -> impl Fn(
Trigger<Pointer<DragStart>>,
Commands,
Query<(Entity, &mut Node, &mut Transform, &Parent), With<Dragable>>,
ResMut<DragDetails>,
) {
move |ev, mut commands, mut dragable, mut drag_details| {
let Ok((item, mut item_node, mut transform, parent)) = dragable.get_mut(ev.target) else {
return;
};
// Record which slot we started from, in case we need to drop the item back again.
drag_details.drag_origin = Some(parent.get());
// Absolutely-positioned items can be made to follow the cursor.
item_node.position_type = PositionType::Absolute;
// TODO: is there a better solution for the scaling problem? Without this, and without a
// parent element, the asset will be un-scaled (and appear tiny). Copying its current
// scaling doesn't seem to be helpful.
transform.scale *= 4.;
// Add a marker to tell our systems that the node is being dragged, and remove it from the
// list of children on its current parent entity.
commands.entity(item).insert(Dragging).remove_parent();
}
}
/// This observer detects the end of our drag-and-drop, and checks the `DragDetails` resource to
/// decide where to place the node: either in a new `InventorySlot`, or back in the one it came
/// from.
fn drag_end() -> impl Fn(
Trigger<Pointer<DragEnd>>,
Commands,
Query<(Entity, &mut Node, &mut Transform), With<Dragging>>,
ResMut<DragDetails>,
) {
move |_ev, mut commands, mut dragging_item, drag_details| {
let Ok((item, mut item_node, mut transform)) = dragging_item.get_single_mut() else {
return;
};
// Not dragging anymore!
commands.entity(item).remove::<Dragging>();
// We either want the new location, or the original one. In theory, both being `None`
// shouldn't happen.
let Some(drop_target) = drag_details.drop_target.or(drag_details.drag_origin) else {
return;
};
// Whichever slot we pick, add the `Node` as a child entity.
commands.entity(drop_target).add_child(item);
// Re-integrate the `Node` into the relatively-positioned layout.
item_node.position_type = PositionType::Relative;
item_node.left = Val::Auto;
item_node.top = Val::Auto;
transform.scale /= 4.;
}
}
/// This observer updates our record of which inventory slot is the current target. We
/// should also set the `drop_target` to `None` if the pointer is hovering over empty
/// space, for example. Even though all of our hovered `Node`s receive picking events, we don't
/// need to check the type of the `ev.target` because we've only attached this observer to
/// `InventorySlot`s.
fn set_drop_target() -> impl Fn(Trigger<Pointer<Over>>, ResMut<DragDetails>) {
move |ev, mut details| {
details.drop_target = Some(ev.target);
}
}
/// This observer ensures that even if we'd previously hovered over an `InventorySlot`, we clear
/// the drop target again once the pointer leaves it.
fn clear_drop_target() -> impl Fn(Trigger<Pointer<Out>>, ResMut<DragDetails>) {
move |_ev, mut details| {
details.drop_target = None;
}
}
/// Update the transform of the dragged item to follow the cursor.
fn drag_inventory_items(
mut dragging: Query<&mut Node, With<Dragging>>,
primary_window: Single<&Window, With<PrimaryWindow>>,
) {
let Ok(mut item_node) = dragging.get_single_mut() else {
return;
};
let Some(cursor_pos) = primary_window.cursor_position() else {
return;
};
// We can't assign exact values with a `PositionType::Absolute` UI node. However, we can use
// distance from top of viewport and distance from left of viewport to accomplish the same
// thing.
item_node.left = Val::Px(cursor_pos.x);
item_node.top = Val::Px(cursor_pos.y);
}
/// Spawn two arbitrary inventory items. Here we'll just pick the first two slots.
fn add_items_to_inventory(
asset_server: Res<AssetServer>,
mut commands: Commands,
slots: Query<Entity, With<InventorySlot>>,
) {
let paths = [
"textures/rpg/props/generic-rpg-loot01.png",
"textures/rpg/props/generic-rpg-loot02.png",
];
for (i, slot) in slots.iter().enumerate().take(2) {
commands.entity(slot).with_children(|parent| {
parent parent
.spawn(( .spawn((
Name::new("Inventory Slot C"), Dragable,
inventory_node.clone(), // This node serves solely to be dragged! It provides the transform, using required
BackgroundColor(Color::WHITE.with_alpha(0.5)), // components.
Node::default(),
ImageNode::new(asset_server.load(paths[i])),
)) ))
.observe(drag_handler::<Pointer<DragEnd>>()); .observe(drag_start())
.observe(drag_end());
parent
.spawn((
Name::new("Inventory Slot C"),
inventory_node.clone(),
BackgroundColor(Color::WHITE.with_alpha(0.5)),
))
.observe(drag_handler::<Pointer<DragStart>>());
}); });
}
} }
/// Display instructions. /// Display instructions.
fn spawn_text(mut commands: Commands) { fn spawn_text(mut commands: Commands) {
commands.spawn(( commands.spawn((
Name::new("Instructions"), Name::new("Instructions"),
Text::new("Drag and drop birds within the inventory slots."), Text::new("Drag and drop items within the inventory slots."),
Node { Node {
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
top: Val::Px(12.), top: Val::Px(12.),
@ -108,13 +240,3 @@ fn spawn_text(mut commands: Commands) {
}, },
)); ));
} }
fn drag_handler<E: Debug + Clone + Reflect>() -> impl Fn(Trigger<E>, Query<&mut Sprite>) {
move |ev, _sprites| {
dbg!(ev);
}
}
//
// fn drop_handler<E: Debug + Clone + Reflect>() -> impl Fn(Trigger<E>) {
// move |ev| {}
// }