A behavior tree implementation for clojure and clojurescript. Also includes a state machine built on top of its primitives.
This implementation started with the idea that the game AI behavior trees could find a place in regular application developement.
There are many flavors of behavior trees. This one takes inspiration on the libgdx behavior trees. Although we had to adapt our implementation to our functional purity requirements, it might be a good idea to read their excellent libgdx behavior trees documentation.
This implementation is processing nodes in a depth first manner, leaving these in one of 4 states:
Each time the core/tick function is called, the tree is traversed, skipping :success and :failure nodes. The :fresh and :running nodes are "ticked", possibly ticking their children. They adjust their status according to their parameters and children status.
This allows the user to describe the modeled processes sequentially although these might be asynchronous.
This implementation communicates with the outside world via incoming and outgoing events and, most importantly, the blackboard.
The behavior tree's database has a blackboard key which contains the application specific data. The tree can act or react on/to this data. The user can also update its value between tick invokations.
The core namespace defines a whole menagerie of functions to query and manipulate the blackboard. These functions have their names starting with bb-
.
This is a little overview, the nodes are described in more details in the node definitions documentation.
These node have no children.
These nodes have a single child
These nodes have several children
Extending the tree with new nodes is expected and probably necessary depending on the use cases. Ping me on slack (Carkh) if the need arises and you can't figure it out.
The tree is described with a hiccup-like notation, then compiled to a more efficient data structure. Many of the parameters defined for different node types can take either a value or a function, that will then be executed receiving the tree context as its parameter. Additional keys may be passed to a node's parameter map with no adverse effect.
;; we define a traffic light that goes through all colors
(defn traffic-light-1 []
(-> [:repeat
[:sequence
[:update {:func (bt/bb-setter :green)}]
[:timer {:timer :traffic-light :duration 50000}]
[:update {:func (bt/bb-setter :yellow)}]
[:timer {:timer :traffic-light :duration 10000}]
[:update {:func (bt/bb-setter :red)}]
[:timer {:timer :traffic-light :duration 60000}]]]
bt/hiccup->context (bt/tick 0)))
;; Real use would not use the time parameter when calling the tick function.
(defn do-traffic-light-tests [traffic-light]
(is (= :green (-> traffic-light bt/bb-get)))
(is (= :green (-> traffic-light (bt/tick+ 49999) bt/bb-get)))
(is (= :yellow (-> traffic-light (bt/tick+ 50000) bt/bb-get)))
(is (= :red (-> traffic-light (bt/tick+ 50000) (bt/tick+ 10000) bt/bb-get)))
(is (= :green (-> traffic-light (bt/tick+ 50000) (bt/tick+ 10000) (bt/tick+ 60000) bt/bb-get)))
(is (= :yellow (-> traffic-light (bt/tick+ 50000) (bt/tick+ 10000) (bt/tick+ 60000) (bt/tick+ 50000) bt/bb-get)))
;;do the same in a single tick (catching up an exceptionally long GC pause !)
(is (= :red (-> traffic-light (bt/tick+ 60000) bt/bb-get)))
(is (= :green (-> traffic-light (bt/tick+ 120000) bt/bb-get)))
(is (= :yellow (-> traffic-light (bt/tick+ 170000) bt/bb-get))))
In the cark.behavior-tree.state-machine namespace, we present an implementation of state machines based on the primitives offered by the behavior trees. The functions of this namespace are indeed merely producing a hiccup tree that will then need to be compiled with the bt/hiccup->context, like any other regular behavior tree.
Compared to a hand coded state machine, our implementation has poor performances (though still quite fast!). On the other hand, it brings the full power of the behavior trees with it. First we gain the benefits of the blackboard. Also such a state machine may be included inside a behavior tree, or use full sub-trees as event handlers. That means that we automatically support hierarchical and parallel state machines, although special care must then be taken with event naming and blackboard paths.
If nothing else, this state machine implementation illustrates how the relatively verbose behavior tree hiccup notation can be encapsulated into higher level, more specialized constructs.
;; we model the UI for a backup and restore page
(def ctx
(-> (sm/make [:sm] :start
(sm/state :start
(sm/enter-event [:update {:func (bt/bb-updater-in [:flags] set/union #{:restore-button :backup-button})}])
(sm/event :restore-pressed (sm/transition :restore))
(sm/event :backup-pressed (sm/transition :backup))
(sm/event :ok-pressed
[:update {:func (bt/bb-updater-in [:flags] set/difference #{:success-dialog :error-dialog})}]))
(sm/state :backup
(sm/enter-event
[:sequence
[:send-event {:event :prepare-backup}]
[:update {:func (bt/bb-assocer-in [:flags] #{:backuping-dialog})}]])
(sm/event :got-database-string
[:sequence
[:send-event {:event :download-database-string :arg sm/event-arg}]
[:update {:func (bt/bb-updater-in [:flags] disj :backuping-dialog)}]
(sm/transition :start)]))
(sm/state :restore
(sm/enter-event [:update {:func (bt/bb-assocer-in [:flags] #{:confirm-dialog})}])
(sm/event :cancel-pressed
[:sequence
[:update {:func (bt/bb-assocer-in [:flags] #{})}]
(sm/transition :start)])
(sm/event :got-file-data
[:sequence
[:send-event {:event :restore-file :arg sm/event-arg}]
[:update {:func (bt/bb-assocer-in [:flags] #{:restoring-dialog})}]])
(sm/event :restore-result
[:sequence
[:update {:func #(cond-> (bt/bb-update % assoc :flags #{:restore-button :backup-button})
(= :success (sm/event-arg %)) (bt/bb-update-in [:flags] conj :success-dialog)
(= :error (sm/event-arg %)) (bt/bb-update-in [:flags] conj :error-dialog))}]
(sm/transition :start)])))
bt/hiccup->context bt/tick))
There are quite a few tests that should help in understanding how the tree works.
They all can be run, for both clojurescript and clojure, with these commands :
npm install
clojure -A:all-tests
It is also possible to run the clojurescript tests with shadow-cljs in your prefered browser with this command :
npx shadow-cljs watch test
You then can navigate to http://localhost:8022/ and see the test results.
In the src/test directory, you'll find a couple real world'ish examples.
Of particular interest is the change control example were we present the process of coming up with a working, modular behavior tree, testing each part along the way. That example exercises many of the primitives provided by this library.
Copyright (c) Sacha De Vos and contributors. All rights reserved.
The use and distribution terms for this software are covered by the Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) which can be found in the file LICENSE.html at the root of this distribution. By using this software in any fashion, you are agreeing to be bound by the terms of this license. You must not remove this notice, or any other, from this software.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close