# Objective Continue improving the user experience of our UI Node API in the direction specified by [Bevy's Next Generation Scene / UI System](https://github.com/bevyengine/bevy/discussions/14437) ## Solution As specified in the document above, merge `Style` fields into `Node`, and move "computed Node fields" into `ComputedNode` (I chose this name over something like `ComputedNodeLayout` because it currently contains more than just layout info. If we want to break this up / rename these concepts, lets do that in a separate PR). `Style` has been removed. This accomplishes a number of goals: ## Ergonomics wins Specifying both `Node` and `Style` is now no longer required for non-default styles Before: ```rust commands.spawn(( Node::default(), Style { width: Val::Px(100.), ..default() }, )); ``` After: ```rust commands.spawn(Node { width: Val::Px(100.), ..default() }); ``` ## Conceptual clarity `Style` was never a comprehensive "style sheet". It only defined "core" style properties that all `Nodes` shared. Any "styled property" that couldn't fit that mold had to be in a separate component. A "real" style system would style properties _across_ components (`Node`, `Button`, etc). We have plans to build a true style system (see the doc linked above). By moving the `Style` fields to `Node`, we fully embrace `Node` as the driving concept and remove the "style system" confusion. ## Next Steps * Consider identifying and splitting out "style properties that aren't core to Node". This should not happen for Bevy 0.15. --- ## Migration Guide Move any fields set on `Style` into `Node` and replace all `Style` component usage with `Node`. Before: ```rust commands.spawn(( Node::default(), Style { width: Val::Px(100.), ..default() }, )); ``` After: ```rust commands.spawn(Node { width: Val::Px(100.), ..default() }); ``` For any usage of the "computed node properties" that used to live on `Node`, use `ComputedNode` instead: Before: ```rust fn system(nodes: Query<&Node>) { for node in &nodes { let computed_size = node.size(); } } ``` After: ```rust fn system(computed_nodes: Query<&ComputedNode>) { for computed_node in &computed_nodes { let computed_size = computed_node.size(); } } ```
		
			
				
	
	
		
			216 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			216 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
//! Demonstrates how to observe life-cycle triggers as well as define custom ones.
 | 
						|
 | 
						|
use bevy::{
 | 
						|
    prelude::*,
 | 
						|
    utils::{HashMap, HashSet},
 | 
						|
};
 | 
						|
use rand::{Rng, SeedableRng};
 | 
						|
use rand_chacha::ChaCha8Rng;
 | 
						|
 | 
						|
