Git Product home page Git Product logo

big-brain's Introduction

Hi there ๐Ÿ‘‹

I'm Kat!

I'm a Rust, C#, and JavaScript developer working at Microsoft and I do a bunch of open source stuff.

You can also find me on Mastodon as @[email protected] and Matrix as @kat:zkat.tech, or on Discord as kat#8645.

big-brain's People

Contributors

cbournhonesque avatar elabajaba avatar erlend-sh avatar guillaumegomez avatar jim-works avatar mrpicklepinosaur avatar ndarilek avatar nisevoid avatar nosideeffects avatar payload avatar piedoom avatar psikik avatar sirhall avatar squimmy avatar stargazing-dino avatar striezel avatar tantandev avatar vortex avatar weibye avatar werner291 avatar will-hart avatar zkat avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

big-brain's Issues

Support Bevy 0.9

Now with the version of Bevy out, it would be nice if Big-Brain could support it.

Bevy-less Implementation

so I use macroquad as my rendering framework as i don't need anything more fancy
and now i want to implement ai for my game and I started porting big_brain to just Bevy_Ecs and so far this been easy
and one of the stuff i found is that most entities have children attached to it but from what I saw that isn't needed?

Combining scorers produces incorrect behavior

The predefined scorers in scorers.rs, which can be used to combine different scorers for a single consideration by the thinker, show incorrect behavior when you combine then. Because all scorers get evaluated as systems there is no way to guarantee the ordering makes sense when they depend on other (possibly even the same) scorers. This results in scores lagging behind by an arbitrary number of frames, depending on the execution order and how deep they are nested.

I think the correct solution for combining multiple scorers would be to calculate them when the thinker checks them, this way we also wouldn't need to calculate anything unnecessary with pickers like FirstTo

Steps is broken

Hi, the Steps thing is broken somehow. I have a sequence of actions but it gets stuck on the second action in an Init state. It seems to work sometimes when I load the program, but most times not.

Pure Rust thinker builder API

Right now, the only way to define thinkers is using the .ron API. This... mostly works. But it would be nice if there were a nice way to build these using just plain old rust.

Evaluator with [0..1) range

What is the intended use of the provided Evaluators given the limited Score range of 0 to 1? Both exponential and sigmoid functions have very different utility in the domain of 0 to 1 vs the previous limit of 100. What is the intention behind clamping the Score values in the first place? I understand the desire to have the final output clamped for decision making, but I see no reason to clamp the intermediary values.

Write guide

There should be a step-by-step guide on how to get started with big-brain and do incrementally more complex things.

Ability to support generics in ActionBuilder derived Component struct.

Hi, I am trying to use generic components to look for targets.

In plain bevy it works fine, but when I try to add it to big-brain, I got an error.

#[derive(Clone, Component, Debug, ActionBuilder)]
pub struct Find<T> (PhantomData<T>);

impl<T> Find<T> {
    fn new() -> Self {
        Find(PhantomData)
    }
}

Error:

37 | pub struct Find<T> (PhantomData<T>);
   |            ^^^^ expected 1 generic argument
   |
note: struct defined here, with 1 generic parameter: `T`

Thinker `ThinkerIterations` question

As I understand it, a thinker will run based on some ThinkerIterations values, and will guarantee that the thinker run at least every 10ms, or when it cycles back on the frame count if time is not elapsed.

Is it possible to set this value somewhere ? as the current value is just the Default for ThinkerIterations

impl Default for ThinkerIterations {
    fn default() -> Self {
        Self::new(Duration::from_millis(10))
    }
}

Also would this be applicable to Actions as well, to avoid the ::Executing on all entities every frame

Need guide for real beginner

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

Alt design pattern with In Out Systems

Hiyo ! I was playing around with other possible patterns - mostly out of curiosity and as a learning experience and was recently reading a one shot systems related PR and thought that'd be cool. Anyways, here's how I'd think it'd make sense as a utility ai architecture

pub struct BrainPlugin;

