Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,6 @@ jobs:
- name: Install system libs
run: |
sudo apt-get update
sudo apt-get install libasound2-dev libudev-dev pkg-config
sudo apt-get install libasound2-dev libudev-dev libwayland-dev pkg-config
- name: ${{ matrix.name }}
run: ${{ matrix.cmd }}
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,34 @@ fn setup(
Note the generic parameter of `BarSettings`. It is used to associate the configuration with the component it is tracking
and necessary to support multiple bars per entity.

## Per-Entity Color Overrides

Colors can also be set per entity directly in `BarSettings`, which takes precedence over the global `ColorScheme` resource. This is useful when entities share the same tracked component type but need different bar colors — for example, ally and enemy health bars:

```rust
// Ally – static blue bar
commands.spawn((
Health { max: 10., current: 8. },
BarSettings::<Health> {
foreground_color: Some(ForegroundColor::Static(Color::srgb(0.2, 0.6, 1.0))),
background_color: Some(Color::srgb(0.05, 0.05, 0.2)),
..default()
},
));

// Enemy – static red bar, same Health component, no generics needed
commands.spawn((
Health { max: 10., current: 4. },
BarSettings::<Health> {
foreground_color: Some(ForegroundColor::Static(Color::srgb(1.0, 0.15, 0.15))),
background_color: Some(Color::srgb(0.2, 0.05, 0.05)),
..default()
},
));
```

See the `ally_enemy` example for a complete demonstration.

That's it! Updates to the values of your component will be automatically propagated through to the bar.

## Rendering Modes
Expand Down
113 changes: 113 additions & 0 deletions examples/ally_enemy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use bevy::pbr::*;
use bevy::prelude::*;

use bevy_health_bar3d::prelude::{BarSettings, ForegroundColor, HealthBarPlugin, Percentage};

/// A single Health component shared by both allies and enemies.
#[derive(Component, Reflect)]
struct Health {
max: f32,
current: f32,
}

impl Percentage for Health {
fn value(&self) -> f32 {
self.current / self.max
}
}

fn main() {
App::new()
.register_type::<Health>()
.add_plugins((DefaultPlugins, HealthBarPlugin::<Health>::default()))
.add_systems(Startup, setup)
.add_systems(Update, rotate_camera)
.run();
}

fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Ground
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))),
MeshMaterial3d(materials.add(Color::srgba(0.3, 0.5, 0.3, 1.))),
));

let radius = 0.2;
let bar_width = radius * 2.;
let bar_offset = radius * 1.5;

// Ally (blue bar) – per-entity foreground color overrides the global ColorScheme
commands.spawn((
Mesh3d(meshes.add(Sphere { radius })),
MeshMaterial3d(materials.add(Color::srgba(0.2, 0.4, 1., 1.))),
Transform::from_xyz(-1.0, 0.5, 0.0),
Health {
max: 10.,
current: 8.,
},
BarSettings::<Health> {
width: bar_width,
offset: bar_offset,
foreground_color: Some(ForegroundColor::Static(Color::srgb(0.2, 0.6, 1.0))),
background_color: Some(Color::srgb(0.05, 0.05, 0.2)),
..default()
},
));

// Enemy (red bar) – different per-entity colors using the same Health component
commands.spawn((
Mesh3d(meshes.add(Sphere { radius })),
MeshMaterial3d(materials.add(Color::srgba(1., 0.2, 0.2, 1.))),
Transform::from_xyz(1.0, 0.5, 0.0),
Health {
max: 10.,
current: 4.,
},
BarSettings::<Health> {
width: bar_width,
offset: bar_offset,
foreground_color: Some(ForegroundColor::Static(Color::srgb(1.0, 0.15, 0.15))),
background_color: Some(Color::srgb(0.2, 0.05, 0.05)),
..default()
},
));

// Light
commands.spawn((
PointLight {
intensity: 1500.0,
shadows_enabled: true,
..default()
},
Transform::from_xyz(4.0, 8.0, 4.0),
));

