Closed ghost closed 1 year ago
I also had to sit down for multiple hours to wrap my head around this. I also find the documentation lacking, but it has been a major reason for me to dive into this project tbh 😄 . Here's how I see it:
There is a Thinker
that has a list of things it can do (actions
), but it needs to know what the top priority (scorers
) is for it to know what to do next. A scorer decides, with a number from 0 to 1, how important something is to do.
Then you have Picker
s. They kinda give the Thinker
a behavior on what to choose next:
Thinker::build()
.picker(FirstToScore { threshold: 0.8 })
That one basically means: If a Scorer
is above 0.8, do that. If not, ignore it. So if no Score is over 0.8, the AI basically does nothing.
Thinker::build()
.picker(First)
This one chooses what to do based on any score above 0. It will do whatever has the highest score.
In the examples there are Thirst
and Thirsty
or Hunger
and Hungry
, they kinda mean the same thing. But there can be something more complex which might make you understand it better:
For example: Money
and Broke
.
With Money
being multiple things your Entity can have: Cash
, BankBalance
, FriendsThatCanHelpYouOut
, Stocks
...etc
and Broke
being the scorer for the Thinker
for it to evaluate what it should do based on the score on how Broke
the entity is.
Maybe that's a bad example though.. What would an Action
for that be, if the Thinker
decided to execute on it? 😄 That said, a scorer does not necessarily have to track a single Component of an Entity.
Here are some of my scorers..
This one checks if it's time to go to work based on how many workers are currently working at its workplace. If it's unmanned, the score is higher:
#[derive(Clone, Component, Debug, ScorerBuilder)]
pub struct TimeToWork;
pub fn work_scorer_system(
workers: Query<&Workplace>,
workplaces: Query<&AvailableWorkers>,
mut query: Query<(&Actor, &mut Score), With<TimeToWork>>,
) {
for (Actor(actor), mut score) in query.iter_mut() {
if let Ok(workplace) = workers.get(*actor) {
// check workplace working times
if let Ok(work) = workplaces.get(workplace.0) {
score.set(1. - (work.current_workers / work.max_workers) as f32);
continue;
}
score.set(0.);
}
}
}
This one checks if the inventory is low. The action could be to go to the store, hunt or go to some water source perhaps?
#[derive(Clone, Component, Debug, ScorerBuilder)]
pub struct InventoryLow;
pub fn inventory_scorer_system(
inventories: Query<Ref<Inventory>>,
mut query: Query<(&Actor, &mut Score), With<InventoryLow>>,
) {
for (Actor(actor), mut score) in query.iter_mut() {
if let Ok(inventory) = inventories.get(*actor) {
if inventory.is_changed() || inventory.is_added() || score.is_added() {
score.set(1. - (inventory.food + inventory.drink) as f32 * 0.1);
}
}
}
}
To wrap it up:
let go_shopping = Steps::build()
.label("GoToStoreAndShop")
.step(FindClosestStore)
.step(GoToDestination)
.step(Shopping {
time_to_buy: Timer::new(Duration::from_secs(1), TimerMode::Repeating),
});
let go_work = Steps::build()
.label("GoWork")
.step(GoWork)
.step(GoToDestination)
.step(Work {
shift: Timer::new(Duration::from_secs(60), TimerMode::Once),
});
Thinker::build()
.label("PeopleThinker")
.picker(Highest)
.when(InventoryLow, go_shopping)
.when(TimeToWork, go_work);
// there is more stuff here but I didn't paste it here
GoToDestination
is also an action that just goes to the location of a Destination
component on the Actor and can be universally used. The GoWork
and FindClosestStore
are actions which set that component on the Actor and succeed instantly.
Once you have a Thinker
with multiple Scorer
s and a First
Picker
, it will start executing Actions
if the scores are above 0.
It will create a new Entity that tracks the progress, with a way to select the initiator (The Actor
) via a separate Query. That way you can mutate the Entity you added the Thinker
on.
Here is an action of mine that that is related to the work scorer above 😄
#[derive(Clone, Component, Debug, ActionBuilder)]
pub struct Work {
pub shift: Timer,
}
pub fn work_action_system(
time: Res<Time>,
workers: Query<(&Name, &Workplace)>,
mut workplaces: Query<(&mut AvailableWorkers, Option<&Name>), Without<Workplace>>,
mut query: Query<(&Actor, &mut ActionState, &mut Work, &ActionSpan)>,
) {
for (Actor(actor), mut state, mut work, span) in query.iter_mut() {
let _guard = span.span().enter();
let (actor_name, workplace) = workers
.get(*actor)
.expect("actor for action not found. Maybe it's missing some components we need?");
let (mut available_workers, workplace_name) = workplaces
.get_mut(workplace.0)
.expect("Workplace not found");
match *state {
ActionState::Requested => {
match workplace_name {
Some(name) => println!("{:?} starting work shift at {:?}..", actor_name, name),
None => println!("{:?} starting work shift..", actor_name),
}
available_workers.current_workers += 1;
*state = ActionState::Executing;
}
ActionState::Executing | ActionState::Cancelled => {
if !work.shift.tick(time.delta()).finished() {
continue;
}
work.shift.reset();
println!("{:?} is done working.", actor_name);
available_workers.current_workers -= 1;
*state = ActionState::Success;
}
_ => {}
}
}
}
All it actually does is wait for a timer to pass and increases/decreases a counter. A separate system, outside of big-brain, handles the workplace output based on available workers.
Since the ActionState::Executing
and ActionState::Cancelled
are together, the Thinker
will have to wait till the shift is over before being able to decide on what to do next 😄 I haven't yet figured out what would be a better way to handle the Cancelled step.. Maybe the shift can pause and continue later again. Hmm.. 🤔
I often noticed actions being cancelled very often.. If some other scorer in the meantime got higher than the scorer of the action currently being executed, the Thinker
might cancel the current action, since it's got more important things to do. I think it's often not necessary to cancel actions. To do this you can often match the executing and cancelled action state together like so:
ActionState::Executing | ActionState::Cancelled => {}
That way your action will continue until you set the state to Failure
or Success
.
I hope this helps you out a bit! I might edit this post later since it's late and I'm currently very tired 😄
I'm gonna close this as a duplicate of #9
Hi,
I want to try making a simulation game and I tried looking at the example with "thirst", but I don't really understand how all of that works. I think I'm lacking knowledge in AI to understand, does some people have recommendation of article or publication I could read to understand ?
Thanks