mirror of
https://github.com/bevyengine/bevy
synced 2024-12-04 18:39:13 +00:00
UI picking example
This commit is contained in:
parent
423ece13c9
commit
6c58666ed2
2 changed files with 219 additions and 87 deletions
12
Cargo.toml
12
Cargo.toml
|
@ -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"
|
||||||
|
|
|
@ -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| {}
|
|
||||||
// }
|
|
||||||
|
|
Loading…
Reference in a new issue