# Objective Yet another PR for migrating stuff to required components. This time, cameras! ## Solution As per the [selected proposal](https://hackmd.io/tsYID4CGRiWxzsgawzxG_g#Combined-Proposal-1-Selected), deprecate `Camera2dBundle` and `Camera3dBundle` in favor of `Camera2d` and `Camera3d`. Adding a `Camera` without `Camera2d` or `Camera3d` now logs a warning, as suggested by Cart [on Discord](https://discord.com/channels/691052431525675048/1264881140007702558/1291506402832945273). I would personally like cameras to work a bit differently and be split into a few more components, to avoid some footguns and confusing semantics, but that is more controversial, and shouldn't block this core migration. ## Testing I ran a few 2D and 3D examples, and tried cameras with and without render graphs. --- ## Migration Guide `Camera2dBundle` and `Camera3dBundle` have been deprecated in favor of `Camera2d` and `Camera3d`. Inserting them will now also insert the other components required by them automatically.
		
			
				
	
	
		
			222 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			222 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
//! Demonstrates how to observe life-cycle triggers as well as define custom ones.
 | 
						|
 | 
						|
use bevy::{
 | 
						|
    math::Isometry2d,
 | 
						|
    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.
 | 
						|
        .observe(
 | 
						|
            |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.
 | 
						|
        .observe(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.
 | 
						|
        .observe(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(
 | 
						|
        TextBundle::from_section(
 | 
						|
            "Click on a \"Mine\" to trigger it.\n\
 | 
						|
            When it explodes it will trigger all overlapping mines.",
 | 
						|
            TextStyle {
 | 
						|
                color: Color::WHITE,
 | 
						|
                ..default()
 | 
						|
            },
 | 
						|
        )
 | 
						|
        .with_style(Style {
 | 
						|
            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(
 | 
						|
            Isometry2d::from_translation(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: Query<(&Camera, &GlobalTransform)>,
 | 
						|
    windows: Query<&Window>,
 | 
						|
    mut commands: Commands,
 | 
						|
) {
 | 
						|
    let (camera, camera_transform) = camera.single();
 | 
						|
    if let Some(pos) = windows
 | 
						|
        .single()
 | 
						|
        .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
 | 
						|
    }
 | 
						|
}
 |