fn main() {
 | 
						|
    App::new()
 | 
						|
        .add_plugins(DefaultPlugins)
 | 
						|
        .init_resource::<SpatialIndex>()
 | 
						|
        .add_systems(Startup, setup)
 | 
						|
        .add_systems(Update, (draw_shapes, handle_click))
 | 
						|
        // Observers are systems that run when an event is "triggered". This observer runs whenever
 | 
						|
        // `ExplodeMines` is triggered.
 | 
						|
        .add_observer(
 | 
						|
            |trigger: Trigger<ExplodeMines>,
 | 
						|
             mines: Query<&Mine>,
 | 
						|
             index: Res<SpatialIndex>,
 | 
						|
             mut commands: Commands| {
 | 
						|
                // You can access the trigger data via the `Observer`
 | 
						|
                let event = trigger.event();
 | 
						|
                // Access resources
 | 
						|
                for e in index.get_nearby(event.pos) {
 | 
						|
                    // Run queries
 | 
						|
                    let mine = mines.get(e).unwrap();
 | 
						|
                    if mine.pos.distance(event.pos) < mine.size + event.radius {
 | 
						|
                        // And queue commands, including triggering additional events
 | 
						|
                        // Here we trigger the `Explode` event for entity `e`
 | 
						|
                        commands.trigger_targets(Explode, e);
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            },
 | 
						|
        )
 | 
						|
        // This observer runs whenever the `Mine` component is added to an entity, and places it in a simple spatial index.
 | 
						|
        .add_observer(on_add_mine)
 | 
						|
        // This observer runs whenever the `Mine` component is removed from an entity (including despawning it)
 | 
						|
        // and removes it from the spatial index.
 | 
						|
        .add_observer(on_remove_mine)
 | 
						|
        .run();
 | 
						|
}
 | 
						|
 | 
						|
#[derive(Component)]
 | 
						|
struct Mine {
 | 
						|
    pos: Vec2,
 | 
						|
    size: f32,
 | 
						|
}
 | 
						|
 | 
						|
impl Mine {
 | 
						|
    fn random(rand: &mut ChaCha8Rng) -> Self {
 | 
						|
        Mine {
 | 
						|
            pos: Vec2::new(
 | 
						|
                (rand.gen::<f32>() - 0.5) * 1200.0,
 | 
						|
                (rand.gen::<f32>() - 0.5) * 600.0,
 | 
						|
            ),
 | 
						|
            size: 4.0 + rand.gen::<f32>() * 16.0,
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
#[derive(Event)]
 | 
						|
struct ExplodeMines {
 | 
						|
    pos: Vec2,
 | 
						|
    radius: f32,
 | 
						|
}
 | 
						|
 | 
						|
#[derive(Event)]
 | 
						|
struct Explode;
 | 
						|
 | 
						|
fn setup(mut commands: Commands) {
 | 
						|
    commands.spawn(Camera2d);
 | 
						|
    commands.spawn((
 | 
						|
        Text::new(
 | 
						|
            "Click on a \"Mine\" to trigger it.\n\
 | 
						|
            When it explodes it will trigger all overlapping mines.",
 | 
						|
        ),
 | 
						|
        Node {
 | 
						|
            position_type: PositionType::Absolute,
 | 
						|
            top: Val::Px(12.),
 | 
						|
            left: Val::Px(12.),
 | 
						|
            ..default()
 | 
						|
        },
 | 
						|
    ));
 | 
						|
 | 
						|
    let mut rng = ChaCha8Rng::seed_from_u64(19878367467713);
 | 
						|
 | 
						|
    commands
 | 
						|
        .spawn(Mine::random(&mut rng))
 | 
						|
        // Observers can watch for events targeting a specific entity.
 | 
						|
        // This will create a new observer that runs whenever the Explode event
 | 
						|
        // is triggered for this spawned entity.
 | 
						|
        .observe(explode_mine);
 | 
						|
 | 
						|
    // We want to spawn a bunch of mines. We could just call the code above for each of them.
 | 
						|
    // That would create a new observer instance for every Mine entity. Having duplicate observers
 | 
						|
    // generally isn't worth worrying about as the overhead is low. But if you want to be maximally efficient,
 | 
						|
    // you can reuse observers across entities.
 | 
						|
    //
 | 
						|
    // First, observers are actually just entities with the Observer component! The `observe()` functions
 | 
						|
    // you've seen so far in this example are just shorthand for manually spawning an observer.
 | 
						|
    let mut observer = Observer::new(explode_mine);
 | 
						|
 | 
						|
    // As we spawn entities, we can make this observer watch each of them:
 | 
						|
    for _ in 0..1000 {
 | 
						|
        let entity = commands.spawn(Mine::random(&mut rng)).id();
 | 
						|
        observer.watch_entity(entity);
 | 
						|
    }
 | 
						|
 | 
						|
    // By spawning the Observer component, it becomes active!
 | 
						|
    commands.spawn(observer);
 | 
						|
}
 | 
						|
 | 
						|
fn on_add_mine(
 | 
						|
    trigger: Trigger<OnAdd, Mine>,
 | 
						|
    query: Query<&Mine>,
 | 
						|
    mut index: ResMut<SpatialIndex>,
 | 
						|
) {
 | 
						|
    let mine = query.get(trigger.entity()).unwrap();
 | 
						|
    let tile = (
 | 
						|
        (mine.pos.x / CELL_SIZE).floor() as i32,
 | 
						|
        (mine.pos.y / CELL_SIZE).floor() as i32,
 | 
						|
    );
 | 
						|
    index.map.entry(tile).or_default().insert(trigger.entity());
 | 
						|
}
 | 
						|
 | 
						|
// Remove despawned mines from our index
 | 
						|
fn on_remove_mine(
 | 
						|
    trigger: Trigger<OnRemove, Mine>,
 | 
						|
    query: Query<&Mine>,
 | 
						|
    mut index: ResMut<SpatialIndex>,
 | 
						|
) {
 | 
						|
    let mine = query.get(trigger.entity()).unwrap();
 | 
						|
    let tile = (
 | 
						|
        (mine.pos.x / CELL_SIZE).floor() as i32,
 | 
						|
        (mine.pos.y / CELL_SIZE).floor() as i32,
 | 
						|
    );
 | 
						|
    index.map.entry(tile).and_modify(|set| {
 | 
						|
        set.remove(&trigger.entity());
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
fn explode_mine(trigger: Trigger<Explode>, query: Query<&Mine>, mut commands: Commands) {
 | 
						|
    // If a triggered event is targeting a specific entity you can access it with `.entity()`
 | 
						|
    let id = trigger.entity();
 | 
						|
    let Some(mut entity) = commands.get_entity(id) else {
 | 
						|
        return;
 | 
						|
    };
 | 
						|
    info!("Boom! {:?} exploded.", id.index());
 | 
						|
    entity.despawn();
 | 
						|
    let mine = query.get(id).unwrap();
 | 
						|
    // Trigger another explosion cascade.
 | 
						|
    commands.trigger(ExplodeMines {
 | 
						|
        pos: mine.pos,
 | 
						|
        radius: mine.size,
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
// Draw a circle for each mine using `Gizmos`
 | 
						|
fn draw_shapes(mut gizmos: Gizmos, mines: Query<&Mine>) {
 | 
						|
    for mine in &mines {
 | 
						|
        gizmos.circle_2d(
 | 
						|
            mine.pos,
 | 
						|
            mine.size,
 | 
						|
            Color::hsl((mine.size - 4.0) / 16.0 * 360.0, 1.0, 0.8),
 | 
						|
        );
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// Trigger `ExplodeMines` at the position of a given click
 | 
						|
fn handle_click(
 | 
						|
    mouse_button_input: Res<ButtonInput<MouseButton>>,
 | 
						|
    camera: Single<(&Camera, &GlobalTransform)>,
 | 
						|
    windows: Single<&Window>,
 | 
						|
    mut commands: Commands,
 | 
						|
) {
 | 
						|
    let (camera, camera_transform) = *camera;
 | 
						|
    if let Some(pos) = windows
 | 
						|
        .cursor_position()
 | 
						|
        .and_then(|cursor| camera.viewport_to_world(camera_transform, cursor).ok())
 | 
						|
        .map(|ray| ray.origin.truncate())
 | 
						|
    {
 | 
						|
        if mouse_button_input.just_pressed(MouseButton::Left) {
 | 
						|
            commands.trigger(ExplodeMines { pos, radius: 1.0 });
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
#[derive(Resource, Default)]
 | 
						|
struct SpatialIndex {
 | 
						|
    map: HashMap<(i32, i32), HashSet<Entity>>,
 | 
						|
}
 | 
						|
 | 
						|
/// Cell size has to be bigger than any `TriggerMine::radius`
 | 
						|
const CELL_SIZE: f32 = 64.0;
 | 
						|
 | 
						|
impl SpatialIndex {
 | 
						|
    // Lookup all entities within adjacent cells of our spatial index
 | 
						|
    fn get_nearby(&self, pos: Vec2) -> Vec<Entity> {
 | 
						|
        let tile = (
 | 
						|
            (pos.x / CELL_SIZE).floor() as i32,
 | 
						|
            (pos.y / CELL_SIZE).floor() as i32,
 | 
						|
        );
 | 
						|
        let mut nearby = Vec::new();
 | 
						|
        for x in -1..2 {
 | 
						|
            for y in -1..2 {
 | 
						|
                if let Some(mines) = self.map.get(&(tile.0 + x, tile.1 + y)) {
 | 
						|
                    nearby.extend(mines.iter());
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        nearby
 | 
						|
    }
 | 
						|
}
 |