Bevy AI navigation
When to use
Load this when an actor must chase, patrol, or wait. This skill owns the intent and the desired velocity. Paths stay on bevy-navigation. Transform integration stays on bevy-character-movement. Line-of-sight sensors stay on bevy-spatial-queries.
ECS shape
- Components: a mode flag can be an enum
Component(see bevy-state-machines for the pattern). Goal position can be your own component. No agent, radius, or layer type was opened. - Resources: a shared patrol table can be a
Resourceyou define. No navigation resource was opened. - Systems: read the mode and write a desired velocity or waypoint on
UpdateorFixed. Use.chain()so policy runs before movement. Do not call an agent callback that writesTransform. - Schedule:
Fixed/FixedMainwhen the result feeds the movement step.
Policy map
| Need | Owner |
|---|---|
| Chase vs patrol vs wait | Enum Component plus a system that writes Goal |
| Retarget throttle | Timer resource ticked on FixedUpdate |
| Crowd and avoidance | bevy-navigation |
| Bake and obstacles | bevy-navigation |
| Line of sight | bevy-spatial-queries |
Policy writes DesiredVelocity. It does not move Transform and it does not query a navmesh.
[dependencies]
bevy = "0.20"
#[derive(Clone, Copy, PartialEq, Eq, Component)]
enum Brain {
Wait,
Patrol,
Chase,
}
#[derive(Component)]
struct WaypointIndex(usize);
#[derive(Component)]
struct Goal(Vec3);
#[derive(Component)]
struct DesiredVelocity(Vec3);
#[derive(Component)]
struct Player;
#[derive(Resource)]
struct PatrolRoute {
points: Vec<Vec3>,
}
#[derive(Resource)]
struct RetargetClock(Timer);
fn tick_policy(
time: Res<Time>,
mut clock: ResMut<RetargetClock>,
route: Res<PatrolRoute>,
players: Query<&Transform, With<Player>>,
mut actors: Query<(&Brain, &Transform, &mut WaypointIndex, &mut Goal)>,
) {
clock.0.tick(time.delta());
if !clock.0.just_finished() {
return;
}
let player_pos = players.iter().next().map(|transform| transform.translation);
for (brain, transform, mut cursor, mut goal) in &mut actors {
match *brain {
Brain::Wait => goal.0 = transform.translation,
Brain::Patrol => {
if route.points.is_empty() {
continue;
}
cursor.0 = (cursor.0 + 1) % route.points.len();
goal.0 = route.points[cursor.0];
}
Brain::Chase => {
if let Some(pos) = player_pos {
goal.0 = pos;
}
}
}
}
}
fn steer(mut actors: Query<(&Transform, &Goal, &mut DesiredVelocity), With<Brain>>) {
for (transform, goal, mut velocity) in &mut actors {
let offset = goal.0 - transform.translation;
let flat = Vec3::new(offset.x, 0.0, offset.z);
velocity.0 = if flat.length_squared() < 0.05 {
Vec3::ZERO
} else {
flat.normalize() * 3.0
};
}
}
pub struct NavPolicyPlugin;
impl Plugin for NavPolicyPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(PatrolRoute {
points: vec![Vec3::new(-6.0, 0.0, 0.0), Vec3::new(6.0, 0.0, 4.0)],
})
.insert_resource(RetargetClock(Timer::from_seconds(
0.4,
TimerMode::Repeating,
)))
.add_systems(FixedUpdate, (tick_policy, steer).chain());
}
}
#[derive(Component)]
struct PatrolAgent;
fn spawn_patrol(mut commands: Commands) {
commands.spawn((
PatrolAgent,
Transform::from_xyz(0.0, 0.0, 0.0),
DesiredDir(Vec3::X),
));
}
#[derive(Component)]
struct DesiredDir(Vec3);
fn hold_when_close(mut agents: Query<(&Transform, &mut DesiredDir), With<PatrolAgent>>) {
for (transform, mut desired) in &mut agents {
if transform.translation.distance(Vec3::ZERO) < 0.5 {
desired.0 = Vec3::ZERO;
}
}
}
Expert Knowledge Delta
Target: Bevy 0.20+.
Core ECS types: Component, Resource, Query, Commands, Startup, Update, Fixed / FixedMain (bevy::time), .chain(), Plugin, DefaultPlugins, MinimalPlugins. Messages and observers are an ownership slot. See bevy-events-messaging for their APIs. Systems run in parallel unless .chain() orders the ones that conflict.
Who owns state
| Piece | Owner | Who may mutate | Schedule |
|---|---|---|---|
| Data on one entity | Component |
One system via Query |
Update or FixedMain |
| One value for the whole app | Resource |
One system | Startup to insert, then the schedule that reads it |
| Spawn | Commands |
The spawning system | Startup for the initial world; later schedules for runtime spawn |
| Order that must not race | .chain() |
The chained systems only | Same schedule |
| Plugin boundary | Plugin |
build registers systems and resources |
Before the app runs |
Borrow rule: two systems that both mutably query the same component must be ordered (.chain()) or split so only one writes. Unrelated systems stay parallel. That is the ECS page rule, not a guess at a system-set API.
Decision
| If you need | Do this | Do not |
|---|---|---|
| Per-entity data | Component + Query |
One script that owns the whole actor |
| App-wide config or clock | Resource |
Copy the value onto every entity |
| Stable simulation step | FixedMain and the Fixed clock |
Tie simulation to raw frame Update |
| Startup spawn | Commands in Startup |
Spawn from a render system |
| Cross-system notice that is not shared state | Message or observer slot in bevy-events-messaging |
A new event type invented in this file |
| An API outside the documented set | Check the official docs first | A made-up crate or API |
Load references/ecs-ownership.md for the ownership table.
NEVER
-
Do not pin
/0.20.0/in docs URLs. WHY: links stay onlatest; the 0.20 target is stated once in prose. -
Do not mutably query the same component from two unordered systems. WHY: the schedule runs systems in parallel unless
.chain()orders them. -
Do not invent a physics, navigation, networking, or XR crate here. WHY: this skill only names documented Bevy crates and modules.
-
No navigation-agent or nav-server types from another engine. WHY: this skill writes
GoalandDesiredVelocity; a foreign agent type would moveTransformand skip the chain into movement. -
No named third-party navigation or physics crate. WHY: Bevy code stays idiomatic ECS and uses only documented Bevy APIs.
-
No invented path, bake, or avoidance APIs. WHY: Bevy code stays idiomatic ECS and uses only documented Bevy APIs.
Reference
The library pin on bevy-master stays 0.20.0. No AI navigation module was opened. Load references/ecs-ownership.md when splitting goal writes from velocity writes. Do not load it to bake a mesh.
Official Documentation
- Assets - Official asset catalog. No AI plugin was selected.
- Examples - Example tree on the
latestbranch. - ECS -
Component,Resourcepairing via bevy-data-resources, systems, and.chain(). - App states are not per-entity patrol modes. Per-entity flags stay components (bevy-state-machines).
Related Skills
Prerequisites
- bevy-master - ECS ownership.
- bevy-navigation - Paths and navigation meshes.
- bevy-state-machines - Enum components for a mode flag.
Complements
- bevy-character-movement - Integrates a waypoint or velocity.
- bevy-spatial-queries - Sensors and spatial queries.
Downstream / consumers
- None yet.
Master
- bevy-master - Bevy router.
