Skip to content

Commit d6641a2

Browse files
authored
docs: Implemented a simple steps-based example (#39)
Co-authored-by: Werner Kroneman <[email protected]>
1 parent 9d770e4 commit d6641a2

File tree

2 files changed

+312
-0
lines changed

2 files changed

+312
-0
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ App::new()
113113
.run();
114114
```
115115

116+
### Examples
117+
118+
The full source code of the above Thirst/Drink action example can be found in the [Thirst example]([)examples/thirst.rs).
119+
120+
Also, the [Sequence Example](examples/sequence.rs) example describes how to use `Steps` to compose several actions
121+
together sequentially.
122+
116123
### Contributing
117124

118125
1. Install the latest Rust toolchain (stable supported).

examples/sequence.rs

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
//! This example describes how to create an action that takes multiple steps.
2+
//!
3+
//! It is similar to the thirst example, but instead of just magically quenching a thirst,
4+
//! the actor must be near a water source in order to drink.
5+
//!
6+
//! Note that it does not matter if the actor is already near a water source:
7+
//! the MoveToWaterSource action will simply terminate immediately.
8+
9+
use bevy::prelude::*;
10+
use big_brain::actions::StepsBuilder;
11+
use big_brain::prelude::*;
12+
13+
/// First, we make a simple Position component.
14+
#[derive(Component, Debug, Copy, Clone)]
15+
pub struct Position {
16+
pub position: Vec2,
17+
}
18+
19+
/// A marker component for an entity that describes a water source.
20+
#[derive(Component, Debug)]
21+
pub struct WaterSource;
22+
23+
/// We steal the Thirst component from the thirst example.
24+
#[derive(Component, Debug)]
25+
pub struct Thirst {
26+
/// How much thirstier the entity gets over time.
27+
pub per_second: f32,
28+
/// How much thirst the entity currently has.
29+
pub thirst: f32,
30+
}
31+
32+
impl Thirst {
33+
pub fn new(thirst: f32, per_second: f32) -> Self {
34+
Self { thirst, per_second }
35+
}
36+
}
37+
38+
/// A simple system that just pushes the thirst value up over time.
39+
/// Just a plain old Bevy system, big-brain is not involved yet.
40+
pub fn thirst_system(time: Res<Time>, mut thirsts: Query<&mut Thirst>) {
41+
42+
for mut thirst in thirsts.iter_mut() {
43+
44+
thirst.thirst += thirst.per_second * time.delta_seconds();
45+
46+
// Thirst is capped at 100.0
47+
if thirst.thirst >= 100.0 {
48+
thirst.thirst = 100.0;
49+
}
50+
51+
println!("Thirst: {}", thirst.thirst);
52+
}
53+
}
54+
55+
/// An action where the actor moves to the closest water source
56+
#[derive(Clone, Component, Debug)]
57+
pub struct MoveToWaterSource {
58+
// The movement speed of the actor.
59+
speed: f32,
60+
}
61+
62+
/// Closest distance to a water source to be able to drink from it.
63+
const MAX_DISTANCE: f32 = 0.1;
64+
65+
fn move_to_water_source_action_system(
66+
time: Res<Time>,
67+
// Find all water sources
68+
mut waters: Query<&Position, With<WaterSource>>,
69+
// We use Without to make disjoint queries.
70+
mut positions: Query<&mut Position, Without<WaterSource>>,
71+
// A query on all current MoveToWaterSource actions.
72+
mut action_query: Query<(&Actor, &mut ActionState, &MoveToWaterSource)>,
73+
) {
74+
// Loop through all actions, just like you'd loop over all entities in any other query.
75+
for (actor, mut action_state, move_to) in action_query.iter_mut() {
76+
77+
// Different behavior depending on action state.
78+
match *action_state {
79+
// Action was just requested; it hasn't been seen before.
80+
ActionState::Requested => {
81+
println!("Let's go find some water!");
82+
// We don't really need any initialization code here, since the queries are cheap enough.
83+
*action_state = ActionState::Executing;
84+
},
85+
ActionState::Executing => {
86+
87+
// Look up the actor's position.
88+
let mut actor_position =
89+
positions
90+
.get_mut(actor.0)
91+
.expect("actor has no position");
92+
93+
println!("Actor position: {:?}", actor_position.position);
94+
95+
// Look up the water source closest to them.
96+
let closest_water_source =
97+
find_closest_water_source(&waters, &actor_position);
98+
99+
// Find how far we are from it.
100+
let delta = closest_water_source.position - actor_position.position;
101+
102+
let distance = delta.length();
103+
104+
println!("Distance: {}", distance);
105+
106+
if distance > MAX_DISTANCE {
107+
// We're still too far, take a step toward it!
108+
109+
println!("Stepping closer.");
110+
111+
// How far can we travel during this frame?
112+
let step_size = time.delta_seconds() * move_to.speed;
113+
114+
let step = if step_size > distance {
115+
// We'll move by the full step
116+
delta / distance * step_size
117+
} else {
118+
// Move only the remaining distance, or we might overshoot.
119+
delta
120+
};
121+
122+
// Move the actor.
123+
actor_position.position += step;
124+
125+
} else {
126+
// We're within the required distance! We can declare success.
127+
128+
println!("We got there!");
129+
130+
// The action will be cleaned up automatically.
131+
*action_state = ActionState::Success;
132+
}
133+
}
134+
ActionState::Cancelled => {
135+
// Always treat cancellations, or we might keep doing this forever!
136+
// You don't need to terminate immediately, by the way, this is only a flag that
137+
// the cancellation has been requested. If the actor is balancing on a tightrope,
138+
// for instance, you may let them walk off before ending the action.
139+
*action_state = ActionState::Failure;
140+
}
141+
_ => {}
142+
}
143+
}
144+
145+
}
146+
147+
/// A utility function that finds the closest water source to the actor.
148+
fn find_closest_water_source(waters: &Query<&Position, With<WaterSource>>, actor_position: &Position) -> Position {
149+
waters
150+
.iter()
151+
.min_by(|a, b| {
152+
let da = (a.position - actor_position.position).length_squared();
153+
let db = (b.position - actor_position.position).length_squared();
154+
da.partial_cmp(&db).unwrap()
155+
})
156+
.expect("no water sources")
157+
.clone()
158+
}
159+
160+
/// A simple action: the actor's thirst shall decrease, but only if they are near a water source.
161+
#[derive(Clone, Component, Debug)]
162+
pub struct Drink {
163+
per_second: f32,
164+
}
165+
166+
fn drink_action_system(
167+
time: Res<Time>,
168+
mut thirsts: Query<(&Position, &mut Thirst), Without<WaterSource>>,
169+
mut waters: Query<&Position, With<WaterSource>>,
170+
mut query: Query<(&Actor, &mut ActionState, &Drink)>,
171+
) {
172+
// Loop through all actions, just like you'd loop over all entities in any other query.
173+
for (Actor(actor), mut state, drink) in query.iter_mut() {
174+
175+
// Look up the actor's position and thirst from the Actor component in the action entity.
176+
let (actor_position, mut thirst) = thirsts.get_mut(*actor).expect("actor has no thirst");
177+
178+
match *state {
179+
ActionState::Requested => {
180+
// We'll start drinking as soon as we're requested to do so.
181+
*state = ActionState::Executing;
182+
}
183+
ActionState::Executing => {
184+
185+
// Look up the closest water source.
186+
// Note that there is no explicit passing of a selected water source from the GoToWaterSource action,
187+
// so we look it up again. Note that this decouples the actions from each other,
188+
// so if the actor is already close to a water source, the GoToWaterSource action
189+
// will not be necessary (though it will not harm either).
190+
//
191+
// Essentially, being close to a water source would be a precondition for the Drink action.
192+
// How this precondition was fulfilled is not this code's concern.
193+
let closest_water_source =
194+
find_closest_water_source(&waters, &*actor_position);
195+
196+
// Find how far we are from it.
197+
let distance = (closest_water_source.position - actor_position.position).length();
198+
199+
// Are we close enough?
200+
if distance < MAX_DISTANCE {
201+
println!("Drinking!");
202+
203+
// Start reducing the thirst. Alternatively, you could send out some kind of
204+
// DrinkFromSource event that indirectly decreases thirst.
205+
thirst.thirst -= drink.per_second * time.delta_seconds();
206+
207+
// Once we hit 0 thirst, we stop drinking and report success.
208+
if thirst.thirst <= 0.0 {
209+
thirst.thirst = 0.0;
210+
*state = ActionState::Success;
211+
}
212+
} else {
213+
// The actor was told to drink, but they can't drink when they're so far away!
214+
// The action doesn't know how to deal with this case, it's the overarching system's
215+
// to fulfill the precondition.
216+
println!("We're too far away!");
217+
*state = ActionState::Failure;
218+
}
219+
}
220+
// All Actions should make sure to handle cancellations!
221+
// Drinking is not a complicated action, so we can just interrupt it immediately.
222+
ActionState::Cancelled => {
223+
*state = ActionState::Failure;
224+
}
225+
_ => {}
226+
}
227+
}
228+
}
229+
230+
// Scorers are the same as in the thirst example.
231+
#[derive(Clone, Component, Debug)]
232+
pub struct Thirsty;
233+
234+
pub fn thirsty_scorer_system(
235+
thirsts: Query<&Thirst>,
236+
mut query: Query<(&Actor, &mut Score), With<Thirsty>>,
237+
) {
238+
for (Actor(actor), mut score) in query.iter_mut() {
239+
if let Ok(thirst) = thirsts.get(*actor) {
240+
score.set(thirst.thirst / 100.);
241+
}
242+
}
243+
}
244+
245+
pub fn init_entities(mut cmd: Commands) {
246+
247+
// Spawn two water sources.
248+
cmd.spawn()
249+
.insert(WaterSource)
250+
.insert(Position {
251+
position: Vec2::new(10.0, 10.0),
252+
});
253+
254+
cmd.spawn()
255+
.insert(WaterSource)
256+
.insert(Position {
257+
position: Vec2::new(-10.0, 0.0),
258+
});
259+
260+
// We use the Steps struct to essentially build a "MoveAndDrink" action by composing
261+
// the MoveToWaterSource and Drink actions.
262+
//
263+
// If either of the steps fails, the whole action fails. That is: if the actor somehow fails
264+
// to move to the water source (which is not possible in our case) they will not attempt to
265+
// drink either. Getting them un-stuck from that situation is then up to other possible actions.
266+
//
267+
// We build up a list of steps that make it so that the actor will...
268+
let move_and_drink =
269+
Steps::build()
270+
// ...move to the water source...
271+
.step(MoveToWaterSource { speed: 1.0 })
272+
// ...and then drink.
273+
.step(Drink { per_second: 10.0 });
274+
275+
// Build the thinker
276+
let thinker = Thinker::build()
277+
// We don't do anything unless we're thirsty enough.
278+
.picker(FirstToScore { threshold: 0.8 })
279+
.when(
280+
Thirsty,
281+
move_and_drink,
282+
);
283+
284+
cmd.spawn()
285+
.insert(Thirst::new(75.0, 2.0))
286+
.insert(Position {
287+
position: Vec2::new(0.0, 0.0),
288+
})
289+
.insert(
290+
thinker,
291+
);
292+
}
293+
294+
fn main() {
295+
// Once all that's done, we just add our systems and off we go!
296+
App::new()
297+
.add_plugins(DefaultPlugins)
298+
.add_plugin(BigBrainPlugin)
299+
.add_startup_system(init_entities)
300+
.add_system(thirst_system)
301+
.add_system_to_stage(BigBrainStage::Actions, drink_action_system)
302+
.add_system_to_stage(BigBrainStage::Actions, move_to_water_source_action_system)
303+
.add_system_to_stage(BigBrainStage::Scorers, thirsty_scorer_system)
304+
.run();
305+
}

0 commit comments

Comments
 (0)