// Camera
commands.spawn((
Camera3d::default(),
Msaa::Sample4,
Transform::from_xyz(0., 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}

fn rotate_camera(
mut transform: Single<&mut Transform, With<Camera3d>>,
mut angle: Local<f32>,
time: Res<Time>,
) {
let mut target_angle = *angle + 10. * time.delta_secs();

if target_angle > 360. {
target_angle = 0.;
}

transform.translation.x = 5. * target_angle.to_radians().cos();
transform.translation.z = 5. * target_angle.to_radians().sin();

*angle = target_angle;
transform.look_at(Vec3::ZERO, Vec3::Y);
}
8 changes: 8 additions & 0 deletions src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ pub struct BarSettings<T: Percentage + Component + TypePath> {
pub height: BarHeight,
pub border: BarBorder,
pub orientation: BarOrientation,
/// Optional foreground color override for this entity.
/// If set, takes precedence over the [`ColorScheme`] resource configured for the component type.
pub foreground_color: Option<ForegroundColor>,
/// Optional background color override for this entity.
/// If set, takes precedence over the [`ColorScheme`] resource configured for the component type.
pub background_color: Option<Color>,
#[reflect(ignore)]
pub phantom_data: PhantomData<T>,
}
Expand Down Expand Up @@ -64,6 +70,8 @@ impl<T: Percentage + Component + TypePath> Default for BarSettings<T> {
height: default(),
border: default(),
orientation: default(),
foreground_color: None,
background_color: None,
phantom_data: default(),
}
}
Expand Down
42 changes: 33 additions & 9 deletions src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,30 @@ impl<T: Percentage + Component> WithBar<T> {
}
}

fn resolve_colors<T: Percentage + Component + TypePath>(
settings: &BarSettings<T>,
color_scheme: &ColorScheme<T>,
) -> (Color, Color, Color, Color) {
let foreground = settings
.foreground_color
.as_ref()
.unwrap_or(&color_scheme.foreground_color);
let background = settings
.background_color
.unwrap_or(color_scheme.background_color);

let (high, moderate, low) = match foreground {
ForegroundColor::Static(color) => (*color, *color, *color),
ForegroundColor::TriSpectrum {
high,
moderate,
low,
} => (*high, *moderate, *low),
};

(background, high, moderate, low)
}

#[allow(clippy::type_complexity)]
fn spawn<T: Percentage + Component + TypePath>(
mut commands: Commands,
Expand All @@ -118,18 +142,11 @@ fn spawn<T: Percentage + Component + TypePath>(
)
});

let (high, moderate, low) = match color_scheme.foreground_color {
ForegroundColor::Static(color) => (color, color, color),
ForegroundColor::TriSpectrum {
high,
moderate,
low,
} => (high, moderate, low),
};
let (background, high, moderate, low) = resolve_colors(settings, &color_scheme);

let material = materials.add(Material {
value_and_dimensions: (percentage.value(), width, height, settings.border.width).into(),
background_color: color_scheme.background_color.into(),
background_color: background.into(),
high_color: high.into(),
moderate_color: moderate.into(),
low_color: low.into(),
Expand Down Expand Up @@ -185,6 +202,7 @@ fn update_settings<T: Percentage + Component + TypePath>(
mut materials: ResMut<Assets<Material>>,
mut meshes: ResMut<Assets<Mesh>>,
mut mesh_handles: ResMut<MeshHandles>,
color_scheme: Res<ColorScheme<T>>,
parent_query: Query<(&WithBar<T>, &BarSettings<T>), Changed<BarSettings<T>>>,
bar_query: Query<(Entity, &MaterialComponent, &MeshComponent)>,
) {
Expand Down Expand Up @@ -225,6 +243,12 @@ fn update_settings<T: Percentage + Component + TypePath>(
material.border_color = settings.border.color.into();
material.value_and_dimensions.w = settings.border.width;
material.vertical = settings.orientation == BarOrientation::Vertical;

let (background, high, moderate, low) = resolve_colors(settings, &color_scheme);
material.background_color = background.into();
material.high_color = high.into();
material.moderate_color = moderate.into();
material.low_color = low.into();
});
}

Expand Down
Loading