docs/bevy/examples/ecs/fallible_params.rs 6.6 K · 165 lines · raw · history

1 //! This example demonstrates how fallible parameters can prevent their systems
2 //! from running if their acquiry conditions aren't met.
3 //!
4 //! Fallible system parameters include:
5 //! - [`Res<R>`], [`ResMut<R>`] - Resource has to exist, and the [`World::default_error_handler`] will be called if it doesn't.
6 //! - [`Single<D, F>`] - There must be exactly one matching entity, but the system will be silently skipped otherwise.
7 //! - [`Option<Single<D, F>>`] - There must be zero or one matching entity. The system will be silently skipped if there are more.
8 //! - [`Populated<D, F>`] - There must be at least one matching entity, but the system will be silently skipped otherwise.
9 //!
10 //! Other system parameters, such as [`Query`], will never fail validation: returning a query with no matching entities is valid.
11 //!
12 //! The result of failed system parameter validation is determined by the [`SystemParamValidationError`] returned
13 //! by [`SystemParam::get_param`] for each system parameter.
14 //! Each system will pass if all of its parameters are valid, or else return [`SystemParamValidationError`] for the first failing parameter.
15 //!
16 //! To learn more about setting the fallback behavior for [`SystemParamValidationError`] failures,
17 //! please see the `error_handling.rs` example.
18 //!
19 //! [`SystemParamValidationError`]: bevy::ecs::system::SystemParamValidationError
20 //! [`SystemParam::get_param`]: bevy::ecs::system::SystemParam::get_param
21
22 use bevy::ecs::error::warn;
23 use bevy::prelude::*;
24 use rand::RngExt;
25
26 fn main() {
27 println!();
28 println!("Press 'A' to add enemy ships and 'R' to remove them.");
29 println!("Player ship will wait for enemy ships and track one if it exists,");
30 println!("but will stop tracking if there are more than one.");
31 println!();
32
33 App::new()
34 // By default, if a parameter fail to be fetched,
35 // `World::get_default_error_handler` will be used to handle the error,
36 // which by default is set to panic.
37 .set_error_handler(warn)
38 .add_plugins(DefaultPlugins)
39 .add_systems(Startup, setup)
40 .add_systems(Update, (user_input, move_targets, track_targets).chain())
41 // This system will always fail validation, because we never create an entity with both `Player` and `Enemy` components.
42 .add_systems(Update, do_nothing_fail_validation)
43 .run();
44 }
45
46 /// Enemy component stores data for movement in a circle.
47 #[derive(Component, Default)]
48 struct Enemy {
49 origin: Vec2,
50 radius: f32,
51 rotation: f32,
52 rotation_speed: f32,
53 }
54
55 /// Player component stores data for going after enemies.
56 #[derive(Component, Default)]
57 struct Player {
58 speed: f32,
59 rotation_speed: f32,
60 min_follow_radius: f32,
61 }
62
63 fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
64 // Spawn 2D camera.
65 commands.spawn(Camera2d);
66
67 // Spawn player.
68 let texture = asset_server.load("textures/simplespace/ship_C.png");
69 commands.spawn((
70 Player {
71 speed: 100.0,
72 rotation_speed: 2.0,
73 min_follow_radius: 50.0,
74 },
75 Sprite {
76 image: texture,
77 color: bevy::color::palettes::tailwind::BLUE_800.into(),
78 ..Default::default()
79 },
80 Transform::from_translation(Vec3::ZERO),
81 ));
82 }
83
84 /// System that reads user input.
85 /// If user presses 'A' we spawn a new random enemy.
86 /// If user presses 'R' we remove a random enemy (if any exist).
87 fn user_input(
88 mut commands: Commands,
89 enemies: Query<Entity, With<Enemy>>,
90 keyboard_input: Res<ButtonInput<KeyCode>>,
91 asset_server: Res<AssetServer>,
92 ) {
93 let mut rng = rand::rng();
94 if keyboard_input.just_pressed(KeyCode::KeyA) {
95 let texture = asset_server.load("textures/simplespace/enemy_A.png");
96 commands.spawn((
97 Enemy {
98 origin: Vec2::new(
99 rng.random_range(-200.0..200.0),
100 rng.random_range(-200.0..200.0),
101 ),
102 radius: rng.random_range(50.0..150.0),
103 rotation: rng.random_range(0.0..std::f32::consts::TAU),
104 rotation_speed: rng.random_range(0.5..1.5),
105 },
106 Sprite {
107 image: texture,
108 color: bevy::color::palettes::tailwind::RED_800.into(),
109 ..default()
110 },
111 Transform::from_translation(Vec3::ZERO),
112 ));
113 }
114
115 if keyboard_input.just_pressed(KeyCode::KeyR)
116 && let Some(entity) = enemies.iter().next()
117 {
118 commands.entity(entity).despawn();
119 }
120 }
121
122 // System that moves the enemies in a circle.
123 // Only runs if there are enemies, due to the `Populated` parameter.
124 fn move_targets(mut enemies: Populated<(&mut Transform, &mut Enemy)>, time: Res<Time>) {
125 for (mut transform, mut target) in &mut *enemies {
126 target.rotation += target.rotation_speed * time.delta_secs();
127 transform.rotation = Quat::from_rotation_z(target.rotation);
128 let offset = transform.right() * target.radius;
129 transform.translation = target.origin.extend(0.0) + offset;
130 }
131 }
132
133 /// System that moves the player, causing them to track a single enemy.
134 /// If there is exactly one, player will track it.
135 /// Otherwise, the player will search for enemies.
136 fn track_targets(
137 // `Single` ensures the system runs ONLY when exactly one matching entity exists.
138 mut player: Single<(&mut Transform, &Player)>,
139 // `Option<Single>` never prevents the system from running, but will be `None` if there is not exactly one matching entity.
140 enemy: Option<Single<&Transform, (With<Enemy>, Without<Player>)>>,
141 time: Res<Time>,
142 ) {
143 let (player_transform, player) = &mut *player;
144 if let Some(enemy_transform) = enemy {
145 // Enemy found, rotate and move towards it.
146 let delta = enemy_transform.translation - player_transform.translation;
147 let distance = delta.length();
148 let front = delta / distance;
149 let up = Vec3::Z;
150 let side = front.cross(up);
151 player_transform.rotation = Quat::from_mat3(&Mat3::from_cols(side, front, up));
152 let max_step = distance - player.min_follow_radius;
153 if 0.0 < max_step {
154 let velocity = (player.speed * time.delta_secs()).min(max_step);
155 player_transform.translation += front * velocity;
156 }
157 } else {
158 // 0 or multiple enemies found, keep searching.
159 player_transform.rotate_axis(Dir3::Z, player.rotation_speed * time.delta_secs());
160 }
161 }
162
163 /// This system always fails param validation, because we never
164 /// create an entity with both [`Player`] and [`Enemy`] components.
165 fn do_nothing_fail_validation(_: Single<(), (With<Player>, With<Enemy>)>) {}