bevy_steering/behaviors/
path_following.rs1use 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#[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 pub path: Vec<Vec3>,
22 #[derivative(Default(value = "3.0"))]
24 pub lookahead_distance: f32,
25}
26
27impl PathFollowing {
28 pub fn new(path: Vec<Vec3>) -> Self {
30 Self {
31 path,
32 lookahead_distance: 3.0,
33 }
34 }
35
36 pub fn with_lookahead_distance(mut self, distance: f32) -> Self {
38 self.lookahead_distance = distance.max(0.1);
39 self
40 }
41
42 pub fn set_path(&mut self, path: Vec<Vec3>) {
44 self.path = path;
45 }
46
47 pub fn path(&self) -> &[Vec3] {
49 &self.path
50 }
51
52 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 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
118fn 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
139pub(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
171pub(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 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 for waypoint in &path_following.path {
194 gizmos.sphere(*waypoint, 0.2, Color::srgb(0.0, 1.0, 1.0));
195 }
196
197 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)); let carrot = path_following.carrot_point(nearest, segment);
202 gizmos.sphere(carrot, 0.3, Color::srgb(0.0, 1.0, 0.0)); gizmos.line(agent_pos, carrot, Color::srgb(1.0, 0.0, 1.0)); }
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}