diff --git a/Cargo.toml b/Cargo.toml index 000bebe13496d..3fd0f8bb80c6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4416,6 +4416,17 @@ description = "Example for cooldown on button clicks" category = "Usage" wasm = true +[[example]] +name = "healthbar" +path = "examples/usage/healthbar.rs" +doc-scrape-examples = true + +[package.metadata.example.healthbar] +name = "Healthbar" +description = "A scene showcasing world-to-viewport UI" +category = "Usage" +wasm = true + [[example]] name = "hotpatching_systems" path = "examples/ecs/hotpatching_systems.rs" diff --git a/examples/README.md b/examples/README.md index 37c1ae4621288..0082ac723d869 100644 --- a/examples/README.md +++ b/examples/README.md @@ -582,6 +582,7 @@ Example | Description --- | --- [Context Menu](../examples/usages/context_menu.rs) | Example of a context menu [Cooldown](../examples/usage/cooldown.rs) | Example for cooldown on button clicks +[Healthbar](../examples/usage/healthbar.rs) | A scene showcasing world-to-viewport UI ## Window diff --git a/examples/usage/healthbar.rs b/examples/usage/healthbar.rs new file mode 100644 index 0000000000000..043a1acfc9e1c --- /dev/null +++ b/examples/usage/healthbar.rs @@ -0,0 +1,215 @@ +//! A simple UI health bar which follows an object around in 3D space. +//! Using UI nodes is just one way to do this. Alternatively, you can use +//! a mesh facing the camera to set up your health bar. + +use bevy::color::palettes::basic::{BLACK, GREEN}; +use bevy::color::ColorCurve; +use bevy::math::ops::{cos, sin}; +use bevy::prelude::*; +use bevy::transform::plugins::TransformSystems; +use bevy::ui::UiSystems; + +const BAR_HEIGHT: f32 = 25.0; +const BAR_WIDTH: f32 = 160.0; +const HALF_BAR_HEIGHT: f32 = BAR_HEIGHT / 2.0; +const HALF_BAR_WIDTH: f32 = BAR_WIDTH / 2.0; + +#[derive(Component)] +struct Health(f32); + +#[derive(Component)] +struct HealthBar { + /// The target entity that the health bar should follow + target: Entity, + health_text_entity: Entity, + root_ui_entity: Entity, + color_curve: ColorCurve, +} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, (move_cube, update_health).chain()) + .add_systems( + // Bevy's UI Layout happens before transform propagation, + // so we will have to run update_health_bar before both + // and do the transform propagation manually. + PostUpdate, + update_health_bar + .before(TransformSystems::Propagate) + .before(UiSystems::Layout), + ) + .run(); +} + +/// set up a 3D scene where the cube will have a health bar +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // circular base + commands.spawn(( + Mesh3d(meshes.add(Circle::new(4.0))), + MeshMaterial3d(materials.add(Color::WHITE)), + Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), + )); + // light + commands.spawn(( + PointLight { + shadows_enabled: true, + ..default() + }, + Transform::from_xyz(4.0, 8.0, 4.0), + )); + // camera + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(6.5, 2.5, 3.0).looking_at(Vec3::ZERO, Vec3::Y), + )); + // cube with a health component + let cube_id = commands + .spawn(( + Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), + MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))), + Transform::from_xyz(0.0, 0.5, 0.0), + Health(42.0), + )) + .id(); + // Root component for the health bar, this one will be moved to follow the cube + let health_bar_root = commands + .spawn((Node { + flex_direction: FlexDirection::Column, + ..default() + },)) + .id(); + + let health_text = commands + .spawn(( + Node::default(), + Text::new("42"), + TextFont { + font_size: 14.0, + ..default() + }, + TextShadow { + offset: Vec2::splat(2.0), + color: BLACK.into(), + }, + )) + .id(); + + let health_bar_background = commands + .spawn(( + Node { + width: Val::Px(BAR_WIDTH), + height: Val::Px(BAR_HEIGHT), + ..default() + }, + BackgroundColor(BLACK.into()), + )) + .id(); + + // Define the control points for the color curve. + // For more information, please see the cubic curve example. + let colors = [ + LinearRgba::RED, + LinearRgba::RED, + LinearRgba::rgb(1., 1., 0.), // Yellow + LinearRgba::GREEN, + ]; + + let health_bar_node = commands + .spawn(( + Node { + align_items: AlignItems::Stretch, + width: Val::Percent(100.), + height: Val::Px(BAR_HEIGHT), + border: UiRect::all(Val::Px(4.)), + ..default() + }, + HealthBar { + target: cube_id, + health_text_entity: health_text, + root_ui_entity: health_bar_root, + color_curve: ColorCurve::new(colors).unwrap(), + }, + BackgroundColor(Color::from(GREEN)), + )) + .id(); + + commands + .entity(health_bar_root) + .add_children(&[health_text, health_bar_background]); + + commands + .entity(health_bar_background) + .add_child(health_bar_node); +} + +// Some placeholder system to affect the health in this example. +fn update_health(time: Res