Git Product home page Git Product logo

bonsai's Introduction

Bonsai 盆栽

Rust implementation of Behavior Trees

Build Status Bonsai crate minimum rustc 1.56 Docs codecov Maintenance GitHub pull-requests GitHub pull-requests closed ViewCount License: MIT

Contents

Using Bonsai

Bonsai is available on crates.io. The recommended way to use it is to add a line into your Cargo.toml such as:

[dependencies]
bonsai-bt = "*"

What is a Behavior Tree?

A Behavior Tree (BT) is a data structure in which we can set the rules of how certain behavior's can occur, and the order in which they would execute. BTs are a very efficient way of creating complex systems that are both modular and reactive. These properties are crucial in many applications, which has led to the spread of BT from computer game programming to many branches of AI and Robotics.

How to use a Behavior tree?

A Behavior Tree forms a tree structure where each node represents a process. When the process terminates, it signals Success or Failure. This can then be used by the parent node to select the next process. A signal Running is used to tell the process is not done yet.

For example, if you have a state A and a state B:

  • Move from state A to state B if A succeeds: Sequence([A, B])
  • Move from state A to sequence of states [B] if A is running. If all states in the sequence [B] succeed in order, check if A is still running and repeat. Stop if A succeeds or any of the states fail: RepeatSequence(A, [B])
  • Try A first and then try B if A fails: Select([A, B])
  • If condition succeedes do A, else do B : If(condition, A, B)
  • If A succeeds, return failure (and vice-versa): Invert(A)
  • Do B repeatedly while A runs: While(A, [B])
  • Do A, B forever: While(WaitForever, [A, B])
  • Run A and B in parallell and wait for both to succeed: WhenAll([A, B])
  • Run A and B in parallell and wait for any to succeed: WhenAny([A, B])
  • Run A and B in parallell, but A has to succeed before B: After([A, B])

See the Behavior enum for more information.

Calling long-running tasks in behavior tree

To make sure that the behavior tree is always responsive, it is important that the actions that are created executes instantly so that they do not block the tree traversal. If you have long-running tasks/functions that can take seconds or minutes to execute - either async or sync - then we can dispatch those jobs into background threads, and get status of the task through a channel.

see async drone example in the /examples folder for more details.

Example of use

See Examples folder.

Similar Crates

Bonsai is inspired by many other crates out there, here's a few worth mentioning:

bonsai's People

Contributors

ddmills avatar kaphula avatar sollimann 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

bonsai's Issues

Remove front page readme code example

I feel like the example code given on the front page or readme should be implemented as fully working code example, removed completely or a link should be given to one of the currently existing examples.

The reason being that since the code is not in any way connected to the build system and tests, it can easily get out of date and confuse new users. It also did not help me much in understanding how to use this library initially since I could not just copy the code to a new empty Rust project and run it without getting errors. This is the reason why I implemented the simple NPC example to aid my own understanding of the library.

Clone breaks BT

Attempting to integrate this project with GDExt I discovered the following behavior.

By simply modifying the game_tick function in the Simple NPC Behavior example to have the following:

let status = bt.clone().tick()...

instead of

let status = bt.tick()...

I encountered an infinite loop that develops.

reached main loop...
NPC is alive...
NPC has action points: 3
Performing action: run. Action points: 2
Performing action: shoot. Action points: 1
NPC has action points: 1
Performing action: run. Action points: 0
Cannot perform action: shoot. Not enough action points.
NPC does not have actions points left... 
Rested for a while... Action points: 1
reached main loop...
NPC is alive...
NPC has action points: 1
Performing action: run. Action points: 0
Cannot perform action: shoot. Not enough action points.
NPC does not have actions points left... 
Rested for a while... Action points: 1
reached main loop...
NPC is alive...
NPC has action points: 1
Performing action: run. Action points: 0
Cannot perform action: shoot. Not enough action points.
NPC does not have actions points left... 
Rested for a while... Action points: 1
reached main loop...

Minimal code example here. You also need to derive(clone) for your blackboard

My operating assumption was that clone semantics couldn't induce this kind of error, but turns out I don't understand it as well as I thought. Is there a way to resolve this where clone could be used?

(also I see this is on your kanban board when I get my gdext implementation working, would you like a copy?)

Separate graph drawing functionality from BT structure

As far as I understand, the graph field in BT struct is only utilized when and if someone wants to create a visual graph from the behavior tree. This means that everytime a behavior tree is constructed, the data related to drawing graphs gets constructed as well even though it may be useless. Would it not make sense to separate the graphing data away from the behavior tree's struct and perhaps construct the needed graph data on the fly when someone wishes to make use of the graph?

