ui: Beginning of UI-related stuff

- UiRoot component
- WindowSize resource and update_window_size system
- UiInventory and UiInventorySlot components
- UiInventory now shows player inventory slots
This commit is contained in:
Alexey 2026-03-09 13:59:51 +03:00
commit ab993be476
6 changed files with 183 additions and 10 deletions

View file

@ -6,7 +6,7 @@ version = "0.1.0"
edition = "2024"
[dependencies]
bevy = { version = "0.18.0" }
bevy = { version = "0.18.0", features = ["debug"] }
bevy_common_assets = { version = "0.15.0", features = ["toml"] }
bevy_input = { version = "0.18.0", features = ["serde", "serialize"] }
leafwing-input-manager = "0.20.0"

View file

@ -1,12 +1,17 @@
use bevy::prelude::*;
pub mod item;
pub mod ui;
#[derive(Component)]
pub struct Inventory {
pub size: UVec2,
}
/// Marker that this inventory will show up when UI is built
#[derive(Component)]
pub struct ActiveInventory;
impl Inventory {
pub fn new(size: UVec2) -> Self {
Self { size }

93
src/inventory/ui.rs Normal file
View file

@ -0,0 +1,93 @@
use bevy::prelude::*;
use crate::{inventory::{ActiveInventory, Inventory}, ui::{UiRoot, WindowSize}};
const UI_SLOT_ASSET_PATH: &'static str = "sprites/ui/inventory_slot.png";
#[derive(Component)]
#[require(Node)]
pub struct UiInventory;
#[derive(Component)]
#[require(Node, ImageNode)]
pub struct UiInventorySlot(UVec2);
fn ui_inventory_bundle(inventory: &Inventory, window_size: &Res<WindowSize>) -> impl Bundle {
let window_ratio = window_size.aspect_ratio();
let (width, height) = {
if window_ratio >= 1. {
(auto(), percent(100))
} else {
(percent(100), auto())
}
};
(
UiInventory,
Node {
align_self: AlignSelf::Center,
align_content: AlignContent::Center,
display: Display::Grid,
width,
height,
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_rows: vec![GridTrack::percent(100. / inventory.size.y as f32)],
..default()
},
)
}
fn inventory_slot_bundle(x: u32, y: u32, width: u32, height: u32, image: Handle<Image>) -> impl Bundle {
(
UiInventorySlot(UVec2::new(x, y)),
ImageNode {
image,
image_mode: NodeImageMode::Stretch,
..default()
},
Node {
width: percent(100.),
height: percent(100.),
grid_column: GridPlacement::start(x as i16 + 1),
grid_row: GridPlacement::start(y as i16 + 1),
..default()
},
)
}
pub fn setup_ui_inventory(
mut commands: Commands,
asset_server: Res<AssetServer>,
inventory_query: Query<(&Inventory, Option<&Children>), With<ActiveInventory>>,
root_query: Query<Entity, With<UiRoot>>,
window_size: Res<WindowSize>,
) {
let Ok(root) = root_query.single() else {
error!("Query contains more than one UiRoot");
return;
};
let ui_slot_image: Handle<Image> = asset_server.load(UI_SLOT_ASSET_PATH);
for (inventory, _children) in inventory_query {
let inventory_entity = commands.spawn(ui_inventory_bundle(inventory, &window_size))
.with_children(|commands| {
for x in 0..inventory.size.x {
for y in 0..inventory.size.y {
commands.spawn(inventory_slot_bundle(x, y, inventory.size.x, inventory.size.y, ui_slot_image.clone()));
}
}
}).id();
commands.entity(root).add_child(inventory_entity);
// for simplicity we'll show only first inventory
break;
}
}
pub fn clear_ui_inventory(
mut commands: Commands,
inventory_query: Query<Entity, With<UiInventory>>,
) {
for entity in inventory_query {
commands.entity(entity).despawn();
}
}

View file

@ -2,6 +2,7 @@ pub mod player;
pub mod layout;
pub mod input;
pub mod inventory;
pub mod ui;
#[cfg(test)]
mod tests;
@ -19,6 +20,13 @@ pub enum InputAction {
Interact,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)]
pub enum GameState {
#[default]
Running,
Inventory,
}
impl InputAction {
pub fn default_input_map() -> InputMap<Self> {
let input_map = InputMap::default()
@ -51,12 +59,20 @@ fn camera_bundle() -> impl Bundle {
fn setup_global(mut commands: Commands) {
commands.spawn(camera_bundle());
commands.spawn(ui::UiRoot::new());
}
impl Plugin for ExpeditionPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(input::InputAssetPlugin::<InputAction>::default())
.init_state::<GameState>()
.insert_resource(ui::WindowSize::default())
.add_systems(Startup, (player::setup_player, setup_global))
.add_systems(Update, player::handle_input);
.add_systems(Update, (
player::handle_input,
ui::update_window_size,
))
.add_systems(OnEnter(GameState::Inventory), inventory::ui::setup_ui_inventory)
.add_systems(OnExit(GameState::Inventory), inventory::ui::clear_ui_inventory);
}
}

View file

@ -1,7 +1,7 @@
use bevy::prelude::*;
use leafwing_input_manager::prelude::*;
use crate::InputAction as Action;
use crate::{GameState, InputAction as Action, inventory::{ActiveInventory, Inventory}};
#[derive(Component)]
pub struct Player {
@ -18,6 +18,8 @@ fn player_bundle(asset_server: &Res<AssetServer>) -> impl Bundle {
Sprite::from_image(image),
Transform::from_xyz(0f32, 0f32, 1f32),
Action::default_input_map(),
Inventory::new(UVec2::new(12, 8)),
ActiveInventory,
)
}
@ -27,10 +29,19 @@ pub fn setup_player(mut commands: Commands, asset_server: Res<AssetServer>) {
pub fn handle_input(
time: Res<Time>,
mut player: Query<(&Player, &ActionState<Action>, &mut Transform, &mut Sprite)>,
state: Res<State<GameState>>,
mut next_state: ResMut<NextState<GameState>>,
mut player: Query<(&Player, &mut ActionState<Action>, &mut Transform, &mut Sprite)>,
) {
let player = player.single_mut().expect("Player should be single");
let (Player {speed}, action_state, mut transform, mut sprite) = player;
match state.get() {
GameState::Running => {
let (Player {speed}, mut action_state, mut transform, mut sprite) = player;
if action_state.just_released(&Action::ToggleInventory) {
next_state.set(GameState::Inventory);
action_state.reset(&Action::ToggleInventory);
}
let direction = action_state.clamped_value(&Action::Move);
@ -38,4 +49,13 @@ pub fn handle_input(
if direction != 0f32 {
sprite.flip_x = direction < 0f32;
}
},
GameState::Inventory => {
let (_, mut action_state, _, _) = player;
if action_state.just_released(&Action::ToggleInventory) {
next_state.set(GameState::Running);
action_state.reset(&Action::ToggleInventory);
}
},
}
}

39
src/ui.rs Normal file
View file

@ -0,0 +1,39 @@
use bevy::{prelude::*, window::WindowResized};
#[derive(Component)]
#[require(Node)]
pub struct UiRoot;
#[derive(Resource, Deref, DerefMut, Default)]
pub struct WindowSize(Vec2);
impl WindowSize {
pub fn aspect_ratio(&self) -> f32 {
self.x as f32 / self.y as f32
}
}
impl UiRoot {
pub fn new() -> impl Bundle {
(
UiRoot,
Node {
width: percent(100.),
height: percent(100.),
align_self: AlignSelf::Center,
justify_self: JustifySelf::Center,
..default()
},
)
}
}
pub fn update_window_size(
mut resize_reader: MessageReader<WindowResized>,
mut window_size: ResMut<WindowSize>,
) {
for event in resize_reader.read() {
window_size.x = event.width;
window_size.y = event.height;
}
}