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
.
Utility AI library for the Bevy game engine
License: Other
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
.
Now with the version of Bevy out, it would be nice if Big-Brain could support it.
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?
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
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.
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.
What is the intended use of the provided Evaluator
s 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.
There should be a step-by-step guide on how to get started with big-brain and do incrementally more complex things.
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`
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
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
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)
I think this'll make what they actually do much more clear?
Also need to rename the consider:
field to scorers:
?
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.
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);
}
}
}
}
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.
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.
Big Brain used to have Crystal AI-style Weight
s for its Consideration
s (now called Scorer
s). 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?
Big Brain used to have Crystal AI-style Measure
s. This was removed as part of #1. That said, it seems like something of the sort might be useful to people?
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.
I have another issue:
But what I would like is this:
let get_food = Steps::build()
.label("GetFood")
.step(FindFood)
.step(GoTo);
Thinker::build()
.label("DefaultThinker")
.picker(Highest)
.when(HungryScorer, get_food)
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.
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?
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 Arc
s and are immutable.
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
?
As found here:
Line 148 in 4e203b7
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
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:
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.
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 stuffAllOrNothing
- 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.
I think we can do much better than this dual Action/ActionBuilder, Thinker/ThinkerBuilder, Scorer/ScorerBuilder nonsense.
Everything needs docs, with examples
Line 219 in efcbd94
This line accesses an entity, which is only spawned (in commands buffer) two lines above. It does not yet have a ActionState component and only gets it at the end of the stage.
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)
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
).
ScorerBuilder
was, and how it was related to me just creating a struct with Component
and Clone
.#[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)
ScorerBuilder
to have a Label
. `#[derive(ScorerBuilder, label="MyScorer")]`
pub struct MyScorer;
...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?
more bevy-flavored this way
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.
subj
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!
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.
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
when
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?
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?
It's kinda ugly and error-prone right now. Can we make it nice, at least at the top level? Fixing #12 made things a little worse and I don't like it :(
We have Steps. But it would be nice to have one that executes several actions concurrently
Get rid of Measures altogether. I don't think they add very much and they just... complicate the whole system :(
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)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.