impl Plugin for BrainPlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(Startup, setup)
            .add_systems(Update, (decide_best_action, sleepiness_tick));
    }
}

#[derive(Component)]
pub struct Thinker;

#[derive(Component)]
pub struct Sleepiness(f32);

#[derive(Bundle)]
pub struct MyActorBundle {
    pub thinker: Thinker,
    pub considerations: Considerations,
    pub action_state: ActionState,
    pub sleepiness: Sleepiness,
}

#[derive(Component)]
pub struct Considerations {
    pub considerations: Vec<Consideration>,
}

#[derive(Component)]
pub enum ActionState {
    Idle,
    Executing,
    Done,
}

pub struct Consideration {
    pub name: String,
    pub scorer: SystemId<Entity, f32>,
    pub action: SystemId<ActionState, ActionState>,
}

fn my_sleep_scorer(In(entity): In<Entity>, sleepiness: Query<&Sleepiness>) -> f32 {
    let sleepiness = sleepiness.get(entity).unwrap();
    sleepiness.0 / 100.0
}

fn sleep(In(action_state): In<ActionState>) -> ActionState {
    todo!();
}

fn decide_best_action(world: &mut World) {
    let mut highest_consideration: Option<&Consideration> = None;
    let mut highest_score = 0.0;
    let mut query = world.query::<(Entity, &Considerations)>();

    for (actor_entity, considerations) in query.iter(world) {
        for consideration in &considerations.considerations {
            // This doesn't compile because of multiple borrows :(
            let Ok(score) = world.run_system_with_input(consideration.scorer, actor_entity) else {
                continue;
            };

            if score > highest_score {
                highest_consideration = Some(consideration);
                highest_score = score;
            }
        }
    }

    if let Some(consideration) = highest_consideration {
        let Ok(next) = world.run_system_with_input(consideration.action, ActionState::Idle) else {
            return;
        };

        todo!("set next action state");
    }
}

fn setup(world: &mut World) {
    let scorer = world.register_system(my_sleep_scorer);
    let action = world.register_system(sleep);

    world.spawn(MyActorBundle {
        thinker: Thinker,
        action_state: ActionState::Idle,
        considerations: Considerations {
            considerations: vec![Consideration {
                name: "Sleep".into(),
                scorer: scorer,
                action: action,
            }],
        },
        sleepiness: Sleepiness(0.0),
    });
}

fn sleepiness_tick(mut sleepiness: Query<&mut Sleepiness>) {
    for mut sleepiness in sleepiness.iter_mut() {
        sleepiness.0 += 1.0;
    }
}

I hope you find this interesting ! I thought so and just wanted to share so I could hear your opinion on it :D

I'm a big rust noob so this might be totally wrong/absurd

(also sorry if this was better suited as a discussion ! lol)

Should `Score` be spawning on a new child entity?

Looking through the code and I notice Score is being spawned on a new child entity of what's actually performing the AI, why not instead make Score generic (via a PhantomData) over the Picker (which yes means propagating the generic up through to the Thinker, making Thinker generic over the Picker, but that seems reasonable and would allow removal of another allocation by letting you get rid of the Arc around the Picker, and thus also getting rid of the dynamic pick call as well), or is it this way for another reason that I'm not seeing in the code? By putting the Picker-generic-Score on the same entity as the AI as well means you can get rid of another query in the systems and have it all in the same query, meaning no dynamic entity lookup is needed either, which saves even more processing time in tight loops. Potentially you could even reuse the same score the same pickers that may exist on multiple thinkers if someone does that as well, saving even more processing.

`otherwise` pre-empts further steps in sequential actions

Not sure if this is desired behavior. If I have a sequential action whose first action scorer stops returning its scoring success criteria, sequential actions keep executing. If I have an otherwise action, that otherwise action seems to run instead of the sequential actions.

I'm guessing what's happening here is that the picker returns no action. Then for some reason, when sequential actions are requested without an otherwise, the sequence runs. But if otherwise is configured, it runs instead and the sequence stops.

I have a minimal reproduction below. It does what I'd expect until the otherwise line is uncommented, then it breaks.

