bevy_steering/behaviors/
path_following.rs

1use bevy::{
2    ecs::{lifecycle::HookContext, query::QueryData, world::DeferredWorld},
3    prelude::*,
4};
5use derivative::Derivative;
6#[cfg(feature = "serialize")]
7use serde::{Deserialize, Serialize};
8
9use crate::control::{BehaviorType, SteeringOutputs};
10
11/// PathFollowing steers an agent along a path using the "carrot on a stick" approach.
12/// The agent always seeks toward a target point that is `lookahead_distance` ahead
13/// on the path from the closest point to the agent.
14#[derive(Component, Debug, Clone, Reflect, Derivative)]
15#[derivative(Default)]
16#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
17#[cfg_attr(feature = "serialize", serde(default))]
18#[component(on_remove = on_path_following_remove)]
19pub struct PathFollowing {
20    /// The points that make up the path (polyline)
21    pub path: Vec<Vec3>,
22    /// How far ahead along the path to place the target "carrot"
23    #[derivative(Default(value = "3.0"))]
24    pub lookahead_distance: f32,
25}
26
27impl PathFollowing {
28    /// Create a new PathFollowing behavior with the given path
29    pub fn new(path: Vec<Vec3>) -> Self {
30        Self {
31            path,
32            lookahead_distance: 3.0,
33        }
34    }
35
36    /// Set the lookahead distance (how far ahead the "carrot" is placed)
37    pub fn with_lookahead_distance(mut self, distance: f32) -> Self {
38        self.lookahead_distance = distance.max(0.1);
39        self
40    }
41
42    /// Set the path points
43    pub fn set_path(&mut self, path: Vec<Vec3>) {
44        self.path = path;
45    }
46
47    /// Get a reference to the path points
48    pub fn path(&self) -> &[Vec3] {
49        &self.path
50    }
51
52    /// Finds the nearest point on the path to the given position.
53    /// Returns the nearest point and the segment index.
54    fn nearest_point_on_path(&self, position: Vec3) -> Option<(Vec3, usize)> {
55        if self.path.is_empty() {
56            return None;
57        }
58
59        if self.path.len() == 1 {
60            return Some((self.path[0], 0));
61        }
62
63        let mut nearest_point = self.path[0];
64        let mut min_dist_sq = position.distance_squared(nearest_point);
65        let mut nearest_segment = 0;
66
67        for i in 0..self.path.len() - 1 {
68            let segment_start = self.path[i];
69            let segment_end = self.path[i + 1];
70
71            let point_on_segment = nearest_point_on_segment(position, segment_start, segment_end);
72            let dist_sq = position.distance_squared(point_on_segment);
73
74            if dist_sq < min_dist_sq {
75                min_dist_sq = dist_sq;
76                nearest_point = point_on_segment;
77                nearest_segment = i;
78            }
79        }
80
81        Some((nearest_point, nearest_segment))
82    }
83
84    /// Finds the "carrot" point that is lookahead_distance ahead on the path
85    /// from the given nearest point.
86    fn carrot_point(&self, nearest_point: Vec3, segment_index: usize) -> Vec3 {
87        if self.path.is_empty() {
88            return nearest_point;
89        }
90
91        if self.path.len() == 1 {
92            return self.path[0];
93        }
94
95        let mut remaining = self.lookahead_distance;
96        let mut current = nearest_point;
97        let mut segment = segment_index;
98
99        while remaining > 0.0 && segment < self.path.len() - 1 {
100            let segment_end = self.path[segment + 1];
101            let to_end = segment_end - current;
102            let len = to_end.length();
103
104            if len <= remaining {
105                remaining -= len;
106                current = segment_end;
107                segment += 1;
108            } else {
109                current += to_end.normalize() * remaining;
110                break;
111            }
112        }
113
114        current
115    }
116}
117
118/// Finds the nearest point on a line segment to a given point.
119fn nearest_point_on_segment(point: Vec3, start: Vec3, end: Vec3) -> Vec3 {
120    let segment = end - start;
121    let len_sq = segment.length_squared();
122
123    if len_sq < f32::EPSILON {
124        return start;
125    }
126
127    let t = (point - start).dot(segment) / len_sq;
128    start + segment * t.clamp(0.0, 1.0)
129}
130
131#[derive(QueryData)]
132#[query_data(mutable)]
133pub struct PathFollowingBehaviorAgentQuery {
134    path_following: &'static PathFollowing,
135    global_transform: &'static GlobalTransform,
136    outputs: &'static mut SteeringOutputs,
137}
138
139/// Path following behavior: find closest point on path, seek toward carrot ahead.
140pub(crate) fn run(mut query: Query<PathFollowingBehaviorAgentQuery>) {
141    for mut item in query.iter_mut() {
142        let agent_pos = item.global_transform.translation();
143
144        if item.path_following.path.is_empty() {
145            item.outputs.clear(BehaviorType::PathFollowing);
146            continue;
147        }
148
149        let Some((nearest, segment)) = item.path_following.nearest_point_on_path(agent_pos) else {
150            item.outputs.clear(BehaviorType::PathFollowing);
151            continue;
152        };
153
154        let carrot = item.path_following.carrot_point(nearest, segment);
155        let to_carrot = carrot - agent_pos;
156
157        if to_carrot.length_squared() < 0.01 {
158            item.outputs.clear(BehaviorType::PathFollowing);
159            continue;
160        }
161
162        let mut target = item
163            .outputs
164            .get(BehaviorType::PathFollowing)
165            .unwrap_or_default();
166        target.set_interest(to_carrot.normalize());
167        item.outputs.set(BehaviorType::PathFollowing, target);
168    }
169}
170
171/// Debug visualization: path line, nearest point, carrot, and steering line.
172pub(crate) fn debug_path_following(
173    mut gizmos: Gizmos,
174    query: Query<(&PathFollowing, &GlobalTransform)>,
175) {
176    for (path_following, transform) in query.iter() {
177        if path_following.path.is_empty() {
178            continue;
179        }
180
181        let agent_pos = transform.translation();
182
183        // Draw path
184        for i in 0..path_following.path.len() - 1 {
185            gizmos.line(
186                path_following.path[i],
187                path_following.path[i + 1],
188                Color::srgb(0.0, 0.8, 0.8),
189            );
190        }
191
192        // Draw waypoints
193        for waypoint in &path_following.path {
194            gizmos.sphere(*waypoint, 0.2, Color::srgb(0.0, 1.0, 1.0));
195        }
196
197        // Draw nearest point and carrot
198        if let Some((nearest, segment)) = path_following.nearest_point_on_path(agent_pos) {
199            gizmos.sphere(nearest, 0.25, Color::srgb(1.0, 0.5, 0.0)); // Orange
200
201            let carrot = path_following.carrot_point(nearest, segment);
202            gizmos.sphere(carrot, 0.3, Color::srgb(0.0, 1.0, 0.0)); // Green
203
204            // Line from agent to carrot
205            gizmos.line(agent_pos, carrot, Color::srgb(1.0, 0.0, 1.0)); // Magenta
206        }
207    }
208}
209
210fn on_path_following_remove(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
211    if let Some(mut outputs) = world.get_mut::<SteeringOutputs>(entity) {
212        outputs.clear(BehaviorType::PathFollowing);
213    }
214}