Although it would probably be advised to cache your behavior trees in advance and clone them later during a life time of an application, if someone wishes to build behavior trees dynamically many times during a single frame of a game for example, getting rid of the graph stuff makes even more sense since it seems to be the "heaviest part" in behavior tree constructor.

Please add some pre-action features:enhancement

I hope that this kind of feature is added.

  1. Register some function to the bonsai bt.
fn check_sleep(..) -> (Status, f64) {
    if monster.is_slept() {
        return (Running, 0.0)
    }
    ... // do next action
  1. Select actions to which the function is called.
bt.register(check_sleep, vec![Monster::Attack, Monster::Walk, ...]);
  1. Registered function is called before selected action is called in a behavior tree.

Bevy code example

Seeing that the repo is tagged with bevy I took a stab at using it along with Bevy, but I'm kind of stuck... The problem is essentially that I am expected to tick the BT and apply all action logic inside the closure passed to tick:

pub fn tick<E, F>(&mut self, e: &E, f: &mut F) -> (Status, f64)
    where
        E: UpdateEvent,
        F: FnMut(ActionArgs<E, A>, &mut BlackBoard<K>) -> (Status, f64),
        A: Debug,
{}

This means, like the example given under lib.rs, I am suppose to check the action enum I defined and do w/e I need to do there, for example:

let (_status, _dt) = bt.tick(&e, &mut |args, blackboard| match *args.action {
    Actions::Inc => {
        acc += 1;
        (Success, args.dt)
    }
    Actions::Dec => {
        acc -= 1;
        (Success, args.dt)
    }
});

In Bevy, this result in a monolithic system that tries to do everything... so afaik, a system with exclusive world access that looks something like:

fn tick_bt_tree(world: &mut World){
    let mut system_state: SystemState<(
        Res<Time>,
        Query<(Entity, BT)>,
    )> = SystemState::new(world);
    
    let (time, query) = system_state.get_mut(world);
    let e: Event = UpdateArgs { time.elapsed().to_f64() }.into();
    for (entity, bt) in &mut query {
        let (_status, _dt) = bt.tick(&e, &mut |args, blackboard| match *args.action {
            Actions::Inc => {
                // get data from `world`
                // but we can't do this because we're already borrowing `world` mutably
                (Success, args.dt)
            }
            // ...similarly for other actions
        }
    }
}

Like the comment added, I essentially can't access the same world that I retrieved the BT from since it's mutably borrowed (system_state is a piece of data mutably borrowed from world).

...This feels kind of like a dumb question but it's something that I nevertheless couldn't figure out. Not familiar with this repo but since it's tagged with bevy would it make sense to add something that could explain how someone would go about using it with bevy?

How do you make a while loop that does not check the loop condition continuously?

How do you make a while loop that does not check the loop condition continuously? It seems that when you create a loop with While, the condition for the While is checked between each behavior. Sometimes what you want is for the behaviours to run and succeed in sequence order before the condition for the upper While is ran to determine if the While block should continue executing another loop, basically just like it is with any programming language and its while loop.

        let loop1 = While(
            Box::new(Action(Self::LoopCondition)),
            vec![ Action(Self::Task1), Action(Self::Task2) ]
        );

This is what happens:

  1. check loop condition (running)
  2. run Task1 (running)
  3. check loop condition (running)
  4. run Task1 (success)
  5. check loop condition (success)
  6. while is terminated

And this is what I want:

  1. check loop condition (running)
  2. run Task1 (running)
  3. run Task1 (success)
  4. run Task2 (success)
  5. check loop condition (success)
  6. while is terminated

I also tried to throw Sequence as a wrapper, but the While still keeps checking the condition between each behavior.

        let loop2 = While(
            Box::new(Action(Self::LoopCondition)),
            vec![
                Sequence(vec![Action(Self::Task1), Action(Self::Task2)])
            ],
        );

Generate Behavior Tree with Bonsaï from a BT.CPP XML

@Sollimann
BehaviorTree CPP is a major library for creating Behavior Tree with C++. It provides an XML Schema to serialize BTs and instantiate them at runtime.

<?xml version="1.0" encoding="ASCII"?>
<!DOCTYPE root>
<root main_tree_to_execute="Usecase1">
  <BehaviorTree ID="Usecase1">
    <Fallback name="">
      <Sequence name="">
        <Action name="" ID="MoveTo" loc_id="kitchen"/>
        <Sequence name="">
          <Action name="" ID="FindObj" obj_id="bottle" obj_p="{object_pose}"/>
          <Action name="" ID="ComputeGraspPose" obj_p="{object_pose}" grasp_p="{grasp_pose}"/>
          <Action name="" ID="OpenGripper"/>
          <Action name="" ID="MoveTo" loc_p="{grasp_pose}" />
          <Inverter>
            <Action name="" ID="CloseGripper"/>
          </Inverter>
        </Sequence>
      </Sequence>
      <Action name="" ID="AskFor" action_id="help"/>
    </Fallback>
  </BehaviorTree>
</root>

It will be great for interoperability and to promote Bonsai to have a way to reuse a serialized BT in BT CPP XML format in Bonsai. I worked on a code generation tool called BT.CPP2Bonsai that generates the template code of a behavior tree with Bonsai from such an XML file. I would like to know if you are interested in this feature in the library. If so, I could make a pull request.

Add node editor for real-time visualization of behavior tree

egui_node_graph looks like the best option.

Potential flow:

  1. Create behavior tree
  2. Convert behavior tree to a immutable node graph
  3. Could use mpsc channels to tell which node is active

We could also have that the behavior tree sets up a websocket service, that writes which node is active. Then a second service will read that and compile the tree. The second service would need to read a json to know how the tree is composed such that it can be visualized.

Simply NPC AI example appears to be broken

Behavior

An error is thrown on cargo build:

error[E0446]: crate-private type `SequenceArgs<'_, A, E, F, B>` in public interface
  --> /home/guest/.cargo/registry/src/index.crates.io-6f17d22bba15001f/bonsai-bt-0.6.0/src/sequence.rs:19:1
   |
4  |   pub(crate) struct SequenceArgs<'a, A, E, F, B> {
   |   ---------------------------------------------- `SequenceArgs<'_, A, E, F, B>` declared as crate-private
...
19 | / pub fn sequence<A, E, F, B>(args: SequenceArgs<A, E, F, B>) -> (Status, f64)
20 | | where
21 | |     A: Clone,
22 | |     E: UpdateEvent,
23 | |     F: FnMut(ActionArgs<E, A>, &mut B) -> (Status, f64),
24 | |     A: Debug,
   | |_____________^ can't leak crate-private type

For more information about this error, try `rustc --explain E0446`.
error: could not compile `bonsai-bt` (lib) due to previous error

Steps to Replicate

Copy enemy npc code into a project. Run cargo build

How do I change wait seconds dynamically?

When I'm using like Wait(1.1), it will wait for 1.1 seconds. I want to change that value without re-constructing whole tree.
Or can Action wait for specific period? I've tried return (Status, f64) with custom f64 value, but it doesn't seem to work.

Find a node editor that can take the behavior tree and convert into a tree/graph

egui_node_graph looks like the best option.

Potential flow:

  1. Create behavior tree
  2. Convert behavior tree to a immutable node grapha
  3. Could use mpsc channels to tell which node is active

We could also have that the behavior tree sets up a websocket service, that writes which node is active. Then a second service will read that and compile the tree. The second service would need to read a json to know how the tree is composed such that it can be visualized.

Let the Blackboard be a generic type

The current implementation of the Blackboard struct in our Rust codebase utilizes a HashMap for key/value storage. While this is functional for many scenarios, there are cases where other data structures might be more appropriate. This issue proposes generalizing the Blackboard struct to allow for various data structures, enhancing its flexibility and potential use cases.

Current Implementation (ref link)

#[derive(Clone, Debug)]
pub struct BlackBoard<K, V>(HashMap<K, V>);

The Blackboard struct is currently hard-coded to use HashMap. This limits its use to scenarios best served by a hash map's characteristics (e.g., quick lookups, key/value storage).

Proposed Change
I propose that we refactor the Blackboard struct to be generic over its data structure, not just over the key and value types. This would allow users of the Blackboard to select the most appropriate data structure for their specific needs, whether that be a HashMap, BTreeMap, Vec, or any other collection type.

Suggested Traits for the Data Structure

  • Iterable Trait: To allow iteration over the elements.
  • Clone + Debug: These are already in use and should be maintained for consistency and ease of debugging.
  • Serialize + Deserialize (optional): For scenarios where blackboard data needs to be serialized/deserialized, e.g., when saving/loading state or for network communication.

Default behavior
Ideally, if a user does not provide a particular data structure, we should default to something like HashMap<(), ()> = HashMap::new();

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.