My actual use case is that systems tag entities with components based on their proximity to things. So if an enemy is within a certain range of a kill, it hears it. That in turn trips the scorer, which kicks off an action, which then clears whatever data tripped the scorer. This basically kicks off a whole "investigate/hunt around/return to post" cycle, but in the middle of that cycle I want the option for a new kill to be heard, thus basically failing the current investigate/hunt routine and starting it over again at the new coordinates. And I think that works, until I try adding in an idle routine to give them an at-rest behavior. Then it breaks. I don't think the at-rest behavior should win when a sequential action is running, even if no scorer trips.

Anyhow, here's the example. If in stage 1 the Increment action increments to stage 2. The InStage1 scorer then fails, but with the otherwise line commented, the subsequent LogIt action without an associated scorer triggers. Uncomment otherwise and the only action that runs is Idle.

Thanks for all your work on this library!

use bevy::prelude::*;
use big_brain::prelude::*;

fn main() {
    App::new()
        .add_plugins(MinimalPlugins)
        .add_plugin(BigBrainPlugin)
        .add_startup_system(setup)
        .add_system_to_stage(BigBrainStage::Scorers, in_stage1_scorer)
        .add_system_to_stage(BigBrainStage::Actions, increment)
        .add_system_to_stage(BigBrainStage::Actions, log_it)
        .add_system_to_stage(BigBrainStage::Actions, idle)
        .run();
}

fn setup(mut commands: Commands) {
    commands.spawn().insert(Stage(1)).insert(
        Thinker::build()
            .picker(FirstToScore::new(1.))
            .when(InStage1, Steps::build().step(Increment).step(LogIt))
            // .otherwise(Idle),
    );
}

#[derive(Component, Deref, DerefMut, Debug)]
struct Stage(u8);

#[derive(Component, Default, Clone, Debug)]
struct InStage1;

fn in_stage1_scorer(
    mut actors: Query<(&Actor, &mut Score), With<InStage1>>,
    stages: Query<&Stage>,
) {
    for (Actor(actor_entity), mut score) in &mut actors {
        if let Ok(stage) = stages.get(*actor_entity) {
            if **stage == 1 {
                println!("In stage 1");
                score.set(1.);
            } else {
                score.set(0.);
            }
        } else {
            score.set(0.);
        }
    }
}

#[derive(Component, Default, Debug, Clone)]
struct Increment;

fn increment(
    mut actors: Query<(&Actor, &mut ActionState), With<Increment>>,
    mut stages: Query<&mut Stage>,
) {
    for (Actor(actor_entity), mut state) in &mut actors {
        if let Ok(mut stage) = stages.get_mut(*actor_entity) {
            match *state {
                ActionState::Requested => {
                    println!("Increment requested");
                    *state = ActionState::Executing;
                }
                ActionState::Executing => {
                    println!("Incrementing");
                    **stage += 1;
                    *state = ActionState::Success;
                }
                _ => {}
            }
        }
    }
}

#[derive(Component, Default, Debug, Clone)]
struct LogIt;

fn log_it(mut actors: Query<(&Actor, &mut ActionState), With<LogIt>>, stages: Query<&Stage>) {
    for (Actor(actor_entity), mut state) in &mut actors {
        if let Ok(stage) = stages.get(*actor_entity) {
            match *state {
                ActionState::Requested => {
                    println!("Logging requested");
                    *state = ActionState::Executing;
                }
                ActionState::Executing => {
                    println!("Stage: {:?}", stage);
                    *state = ActionState::Executing;
                }
                _ => {}
            }
        }
    }
}

#[derive(Component, Default, Debug, Clone)]
struct Idle;

fn idle(mut actors: Query<(&Actor, &mut ActionState), With<Idle>>) {
    for (Actor(actor_entity), mut state) in &mut actors {
        match &*state {
            ActionState::Requested => {
                println!("Idle requested");
                *state = ActionState::Executing;
            }
            ActionState::Executing => {
                println!("Idle executing");
                *state = ActionState::Executing;
            }
            state => {
                println!("Idle {:?}", state);
            }
        }
    }
}

