Open seivan opened 3 months ago
Problem is - Yoetz does not maintain such a list.
This is YoetzAdvisor
:
It does not keep track of all the suggestions so far - it only needs the top_suggestion
. If a new suggestion scores worse than the current top one - the new suggestion is immediately discarded. If a new suggestion scores better than the current top one - it immediately becomes the new top one, and the old top one is immediately discarded.
Hmm, looking at the code it does seems possible for it to actually maintain a list, but comes with a performance hit if I am not mistaken?
Correct. If needed, I'm willing to consider a supporting various storage options. Putting it on YoetzPlugin
will get kind of ugly without HKT, but putting it on the YoetzSuggestion
can be neat - especially considering it gets generated by a derive macro:
#[derive(YoetzSuggestion)]
#[yoetz(storage = Vec)] // maybe it needs a better name than "storage"?
enum AiBehavior {
// ...
}
I do, however, want to open to debate the question of this really is the right abstraction. Maybe it would be better to add an option to interfere at the suggestion stage, determining with custom user code if the new suggestion should replace the old one? If you can achieve your goals with that, it'd be more performant than switching to a list.
Another option: since the only reason to collect it into a list is so that user code can temper with that list, why not go one step farther and leave the entire handling of the list to the user code?
First, you'll define the behavior like this:
#[derive(YoetzSuggestion)]
#[yoetz(score = ())]
enum AiBehavior {
// ...
}
If you don't set #[yoetz(score = ...)]
it'll default to f32
. And of course - it can be other numeric types. But if you set it to ()
, it'll disable the suggest
method, and instead add a decide
method that forces a specific value.
The Suggest systems will not interact with Yoetz, and instead use user defined components (or maybe even events?) to store their suggestions:
fn suggest_system_foo(mut query: Query<(&mut MyOwnSuggestionStruct, ...)>) {
for (mut suggestions, ...) in query.iter_mut() {
suggestions.append(AiBehavior::Foo { ... });
}
}
fn suggest_system_bar(mut query: Query<(&mut MyOwnSuggestionStruct, ...)>) {
for (mut suggestions, ...) in query.iter_mut() {
suggestions.append(AiBehavior::Bar { ... });
}
}
fn suggest_system_baz(mut query: Query<(&mut MyOwnSuggestionStruct, ...)>) {
for (mut suggestions, ...) in query.iter_mut() {
suggestions.append(AiBehavior::Baz { ... });
}
}
fn suggest_system_qux(mut query: Query<(&mut MyOwnSuggestionStruct, ...)>) {
for (mut suggestions, ...) in query.iter_mut() {
suggestions.append(AiBehavior::Qux { ... });
}
}
At this point, of course, it will not interact with Yoetz yet at all. For that, you'll need another system (in a new system set, which will happen between YoetzSystemSet::Suggest
and YoetzInternalSystemSet::Think
):
fn suggest_system_foo(mut query: Query<(
&mut MyOwnSuggestionStruct,
&mut YoetzAdvisor<AiBehavior>,
...
)>) {
for (mut suggestions, mut advisor, ...) in query.iter_mut() {
let decision = decide_on_suggestion(suggestions.as_mut());
advisor.decide(decision);
suggestions.clear();
}
}
This will give user code free reign over the decision process (Yoetz doesn't do anything awfully complicated in that department anyway) leaving Yoetz responsible for it's main feature - arranging the components based on the decision so that the Act systems can
Correct. If needed, I'm willing to consider a supporting various storage options. Putting it on
YoetzPlugin
will get kind of ugly without HKT, but putting it on theYoetzSuggestion
can be neat - especially considering it gets generated by a derive macro:#[derive(YoetzSuggestion)] #[yoetz(storage = Vec)] // maybe it needs a better name than "storage"? enum AiBehavior { // ... }
Why is the storage type important, whatever storage it is it's not going to be that impactful.
I do, however, want to open to debate the question of this really is the right abstraction. Maybe it would be better to add an option to interfere at the suggestion stage, determining with custom user code if the new suggestion should replace the old one? If you can achieve your goals with that, it'd be more performant than switching to a list.
I think you're right about questioning the abstraction, I am not 100% sold mostly because it's a X Y problem. The issue is baby sitting numbers here because a proper solution would be having the variation on the thinking logic itself. Instead of selecting from the end result.
Why is the storage type important, whatever storage it is it's not going to be that impactful.
As the comment says - "storage" is probably not the best name here. But I'll use it as long as we don't have a better name. The idea of this approach is that the storage also defines the way suggestions are collected. The default is Option
, where new suggestions either immediately replace the old ones - or are immediately discarded. It's a simplistic scheme, but it has the least overhead.
Vec
means that suggestions get appended to the vector. And I guess either the best one gets picked, or maybe it gets sorted before the pick? Either way, it allows for tempering with the vector before Yoetz takes the suggestion from it and converts it to components.
I'll create a YoetzStorage
trait that implements both Option
and Vec
(for types that implement YoetzSuggestion
), and user code could also implement it to add more complex behaviors.
But I think my second idea may be a better solution. Why manipulate the list to get Yoetz to pick the entry you want from it, when you can just take that entry out of the list yourself and pass it to Yoetz?
In hindsight I think this just pollutes the namespace and the API design. I would not carry it out. There are some other issues that are more pressing ( I made another ticket about it).
The problem with Yoetz (or Utility AI / Fuzzy Logic) in general is that you end up tweaking the crap out of numbers.
With something like GOAP (yes I am aware it has costs) at least the driving force on what to do is the GoalState
.
The problem is GOAP isn't really that compatible with ECS and running things in parallel so it's moot.
With Utility AI you don't have a GoalState. The GoalState is whatever scores the highest and that becomes the task/action at hand. While GOAP will build those actions for you.
Most of it can be solved, but I wouldn't do the things you are suggesting, there are more pressing matters.
My focus would be to look into the state handling of actions and debugging tools to avoid baby sitting the hell out of the numbers to get the behaviour you want.
State handling is more pressing matter as current. Followed by debugging tools. It would be nice to see what all the scores were before the regular actions run, without having to write that cache. In general Utility AI needs decent debugging tools.
Followed by a guideline on what default systems are running from the plugin. I am guessing a lot of them are macros
Since the YoetzAdvisor
is being used everywhere, I am guessing none of the systems for scoring run in parallel.
But yeah, built in state management of actions is a big piece missing.
I am not sure if I should close this ticket. I still think there needs to be a list of scores for debugging purposes. I've managed to solve my issue
There should be a way to get the final sorted list of actions to override which one gets picked. Maybe there is a need for randomization or maybe avoid babysitting score values for exceptions that can occur.