idanarye / bevy-yoetz

Apache License 2.0
7 stars 2 forks source link

Allow getting the sorted list of behaviours for overriding. #2

Open seivan opened 3 months ago

seivan commented 3 months ago

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.

idanarye commented 3 months ago

Problem is - Yoetz does not maintain such a list.

This is YoetzAdvisor:

https://github.com/idanarye/bevy-yoetz/blob/c0d7626a242d021291c8083d33b20823719f795e/src/advisor.rs#L56-L63

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.

seivan commented 3 months ago

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?

idanarye commented 3 months ago

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.

idanarye commented 3 months ago

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

seivan commented 3 months ago

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 {
    // ...
}

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.

idanarye commented 3 months ago

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?

seivan commented 2 months ago

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