Support multiple Thinkers per Entity

After fixing #13, the change also made it so you can basically only have one Thinker per Entity. Might be nice to bring back multiple Thinkers but I don't think it's high priority.

[Feature] Manual Triggering of Actions

The Action system in Big Brain is quite powerful. However, not all actions in a game are triggered by the AI itself. There might be traps, triggers, player input, etc. It would be very powerful to enable manual triggering of such actions. However, currently this does not seem to be possible.

An Action can be added to an actor as follows:

        let drink_action_builder = DrinkBuilder;
        drink_action_builder.attach(&mut cmd, actor_entity);

However, there is no way to replicate the logic in thinker.rs#347::

        let new_action = picked_action.1.attach(cmd, actor);
        thinker.current_action = Some((ActionEnt(new_action), picked_action.clone()));

Because thinker.current_action is private.

Bring back weights?

Big Brain used to have Crystal AI-style Weights for its Considerations (now called Scorers). I wasn't sure they would be very useful, but maybe they are? It's worth thinking about bringing some kind of weight system back?

Clean up actions on success/failure

Ran into a surprising behavior. I have an Investigate action that sets ActionState::Success when pathfinding reaches the destination. I then tried to add a component that triggered a wait and ReturnToPost action when the timer expired from the ActionState::Success match arm.

Unfortunately, I discovered that successful actions don't seem to stop or get cleaned up. They just perpetually run in ActionState::Success. So in this case, the handler kept running and re-adding the timer, and my second action didn't run.

Not sure if this is a bug or by design. I removed the action component from the actor myself when the action succeeded. Ideally, I'd have liked it if the action hung around in the success state for long enough for my system to notice, then cleaned up so the actor didn't infinitely run the success state handler.

Allow thinker to restart a given action

I have another issue:

  • i have a pathfinding agent that is trying to find food
  • my scorer computes a score for 'find-food', and puts in an intermediate component the position of the food to eat
  • then i have a sequence action consisting of 'FindFoodPath' (compute the path) and 'WalkOnPath' (just execute the path)

But what I would like is this:

  • the systems keep running, and if the FindFoodScorer gets a higher scorer than the original FindFoodScorer that started the sequence, then I cancel the current sequence and I start a new one.
  • I thought this is already what would happen (the scorer cancels the current action and executes a new one if the scorer becomes higher). But the problem is that this time the action chosen is the same one, just for a different parameter (different food to eat), so the Thinker doesn't cancel the existing FindFoodPath + WalkOnPath sequence.
  • I can probably find workarounds, but they are a bit clunky. It's just something to think about: how to allow the thinker to cancel the current action to re-start an action of the same type
    let get_food = Steps::build()
        .label("GetFood")
        .step(FindFood)
        .step(GoTo);

    Thinker::build()
        .label("DefaultThinker")
        .picker(Highest)
        .when(HungryScorer, get_food)
  • HungryScorer: finds the closest food and assigns a score based on how close it is.
  • GetFood: find a path to the food chosen by HungryScorer and executes on the path
    What i would like is:
  • if at some point HungryScorer finds a better food (closer food position, = higher score), then I cancel my current get_food action and start a new one
    i.e. some kind of mode where the Action stores the original Score value that triggered it, and if the Scorer gets higher score, we restart the action (even if it's the same action-type)

Potential solutions

  • In Here, allow cancelling the current action if the new-score for the same action is higher than the original score (instead of only spawning a new action if its a different action-type)

  • For a given action A, add a way for A to constantly have access to the score that leads to action A + the original score that led to action A being picked. (maybe just a link the corresponding scorer entity?) -> might not be enough in the case of Steps, because a user can only access individual actions that compose steps, not steps itself, unless Steps is refactored a bit)

  • basically a HighestUnchanging picker, that remembers which Choice it picked, and then picks "None" if the score for that item has changed. Make sure that when None is picked, the current action is Canceled.

