generated from 2ndbeam/bevy-template
ui: Multiple inventories support
- Added scrollable container for inventories - Fixed slots being visibly hovered when dragging items
This commit is contained in:
parent
0c8259583a
commit
337986d2b9
5 changed files with 113 additions and 43 deletions
21
Cargo.lock
generated
21
Cargo.lock
generated
|
|
@ -952,6 +952,7 @@ dependencies = [
|
||||||
"bevy_transform",
|
"bevy_transform",
|
||||||
"bevy_ui",
|
"bevy_ui",
|
||||||
"bevy_ui_render",
|
"bevy_ui_render",
|
||||||
|
"bevy_ui_widgets",
|
||||||
"bevy_utils",
|
"bevy_utils",
|
||||||
"bevy_window",
|
"bevy_window",
|
||||||
"bevy_winit",
|
"bevy_winit",
|
||||||
|
|
@ -1575,6 +1576,26 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bevy_ui_widgets"
|
||||||
|
version = "0.18.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6a63cb818b0de41bdb14990e0ce1aaaa347f871750ab280f80c427e83d72712"
|
||||||
|
dependencies = [
|
||||||
|
"accesskit",
|
||||||
|
"bevy_a11y",
|
||||||
|
"bevy_app",
|
||||||
|
"bevy_camera",
|
||||||
|
"bevy_ecs",
|
||||||
|
"bevy_input",
|
||||||
|
"bevy_input_focus",
|
||||||
|
"bevy_log",
|
||||||
|
"bevy_math",
|
||||||
|
"bevy_picking",
|
||||||
|
"bevy_reflect",
|
||||||
|
"bevy_ui",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bevy_utils"
|
name = "bevy_utils"
|
||||||
version = "0.18.0"
|
version = "0.18.0"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy = { version = "0.18.0", features = ["bevy_remote", "debug"] }
|
bevy = { version = "0.18.0", features = ["bevy_remote", "debug", "experimental_bevy_ui_widgets"] }
|
||||||
bevy_common_assets = { version = "0.15.0", features = ["toml"] }
|
bevy_common_assets = { version = "0.15.0", features = ["toml"] }
|
||||||
bevy_input = { version = "0.18.0", features = ["serde", "serialize"] }
|
bevy_input = { version = "0.18.0", features = ["serde", "serialize"] }
|
||||||
leafwing-input-manager = "0.20.0"
|
leafwing-input-manager = "0.20.0"
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
use std::f32::consts::FRAC_PI_2;
|
use std::f32::consts::FRAC_PI_2;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::{ecs::relationship::RelatedSpawner, prelude::*, ui_widgets::{ControlOrientation, CoreScrollbarThumb, Scrollbar}};
|
||||||
|
|
||||||
use crate::{inventory::{ActiveInventory, Inventory, item::Item}, ui::{UiRoot, UiRotateEvent, WindowSize}};
|
use crate::{inventory::{ActiveInventory, Inventory, item::Item}, ui::{UiRoot, UiRotateEvent}};
|
||||||
|
|
||||||
const UI_SLOT_ASSET_PATH: &'static str = "sprites/ui/inventory_slot.png";
|
const UI_SLOT_ASSET_PATH: &'static str = "sprites/ui/inventory_slot.png";
|
||||||
const TEMP_ITEM_PATH: &'static str = "sprites/items/choco_bar.png";
|
const TEMP_ITEM_PATH: &'static str = "sprites/items/choco_bar.png";
|
||||||
|
|
||||||
|
#[derive(Component, Reflect)]
|
||||||
|
#[require(Node)]
|
||||||
|
pub struct UiInventoryManager;
|
||||||
|
|
||||||
#[derive(Component, Reflect)]
|
#[derive(Component, Reflect)]
|
||||||
#[require(Node)]
|
#[require(Node)]
|
||||||
pub struct UiInventory(pub Entity);
|
pub struct UiInventory(pub Entity);
|
||||||
|
|
@ -53,6 +57,12 @@ fn update_ui_node(item: &Item, mut node: Mut<'_, Node>, mut ui_transform: Mut<'_
|
||||||
ui_transform.rotation = new_ui_transform.rotation;
|
ui_transform.rotation = new_ui_transform.rotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reset_slots_colors(query: Query<&mut ImageNode, With<UiInventorySlot>>) {
|
||||||
|
for mut image in query {
|
||||||
|
image.color = Color::WHITE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_slot_over(e: On<Pointer<Over>>, mut query: Query<&mut ImageNode, With<UiInventorySlot>>) {
|
fn on_slot_over(e: On<Pointer<Over>>, mut query: Query<&mut ImageNode, With<UiInventorySlot>>) {
|
||||||
if let Ok(mut image) = query.get_mut(e.event_target()) {
|
if let Ok(mut image) = query.get_mut(e.event_target()) {
|
||||||
image.color = Color::WHITE.darker(0.3);
|
image.color = Color::WHITE.darker(0.3);
|
||||||
|
|
@ -155,25 +165,19 @@ fn on_item_drag_drop(
|
||||||
temp_item.position = Some(*new_position);
|
temp_item.position = Some(*new_position);
|
||||||
|
|
||||||
if inventory.can_replace(item_query.as_readonly(), items, *item_entity, temp_item) {
|
if inventory.can_replace(item_query.as_readonly(), items, *item_entity, temp_item) {
|
||||||
info!("Replaced item");
|
|
||||||
let mut item = item_query.get_mut(*item_entity).unwrap();
|
let mut item = item_query.get_mut(*item_entity).unwrap();
|
||||||
item.position = temp_item.position;
|
item.position = temp_item.position;
|
||||||
item.size = temp_item.size;
|
item.size = temp_item.size;
|
||||||
item.rotated = temp_item.rotated;
|
item.rotated = temp_item.rotated;
|
||||||
commands.entity(ui_item_entity).insert(ChildOf(event.event_target()));
|
commands.entity(ui_item_entity).insert(ChildOf(event.event_target()));
|
||||||
|
commands.entity(*item_entity).insert(ChildOf(*inventory_id));
|
||||||
update_ui_node(item.as_ref(), node, ui_transform);
|
update_ui_node(item.as_ref(), node, ui_transform);
|
||||||
} else {
|
} else {
|
||||||
if let Ok(item) = item_query.get(*item_entity) {
|
if let Ok(item) = item_query.get(*item_entity) {
|
||||||
update_ui_node(item, node, ui_transform);
|
update_ui_node(item, node, ui_transform);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/*
|
commands.run_system_cached(reset_slots_colors);
|
||||||
if inventory.can_move(item_query.as_readonly(), items, *item_entity, *new_position) {
|
|
||||||
let mut item = item_query.get_mut(*item_entity).unwrap();
|
|
||||||
item.position = Some(*new_position);
|
|
||||||
commands.entity(ui_item_entity).insert(ChildOf(event.event_target()));
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_ui_rotate(
|
pub fn on_ui_rotate(
|
||||||
|
|
@ -218,27 +222,73 @@ pub fn on_ui_rotate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ui_manager_bundle(children: Vec<Entity>) -> impl Bundle {
|
||||||
|
(
|
||||||
|
UiInventoryManager,
|
||||||
|
Node {
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
width: percent(50.),
|
||||||
|
height: percent(100.),
|
||||||
|
scrollbar_width: 8.,
|
||||||
|
display: Display::Grid,
|
||||||
|
grid_template_columns: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)],
|
||||||
|
grid_template_rows: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)],
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Pickable::IGNORE,
|
||||||
|
GlobalZIndex::default(),
|
||||||
|
Children::spawn(SpawnWith(move |parent: &mut RelatedSpawner<ChildOf>| {
|
||||||
|
let scroll_area_id = parent.spawn((
|
||||||
|
Node {
|
||||||
|
width: percent(100.),
|
||||||
|
height: percent(100.),
|
||||||
|
display: Display::Flex,
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
justify_content: JustifyContent::SpaceAround,
|
||||||
|
overflow: Overflow::scroll_y(),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
ScrollPosition(Vec2::ZERO),
|
||||||
|
)).add_children(children.as_slice()).id();
|
||||||
|
parent.spawn((
|
||||||
|
Node {
|
||||||
|
min_width: px(8),
|
||||||
|
grid_row: GridPlacement::start(1),
|
||||||
|
grid_column: GridPlacement::start(2),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Scrollbar {
|
||||||
|
orientation: ControlOrientation::Vertical,
|
||||||
|
target: scroll_area_id,
|
||||||
|
min_thumb_length: 8.0,
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::hsl(0., 0., 0.5)),
|
||||||
|
children![(
|
||||||
|
Node {
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
border_radius: BorderRadius::all(px(4.)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::hsl(0., 0., 0.3)),
|
||||||
|
CoreScrollbarThumb,
|
||||||
|
)],
|
||||||
|
));
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn ui_inventory_bundle(
|
fn ui_inventory_bundle(
|
||||||
inventory: &Inventory,
|
inventory: &Inventory,
|
||||||
inventory_entity: Entity,
|
inventory_entity: Entity,
|
||||||
window_size: &Res<WindowSize>,
|
|
||||||
) -> impl Bundle {
|
) -> impl Bundle {
|
||||||
let window_ratio = window_size.aspect_ratio();
|
|
||||||
let (width, height) = {
|
|
||||||
if window_ratio >= 1. {
|
|
||||||
(auto(), percent(100))
|
|
||||||
} else {
|
|
||||||
(percent(100), auto())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
(
|
(
|
||||||
UiInventory(inventory_entity),
|
UiInventory(inventory_entity),
|
||||||
Node {
|
Node {
|
||||||
align_self: AlignSelf::Center,
|
align_self: AlignSelf::Stretch,
|
||||||
align_content: AlignContent::Center,
|
align_content: AlignContent::Center,
|
||||||
display: Display::Grid,
|
display: Display::Grid,
|
||||||
width,
|
width: percent(100.),
|
||||||
height,
|
|
||||||
aspect_ratio: Some(inventory.size.x as f32 / inventory.size.y as f32),
|
aspect_ratio: Some(inventory.size.x as f32 / inventory.size.y as f32),
|
||||||
grid_auto_columns: vec![GridTrack::percent(100. / inventory.size.x as f32)],
|
grid_auto_columns: vec![GridTrack::percent(100. / inventory.size.x as f32)],
|
||||||
grid_auto_rows: vec![GridTrack::percent(100. / inventory.size.y as f32)],
|
grid_auto_rows: vec![GridTrack::percent(100. / inventory.size.y as f32)],
|
||||||
|
|
@ -264,6 +314,7 @@ fn inventory_slot_bundle(x: u32, y: u32, image: Handle<Image>) -> impl Bundle {
|
||||||
height: percent(100.),
|
height: percent(100.),
|
||||||
grid_column: GridPlacement::start(x as i16 + 1),
|
grid_column: GridPlacement::start(x as i16 + 1),
|
||||||
grid_row: GridPlacement::start(y as i16 + 1),
|
grid_row: GridPlacement::start(y as i16 + 1),
|
||||||
|
aspect_ratio: Some(1.),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
Pickable {
|
Pickable {
|
||||||
|
|
@ -311,7 +362,6 @@ pub fn setup_ui_inventory(
|
||||||
inventory_query: Query<(Entity, &Inventory, Option<&Children>), With<ActiveInventory>>,
|
inventory_query: Query<(Entity, &Inventory, Option<&Children>), With<ActiveInventory>>,
|
||||||
item_query: Query<&Item>,
|
item_query: Query<&Item>,
|
||||||
root_query: Query<Entity, With<UiRoot>>,
|
root_query: Query<Entity, With<UiRoot>>,
|
||||||
window_size: Res<WindowSize>,
|
|
||||||
) {
|
) {
|
||||||
let Ok(root) = root_query.single() else {
|
let Ok(root) = root_query.single() else {
|
||||||
error!("Query contains more than one UiRoot");
|
error!("Query contains more than one UiRoot");
|
||||||
|
|
@ -319,7 +369,8 @@ pub fn setup_ui_inventory(
|
||||||
};
|
};
|
||||||
let ui_slot_image: Handle<Image> = asset_server.load(UI_SLOT_ASSET_PATH);
|
let ui_slot_image: Handle<Image> = asset_server.load(UI_SLOT_ASSET_PATH);
|
||||||
let temp_item_image: Handle<Image> = asset_server.load(TEMP_ITEM_PATH);
|
let temp_item_image: Handle<Image> = asset_server.load(TEMP_ITEM_PATH);
|
||||||
for (inventory_entity, inventory, children) in inventory_query {
|
let mut inventory_ids = Vec::new();
|
||||||
|
for (inventory_entity, inventory, children) in inventory_query.iter().sort::<Entity>() {
|
||||||
let items = match children {
|
let items = match children {
|
||||||
Some(children) => {
|
Some(children) => {
|
||||||
children.iter().filter_map(|item_entity| {
|
children.iter().filter_map(|item_entity| {
|
||||||
|
|
@ -334,7 +385,7 @@ pub fn setup_ui_inventory(
|
||||||
}
|
}
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
};
|
};
|
||||||
let inventory_entity = commands.spawn(ui_inventory_bundle(inventory, inventory_entity, &window_size))
|
let inventory_id = commands.spawn(ui_inventory_bundle(inventory, inventory_entity))
|
||||||
.with_children(|commands| {
|
.with_children(|commands| {
|
||||||
for x in 0..inventory.size.x { for y in 0..inventory.size.y {
|
for x in 0..inventory.size.x { for y in 0..inventory.size.y {
|
||||||
let mut slot_commands = commands.spawn(inventory_slot_bundle(x, y, ui_slot_image.clone()));
|
let mut slot_commands = commands.spawn(inventory_slot_bundle(x, y, ui_slot_image.clone()));
|
||||||
|
|
@ -353,19 +404,16 @@ pub fn setup_ui_inventory(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} }
|
} }
|
||||||
})
|
}).id();
|
||||||
.id();
|
inventory_ids.push(inventory_id);
|
||||||
commands.entity(root)
|
|
||||||
.add_child(inventory_entity);
|
|
||||||
|
|
||||||
// for simplicity we'll show only first inventory
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
let inventory_manager = commands.spawn(ui_manager_bundle(inventory_ids)).id();
|
||||||
|
commands.entity(root).add_child(inventory_manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_ui_inventory(
|
pub fn clear_ui_inventory(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
inventory_query: Query<Entity, With<UiInventory>>,
|
inventory_query: Query<Entity, With<UiInventoryManager>>,
|
||||||
) {
|
) {
|
||||||
for entity in inventory_query {
|
for entity in inventory_query {
|
||||||
commands.entity(entity).despawn();
|
commands.entity(entity).despawn();
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ pub mod ui;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::{prelude::*, ui_widgets::ScrollbarPlugin};
|
||||||
use leafwing_input_manager::prelude::*;
|
use leafwing_input_manager::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
@ -86,6 +86,7 @@ impl Plugin for ExpeditionPlugin {
|
||||||
app.add_plugins((
|
app.add_plugins((
|
||||||
input::InputAssetPlugin::<InputAction>::default(),
|
input::InputAssetPlugin::<InputAction>::default(),
|
||||||
input::InputAssetPlugin::<UiAction>::default(),
|
input::InputAssetPlugin::<UiAction>::default(),
|
||||||
|
ScrollbarPlugin,
|
||||||
))
|
))
|
||||||
.init_state::<GameState>()
|
.init_state::<GameState>()
|
||||||
.insert_resource(ui::WindowSize::default())
|
.insert_resource(ui::WindowSize::default())
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,12 @@ fn player_bundle(asset_server: &Res<AssetServer>) -> impl Bundle {
|
||||||
Sprite::from_image(image),
|
Sprite::from_image(image),
|
||||||
Transform::from_xyz(0f32, 0f32, 1f32),
|
Transform::from_xyz(0f32, 0f32, 1f32),
|
||||||
Action::default_input_map(),
|
Action::default_input_map(),
|
||||||
Inventory::new(UVec2::new(4, 4)),
|
|
||||||
ActiveInventory,
|
|
||||||
Name::new("Player"),
|
Name::new("Player"),
|
||||||
|
children![
|
||||||
|
(Inventory::new(UVec2::new(6, 2)), ActiveInventory),
|
||||||
|
(Inventory::new(UVec2::new(5, 3)), ActiveInventory),
|
||||||
|
(Inventory::new(UVec2::new(4, 4)), ActiveInventory),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,7 +38,7 @@ pub fn try_insert_item(
|
||||||
) {
|
) {
|
||||||
let mut item = Item::new(UVec2::new(1, 1));
|
let mut item = Item::new(UVec2::new(1, 1));
|
||||||
let name = Name::new(format!("Item {}x{}", item.size.x, item.size.y));
|
let name = Name::new(format!("Item {}x{}", item.size.x, item.size.y));
|
||||||
for (entity, inventory, children) in inventory_query {
|
for (entity, inventory, children) in inventory_query.iter().sort::<Entity>() {
|
||||||
let children = match children {
|
let children = match children {
|
||||||
Some(children) => &children[..],
|
Some(children) => &children[..],
|
||||||
None => &[],
|
None => &[],
|
||||||
|
|
@ -48,13 +51,10 @@ pub fn try_insert_item(
|
||||||
item.position = Some(position);
|
item.position = Some(position);
|
||||||
info!("Spawning item {item:?}");
|
info!("Spawning item {item:?}");
|
||||||
commands.entity(entity).with_child((item, name));
|
commands.entity(entity).with_child((item, name));
|
||||||
|
break;
|
||||||
},
|
},
|
||||||
None => {
|
None => (),
|
||||||
warn!("Inventory does not have space for {}", item.size);
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
// only first inventory
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue