bevy_steering/behaviors/
approach.rs

1use avian3d::prelude::*;
2use bevy::{
3    ecs::{lifecycle::HookContext, query::QueryData, world::DeferredWorld},
4    prelude::*,
5};
6#[cfg(feature = "serialize")]
7use serde::{Deserialize, Serialize};
8
9use crate::{
10    agent::SteeringAgent,
11    control::{BehaviorType, SteeringOutputs},
12    speed::SpeedOverride,
13};
14
15/// Approach behavior attempts to move the agent towards a target position.
16/// Similar to Seek, except the agent slows down as it gets closer to the target.
17#[derive(Component, Debug, Copy, Clone, Reflect)]
18#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
19#[cfg_attr(feature = "serialize", serde(default))]
20#[component(on_remove = on_approach_remove)]
21pub struct Approach {
22    /// The radius within which the agent will approach the target.
23    pub target_radius: f32,
24    /// The target position to approach.
25    pub target: Vec3,
26    /// Distance at which the agent begins slowing down when approaching at max speed.
27    /// At this distance, speed = 1.0; closer distances result in proportionally lower speeds.
28    pub slowdown_distance: f32,
29}
30
31impl Default for Approach {
32    fn default() -> Self {
33        Self {
34            target_radius: 0.0,
35            target: Vec3::ZERO,
36            slowdown_distance: 10.0,
37        }
38    }
39}
40
41impl Approach {
42    pub fn new(target: Vec3, target_radius: f32) -> Self {
43        Self {
44            target,
45            target_radius,
46            ..Default::default()
47        }
48    }
49
50    ///  Set the slowdown distance for the approach behavior. Depending on
51    ///  the agent's max speed and force, you may need to increase or
52    ///  decrease this to avoid overshooting the target.
53    pub fn with_slowdown_distance(mut self, distance: f32) -> Self {
54        self.slowdown_distance = distance;
55        self
56    }
57
58    pub fn set_target(&mut self, target: Vec3) {
59        self.target = target;
60    }
61}
62
63#[derive(QueryData)]
64#[query_data(mutable)]
65pub struct ApproachBehaviorAgentQuery {
66    agent: &'static SteeringAgent,
67    approach: &'static Approach,
68    global_transform: &'static GlobalTransform,
69    forces: Forces,
70    outputs: &'static mut SteeringOutputs,
71    speed_override: &'static mut SpeedOverride,
72}
73
74/// Approach behavior moves the agent towards the target position. At
75/// far distances it behaves the same as [Seek], but slows down as
76/// it approaches the target using kinematic arrival.
77pub(crate) fn run(mut query: Query<ApproachBehaviorAgentQuery>) {
78    for mut item in query.iter_mut() {
79        let to_target = item.approach.target - item.global_transform.translation();
80        let distance = to_target.length();
81
82        // If we've arrived, express no interest in movement
83        if distance < item.approach.target_radius {
84            item.outputs.clear(BehaviorType::Approach);
85            continue;
86        }
87
88        // Constant deceleration as the agent approaches the target.
89        let speed = (distance / item.approach.slowdown_distance)
90            .sqrt()
91            .clamp(0.0, 1.0);
92
93        let mut steering_target = item.outputs.get(BehaviorType::Approach).unwrap_or_default();
94        steering_target.set_interest(to_target.normalize());
95        item.outputs.set(BehaviorType::Approach, steering_target);
96        item.speed_override.set(speed);
97    }
98}
99
100/// Debug visualization for approach behavior. Draws a yellow arrow to the
101/// target and a yellow circle around the target radius.
102pub(crate) fn debug_approach(mut gizmos: Gizmos, query: Query<(&GlobalTransform, &Approach)>) {
103    for (transform, approach) in query.iter() {
104        let position = transform.translation();
105        let yellow = Color::srgb(1.0, 1.0, 0.0);
106
107        // Draw arrow to target
108        gizmos.arrow(position, approach.target, yellow);
109
110        // Draw circle around target radius
111        if approach.target_radius > 0.0 {
112            gizmos.circle(
113                Isometry3d::from_translation(approach.target),
114                approach.target_radius,
115                yellow,
116            );
117        }
118    }
119}
120
121fn on_approach_remove(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
122    if let Some(mut outputs) = world.get_mut::<SteeringOutputs>(entity) {
123        outputs.clear(BehaviorType::Approach);
124    }
125}