Ability for the Action to modify the Scorer Entity?

I'm currently trying to make a Scorer that increases its score based on how much a timer has passed:

#[derive(Debug, Clone, Component, ScorerBuilder)]
pub struct TimePassed(pub Timer);

pub fn timer_scorer_system(
    time: Res<Time>,
    mut query: Query<(&Actor, &mut Score, &mut TimePassed)>,
) {
    for (Actor(actor), mut score, mut timer) in query.iter_mut() {
        if !timer.0.tick(time.delta()).finished() {
            score.set(0.);
            continue;
        }

        score.set(1.);
    }
    return;
}

With a repeating timer, the 1 frame it has to set the score to 1 is not enough for the thinker to start the action. And even if so, it would probably just cancel the action immediately due to the score dropping to 0. So in this case I was wondering if i could somehow manage to access the Scorer in the action to reset the timer when the action has finished, but I'm not sure how to do that.

I was thinking of adding the timer to the Actor entity, but I can see Thinkers with multiple TimePassed Scorers, so that does not really work either.

Do you have an idea on how I can accomplish something like this?

Make `ThinkerBuilder` be `Clone`

This should be trivial, and I don't see a reason not to do it: it would really improve reuse when having a lot of entities with similar/identical thinkers. This won't require ActionBuilder and ScorerBuilder to be Clone, since they're already wrapped in Arcs and are immutable.

Graphical demo

Would be nice to have a more involved example that actually does some neat graphical stuff. Maybe it would be cool to just adapt an example from bevy_tilemap?

docs.rs build failing for version 0.19.0

I was looking for the docs on the latest big-brain version 0.19.0 and noticed the build failed on docs.rs.

Build link: https://docs.rs/crate/big-brain/0.19.0/builds/1095409

Error:

# rustc version
rustc 1.77.0-nightly (25f8d01fd 2024-01-18)
# docs.rs version
docsrs 0.6.0 (7667f348 2024-01-17)

