Open seivan opened 1 month ago
What will Yoetz do with that state? Currently the Act systems don't decide when the action ends - it ends automatically when the suggest systems stop suggesting it, or when an action gets suggested with a higher score.
Since you've mentioned GOAP - do you mean that Yoetz should add the ability to register sequences of actions, where when one action ends its components get replaced by the next action in the sequence?
What will Yoetz do with that state? Currently the Act systems don't decide when the action ends - it ends automatically when the suggest systems stop suggesting it, or when an action gets suggested with a higher score.
When I say "no way", "not have" or "not possible" I mean it in a way that it's not without a ton of hand holding or boilerplate. These can be solved right now, but it's unnecessary boilerplate and since the problem domain is common, it's going to be a lot of it.
Right now there's a few consequences with not having a clear state:
There's no distinction between start | running | end
of a suggested action.
Actually running
is somewhat solved with a property called state
. But that's not enough.
There is no way to design long running actions.
There is no way to know when to clear up resources once an action is done.
It forces your code to be granular (example further below).
Yoetz
will interrupt an action by suggesting something else. That interrupted action
doesn't get a chance to react. The new register_component_hooks#hooks.on_remove
can, but it doesn't tell you why. If the scene transition to main menu, then why react. Yoetz
needs to inform. Again, as mentioned earlier many things can be solved with unnecessary boilerplate.
Some of these can be remedied with the concept of a clearer action state rather than designing state keys as properties for the action itself.
Obviously they can be solved by writing a bunch of state code and inject it in all the suggestions to flag for no-op, or baby sitting scores by temporarily using f32::MAX
for scores and etc, but it's wasteful and messes things up.
Earlier I mentioned register_component_hooks#hooks.on_remove
but it doesn't tell you the reason the component was removed, so you have to code that yourself by adding a bunch of states so you can track it.
In your example code you have something like
fn enemies_follow_player(mut query: Query<(&EnemyBehaviorChase, &mut Transform)>, time: Res<Time>) {
for (chase, mut transform) in query.iter_mut() {
let Some(direction) = chase.vec_to_target.try_normalize() else {
continue;
};
transform.translation += 5.0 * time.delta_seconds() * direction.extend(0.0);
}
}
What if you have ally_follow_player
?
Or animal_follow_player
?
They will have their own behaviours and Yoetz
suggestions.
Let say we revisit this in order to generalize movement and write a Movement
component.
This is a bit contrived but shows why you need it
fn enemies_follow_player(mut query: ...) {
let target = ..
let command = ..
let enemy = ..
command(enemiy).insert(Movement {
target: player,
is_interruptible: true
cancel_distance: Some(10.0) // Default None, otherwise handled by movement
});
// Assume other components are added: e.g sneaking, emoting, sounds and etc all very specific to EnemyBehaviourChase.
}
So we've created a set of resources.
Where do we clean them up? When do we clean them up? Why do we clean them up?
ActionState::Load => {
// Safe programming...
// Assume it's zipped options because of the conditional checks.
.map(|(target, enemy)| enemy.insert(Movement {
target: target,
is_interruptible: true
cancel_distance: Some(10.0) // Default None, otherwise handled by movement
}))
.map(|_| ActionResult::Success)
.unwrap_or(ActionState::Failure)
}
ActionState::Run => {
movement_components
.get(entity)
.map(|m| if m.is_done() { ActionResult::Success } else {ActionResult::Running } )
.unwrap_or(ActionState::Failure)
}
ActionState::Unload => {
movement_components
.get(entity)
.filter(|m| m.interruptible())
.map(commands.entity.remove)
.unwrap_or(ActionResult::Success)
}
Since you've mentioned GOAP - do you mean that Yoetz should add the ability to register sequences of actions, where when one action ends its components get replaced by the next action in the sequence?
Not at all. That defeats the purpose of being reactive for 90% of the time. There are instances with the need for a long running non-interruptible sequences actions, these ideas will help. But it's not a "plan" and is outside of the scope of Yoetz
.
If the user wants to leak outside the abstraction for a long running task they're gonna have to make sure that the suggestion responsible for this:
f32::MAX
for the suggestion.Doesn't even have to be for a "long plan". It could be as simple as for just Movement
.
So... this is actually two things, right?
Movement
) that get automatically removed when the action is done (replaced by another action)I've created discussions for these two things: #9 and #10. Let's discuss there.
How do I describe that an Action is
starting
,running
andfailed
? Thefailed
part is a bit silly becauseUtility AI
always rechecks scores to be dynamic, while e.gGOAP
only does it when the when theactor_state
orgoal_state
changed from an external source, but also if an action returnsfailed
orreplan
state. But just bear with me for a second.We need a way to describe starting, running, failing states for the
Action
componentsLoad
sets things up, like adding aMovement
component, whileUnload
could remove things. You could obviously do it in theRun
state when it'sSuccess
but this makes it cleaner and useful for debugging purposes as well to have an unload.The problem right now is, there is too much boilerplate expected to do banal stuff that the AI should have built in for. Handling state for the action in terms of progress isn't there, so you would have to add it for every single action. You would also have to look it up on every single YoetzAdvisor/Suggestion if there is a in progress task.
This can all be fixed by a lot of boilerplate but this should inherently be builtin. Not necessarily like this design I've shown here. Though I like it because it forces a return state on the programmer so you won't forget.
There are some other examples on issues that can occur because there isn't any state handling, but they're all related to the fact that there isn't a way to have a an in progress task associated with an action with a ton of boilerplate.