# build log
[INFO] running `Command { std: "docker" "create" "-v" "/home/cratesfyi/workspace/builds/big-brain-0.19.0/target:/opt/rustwide/target:rw,Z" "-v" "/home/cratesfyi/workspace/builds/big-brain-0.19.0/source:/opt/rustwide/workdir:ro,Z" "-v" "/home/cratesfyi/workspace/cargo-home:/opt/rustwide/cargo-home:ro,Z" "-v" "/home/cratesfyi/workspace/rustup-home:/opt/rustwide/rustup-home:ro,Z" "-e" "SOURCE_DIR=/opt/rustwide/workdir" "-e" "CARGO_TARGET_DIR=/opt/rustwide/target" "-e" "DOCS_RS=1" "-e" "CARGO_HOME=/opt/rustwide/cargo-home" "-e" "RUSTUP_HOME=/opt/rustwide/rustup-home" "-w" "/opt/rustwide/workdir" "-m" "6442450944" "--cpus" "6" "--user" "1001:1001" "--network" "none" "ghcr.io/rust-lang/crates-build-env/linux@sha256:2788e3201cd34a07e3172128adcd8b3090168a8e3bcc40d7c032b9dda1df7d1c" "/opt/rustwide/cargo-home/bin/cargo" "+nightly" "rustdoc" "--lib" "-Zrustdoc-map" "-Z" "unstable-options" "--config" "build.rustdocflags=[\"-Z\", \"unstable-options\", \"--emit=invocation-specific\", \"--resource-suffix\", \"-20240118-1.77.0-nightly-25f8d01fd\", \"--static-root-path\", \"/-/rustdoc.static/\", \"--cap-lints\", \"warn\", \"--extern-html-root-takes-precedence\"]" "--offline" "-Zunstable-options" "--config=doc.extern-map.registries.crates-io=\"https://docs.rs/{pkg_name}/{version}/x86_64-unknown-linux-gnu\"" "-Zrustdoc-scrape-examples" "-j6" "--target" "x86_64-unknown-linux-gnu", kill_on_drop: false }`
[INFO] [stdout] 6b4581db6dd6718b86d83417b6be1d59c29393a2449847d389bade21481e4a6d
[INFO] [stderr] WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap.
[INFO] running `Command { std: "docker" "start" "-a" "6b4581db6dd6718b86d83417b6be1d59c29393a2449847d389bade21481e4a6d", kill_on_drop: false }`
[INFO] [stderr] warning: Rustdoc did not scrape the following examples because they require dev-dependencies: farming_sim, custom_measure, concurrent, sequence, thirst, one_off
[INFO] [stderr]     If you want Rustdoc to scrape these examples, then add `doc-scrape-examples = true`
[INFO] [stderr]     to the [[example]] target configuration of at least one example.
[INFO] [stderr] warning: Target filter specified, but no targets matched. This is a no-op
[INFO] [stderr]     Checking bevy_transform v0.12.1
[INFO] [stderr] error: lifetime may not live long enough
[INFO] [stderr]   --> /opt/rustwide/cargo-home/registry/src/index.crates.io-6f17d22bba15001f/bevy_transform-0.12.1/src/systems.rs:14:1
[INFO] [stderr]    |
[INFO] [stderr] 14 | / pub fn sync_simple_transforms(
[INFO] [stderr] 15 | |     mut query: ParamSet<(
[INFO] [stderr] 16 | |         Query<
[INFO] [stderr] 17 | |             (&Transform, &mut GlobalTransform),
[INFO] [stderr]    | |              - let's call the lifetime of this reference `'1`
[INFO] [stderr] ...  |
[INFO] [stderr] 26 | |     mut orphaned: RemovedComponents<Parent>,
[INFO] [stderr] 27 | | ) {
[INFO] [stderr]    | |_^ requires that `'1` must outlive `'static`
[INFO] [stderr] 
[INFO] [stderr] error: lifetime may not live long enough
[INFO] [stderr]   --> /opt/rustwide/cargo-home/registry/src/index.crates.io-6f17d22bba15001f/bevy_transform-0.12.1/src/systems.rs:14:1
[INFO] [stderr]    |
[INFO] [stderr] 14 | / pub fn sync_simple_transforms(
[INFO] [stderr] 15 | |     mut query: ParamSet<(
[INFO] [stderr] 16 | |         Query<
[INFO] [stderr] 17 | |             (&Transform, &mut GlobalTransform),
[INFO] [stderr]    | |                          - let's call the lifetime of this reference `'2`
[INFO] [stderr] ...  |
[INFO] [stderr] 26 | |     mut orphaned: RemovedComponents<Parent>,
[INFO] [stderr] 27 | | ) {
[INFO] [stderr]    | |_^ requires that `'2` must outlive `'static`
[INFO] [stderr] 
[INFO] [stderr] error: lifetime may not live long enough
[INFO] [stderr]   --> /opt/rustwide/cargo-home/registry/src/index.crates.io-6f17d22bba15001f/bevy_transform-0.12.1/src/systems.rs:14:1
[INFO] [stderr]    |
[INFO] [stderr] 14 | / pub fn sync_simple_transforms(
[INFO] [stderr] 15 | |     mut query: ParamSet<(
[INFO] [stderr]    | |     --------- has type `ParamSet<'_, '_, (Query<'_, '_, (&transform::Transform, &mut global_transform::GlobalTransform), (Or<(Changed<transform::Transform>, Added<global_transform::GlobalTransform>)>, Without<Parent>, Without<Children>)>, Query<'_, '_, (bevy_ecs::change_detection::Ref<'3, transform::Transform>, &mut global_transform::GlobalTransform), (Without<Parent>, Without<Children>)>)>`
[INFO] [stderr] 16 | |         Query<
[INFO] [stderr] 17 | |             (&Transform, &mut GlobalTransform),
[INFO] [stderr] ...  |
[INFO] [stderr] 26 | |     mut orphaned: RemovedComponents<Parent>,
[INFO] [stderr] 27 | | ) {
[INFO] [stderr]    | |_^ requires that `'3` must outlive `'static`
[INFO] [stderr] 
[INFO] [stderr] error: lifetime may not live long enough
[INFO] [stderr]   --> /opt/rustwide/cargo-home/registry/src/index.crates.io-6f17d22bba15001f/bevy_transform-0.12.1/src/systems.rs:14:1
[INFO] [stderr]    |
[INFO] [stderr] 14 | / pub fn sync_simple_transforms(
[INFO] [stderr] 15 | |     mut query: ParamSet<(
[INFO] [stderr] 16 | |         Query<
[INFO] [stderr] 17 | |             (&Transform, &mut GlobalTransform),
[INFO] [stderr] ...  |
[INFO] [stderr] 24 | |         Query<(Ref<Transform>, &mut GlobalTransform), (Without<Parent>, Without<Children>)>,
[INFO] [stderr]    | |                                - let's call the lifetime of this reference `'4`
[INFO] [stderr] 25 | |     )>,
[INFO] [stderr] 26 | |     mut orphaned: RemovedComponents<Parent>,
[INFO] [stderr] 27 | | ) {
[INFO] [stderr]    | |_^ requires that `'4` must outlive `'static`
[INFO] [stderr] 
[INFO] [stderr] error: could not compile `bevy_transform` (lib) due to 4 previous errors
[INFO] running `Command { std: "docker" "inspect" "6b4581db6dd6718b86d83417b6be1d59c29393a2449847d389bade21481e4a6d", kill_on_drop: false }`
[INFO] running `Command { std: "docker" "rm" "-f" "6b4581db6dd6718b86d83417b6be1d59c29393a2449847d389bade21481e4a6d", kill_on_drop: false }`
[INFO] [stdout] 6b4581db6dd6718b86d83417b6be1d59c29393a2449847d389bade21481e4a6d

`FirstToScore` cancels ongoing action when different action passes threshold

I've found out that FirstToScore behaves not how I thought it should.

Here's a code snippet:

        // Define a multistep action of moving and drinking
        let move_and_drink = Steps::build()
            .label("MoveAndDrink")
            .step(MoveToWaterSource)
            .step(Drink);

        // Define a multistep action of moving and eating
        let move_and_eat = Steps::build()
            .label("MoveAndEat")
            .step(MoveToFoodSource)
            .step(Eat);

        // Define the thinker
        let thinker = Thinker::build()
            .label("Thinker")
            .picker(FirstToScore { threshold: 0.8 })
            .when(Hungry, move_and_eat)
            .when(Thirsty, move_and_drink);

Situation:

  1. Thirsty is 0.6, Hungry is 0.8
  2. Thinker starts executing move and eat action.
  3. Meanwhile doing the MoveToFoodSource part of the action, Thirsty reaches the threshold of 0.8.

Expected Behavior: Since the score calculation is FirstToScore, the move_and_eat action is continuing execution because it reached the threshold first.

Actual Behavior: MoveToFoodSource gets cancelled and MoveAndDrink starts executing.

In this exact case it this happens because move_and_eat is defined first in the thinker and move_and_drink second. It prioritizes the firstly defined action.

Write several "composite Scorers"

After removing the Scorer vector feature, it's gotten harder to compose several scorers together. Let's write some "composite" scorers that people can just use out of the box!

Some ideas:

  • FixedScore - always returns the same score. Useful for setting minimum values for stuff
  • AllOrNothing - Returns the sum of all its child scores if all scorers score above its threshold. Otherwise, scores 0.
  • SumOfChildren - Returns the sum of all its child scores if the sum is above its threshold, even if some children score below it.

These are all taken from Apex AI and should be credited as such, but we should build our own larger library of these over time.

simplify API

I think we can do much better than this dual Action/ActionBuilder, Thinker/ThinkerBuilder, Scorer/ScorerBuilder nonsense.

Make big-brain Game Engine independent

Was looking at common utility AI systems like BehaviorTrees and GOAP which I could integrate into Godot 4 using the GDExtension interface (see https://github.com/godot-rust/gdext)

Would it be possible to make this AI system game engine independent (abstract away the Bevy-specific part) so that it can be used by other Game Engines?

Godot has these overrideable functions, (https://docs.godotengine.org/en/stable/tutorials/scripting/overridable_functions.html)

Require deriving explicitly `ActionBuilder` or `ScorerBuilder`

I think it would be helpful to require users to derive explicitly ActionBuilder and ScorerBuilder (and to change all the impl ActionBuilder into ActionBuilder, so that users need to provide explicitly a ActionBuilder).

  1. Too much flexibility is kind of confusing, and not that helpful. When i first read the docs I was confused as to what a ScorerBuilder was, and how it was related to me just creating a struct with Component and Clone.
    I think it would be much clearer for users to have their scorers-builders/action-builders with explicit #[derive(ScorerBuilder)]

This is the exact same argument as: bevyengine/bevy#1843
In theory, it's more flexible to accept any std::fmt::Debug + Sync + Send as a ScorerBuilder (by allowing pre-existing types to be ScorerBuilders, but in practice it just makes it less clear, and i believe 99% of users won't use a pre-existing type as a scorer builder)

  1. Easy labels. I think right now a user needs to explicitly implement ScorerBuilder to have a Label.
    By forcing all ScorerBuilders to have the explicit type, users could easily add the label via
 `#[derive(ScorerBuilder, label="MyScorer")]`
 pub struct MyScorer;

Despawn Scorers when Thinkers despawn

...I'm pretty sure we're just adding a ton of duplicate Scorers whenever we switch around between nested Thinker "actions". Should probably fix it. Maybe come up with a way that others implementing such things won't forget and memory leak like hell?

Stop action_builders on entity despawn

When running my game I've came across a warning when killing my enemy entities. The warning is as follows:

WARN bevy_ecs::world: error[B0003]: Could not despawn entity 25646v68 because it doesn't exist in this World.

After a lot of debugging I figured out that after I despawn the entity, the big-brain crate keeps a reference to that entity in the Actor struct. And attempts to perform some despawning of its own. I've tried to move the system that despawns entities to the PostUpdate and PreUpdate stages, but still the same result.

Add `ActionState::Finally` that always runs

I have actions that can succeed or fail. Independently of that, they generally need to run some cleanup to ensure that scorers know what has already been handled.

It'd be nice if actions first ran with ActionState::Success or ActionState::Failure, then an ActionState::Final match could handle all my cleanup. Now I'm performing all my cleanup in Success or Failure arms which is a bit confusing, particularly if I want a failure case but don't want to repeat the cleanup.

Thanks!

[Suggestion] Use SparseSets for ActionState Component and Benchmarks

Bevy 0.5 introduced SparseSet Storage (see Release Notes) for components with high add/remove volume to avoid . If I'm understanding, from my (somewhat cursory) reading of the code that ActionState probably falls into this category and would probably generate a performance boost if added.

I might be very wrong on the way that ActionState is implemented, but it would be very good to add some benchmarks for performance validation.

Debugging and logging

I'm finding it a bit tricky to debug thinkers as the Thinker is a bit opaque to my code. What I'd love to have is a way to inspect the Thinker state for an Actor. For instance

  • the result of all the scorers
  • the result of compound scorers
  • the final result of the scorer passed to when
  • the action that these scores are related to

At the moment, this requires me instrumenting all the scorer systems, and reimplementing the built-in Scorers with additional debug output. However there is still no way to link the scorer back to the action as e.g. SumOfScorers isn't aware of the action it relates to.

Is there a recommended way to debug thinkers?

Add Composite Action

We should have a built-in Action type that executes multiple sub-actions in order. Alterrnately, maybe just make the then be an array that supports multiple actions?

Simplify Scorers

Get rid of Measures altogether. I don't think they add very much and they just... complicate the whole system :(

Implement Reflect

It could be useful to implement Reflect on all components so that they can be displayed with bevy_inspector_egui.
(could be behind a feature-flag)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.