Documentation ¶
Overview ¶
Package behaviortree provides a simple and powerful Go implementation of behavior trees without fluff.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Manager ¶
type Manager interface { Ticker // Add will register a new ticker under this manager Add(ticker Ticker) error }
Manager models an aggregate Ticker, which should stop gracefully on the first failure
func NewManager ¶
func NewManager() Manager
NewManager will construct an implementation of the Manager interface, which is a stateful set of Ticker implementations, aggregating the behavior such that the Done channel will close when ALL tickers registered with Add are done, Err will return a combined error if there are any, and Stop will stop all registered tickers.
Note that any error (of any registered tickers) will also trigger stopping, and stopping will prevent further Add calls from succeeding.
type Node ¶
Node represents an node in a tree, that can be ticked
func New ¶
New constructs a new behavior tree aliasing NewNode with vararg support for less indentation
type Status ¶
type Status int
Status is a type with three valid values, Running, Success, and Failure, the three possible states for BTs
func All ¶ added in v1.2.0
All implements a tick which will tick all children sequentially until the first running status or error is encountered (propagated), and will return success only if all children were ticked and returned success (returns success if there were no children, like sequence).
func Selector ¶
Selector is a tick implementation that ticks each child sequentially, until the the first error (returning the error), the first non-failure status (returning the status), or all children are ticked (returning failure)
func Sequence ¶
Sequence is a tick implementation that ticks each child sequentially, until the the first error (returning the error), the first non-success status (returning the status), or all children are ticked (returning success)
type Tick ¶
Tick represents the logic for a node, which may or may not be stateful
func Any ¶ added in v1.2.0
Any wraps a tick such that non-error non-running statuses will be overridden with a success if at least one child succeeded - which is achieved by encapsulation of children, before passing them into the wrapped tick. Nil will be returned if tick is nil, and nil children will be passed through as such.
Example (AllPartialSuccess) ¶
fmt.Println(New( Any(All), New(func(children []Node) (Status, error) { fmt.Println(1) return Success, nil }), New(func(children []Node) (Status, error) { fmt.Println(2) return Success, nil }), New(func(children []Node) (Status, error) { fmt.Println(3) return Success, nil }), New(func(children []Node) (Status, error) { fmt.Println(4) return Success, nil }), New(func(children []Node) (Status, error) { fmt.Println(5) return Failure, nil }), New(func(children []Node) (Status, error) { fmt.Println(6) return Success, nil }), ).Tick())
Output: 1 2 3 4 5 6 success <nil>
Example (ForkPartialSuccess) ¶
var ( c1 = make(chan struct{}) c2 = make(chan struct{}) c3 = make(chan struct{}) c4 = make(chan struct{}) c5 = make(chan struct{}) c6 = make(chan struct{}) status = Running ) go func() { time.Sleep(time.Millisecond * 100) fmt.Println(`unblocking the forked nodes`) close(c1) time.Sleep(time.Millisecond * 100) close(c2) time.Sleep(time.Millisecond * 100) close(c3) time.Sleep(time.Millisecond * 100) close(c4) time.Sleep(time.Millisecond * 100) close(c5) time.Sleep(time.Millisecond * 100) close(c6) }() node := New( Any(Fork()), New(func(children []Node) (Status, error) { fmt.Println(`ready`) <-c1 fmt.Println(1) return Success, nil }), New(func(children []Node) (Status, error) { fmt.Println(`ready`) <-c2 fmt.Println(2) return Success, nil }), New(func(children []Node) (Status, error) { fmt.Println(`ready`) <-c3 fmt.Println(3) return status, nil }), New(func(children []Node) (Status, error) { fmt.Println(`ready`) <-c4 fmt.Println(4) return Failure, nil }), New(func(children []Node) (Status, error) { fmt.Println(`ready`) <-c5 fmt.Println(5) return Failure, nil }), New(func(children []Node) (Status, error) { fmt.Println(`ready`) <-c6 fmt.Println(6) return Success, nil }), ) fmt.Println(node.Tick()) fmt.Println(`same running behavior as Fork`) fmt.Println(node.Tick()) fmt.Println(`but the exit status is overridden`) status = Failure fmt.Println(node.Tick())
Output: ready ready ready ready ready ready unblocking the forked nodes 1 2 3 4 5 6 running <nil> same running behavior as Fork ready 3 running <nil> but the exit status is overridden ready 3 success <nil>
Example (ResetBehavior) ¶
var ( status Status err error node = New( Any(Sequence), New(func(children []Node) (Status, error) { fmt.Println(1) return status, err }), New(func(children []Node) (Status, error) { fmt.Println(2) return Success, nil }), ) ) status = Success err = nil fmt.Println(node.Tick()) status = Failure err = nil fmt.Println(node.Tick()) status = Success err = errors.New(`some_error`) fmt.Println(node.Tick()) status = Success err = nil fmt.Println(node.Tick())
Output: 1 2 success <nil> 1 failure <nil> 1 failure some_error 1 2 success <nil>
Example (Running) ¶
status := Running node := New( Any(All), New(func(children []Node) (Status, error) { fmt.Printf("child ticked: %s\n", status) return status, nil }), ) fmt.Println(node.Tick()) status = Failure fmt.Println(node.Tick()) status = Running fmt.Println(node.Tick()) status = Success fmt.Println(node.Tick())
Output: child ticked: running running <nil> child ticked: failure failure <nil> child ticked: running running <nil> child ticked: success success <nil>
Example (SequencePartialSuccess) ¶
fmt.Println(New( Any(Sequence), New(func(children []Node) (Status, error) { fmt.Println(1) return Success, nil }), New(func(children []Node) (Status, error) { fmt.Println(2) return Success, nil }), New(func(children []Node) (Status, error) { fmt.Println(3) return Success, nil }), New(func(children []Node) (Status, error) { fmt.Println(4) return Success, nil }), New(func(children []Node) (Status, error) { fmt.Println(5) return Failure, nil }), New(func(children []Node) (Status, error) { panic(`wont reach here`) }), ).Tick())
Output: 1 2 3 4 5 success <nil>
func Background ¶ added in v1.2.0
Background pushes running nodes into the background, allowing multiple concurrent ticks (potentially running independent children, depending on the behavior of the node). It accepts a tick via closure, in order to support stateful ticks. On tick, backgrounded nodes are ticked from oldest to newest, until the first non-running status is returned, which will trigger removal from the backgrounded node list, and propagating status and any error, without modification. All other normal operation will result in a new node being generated and ticked, backgrounding it on running, otherwise discarding the node and propagating it's return values immediately. Passing a nil value will cause nil to be returned. WARNING there is no upper bound to the number of backgrounded nodes (the caller must manage that externally).
Example (AsyncJobQueue) ¶
ExampleBackground_asyncJobQueue implements a basic example of backgrounding of long-running tasks that may be performed concurrently, see ExampleNewTickerStopOnFailure_counter for an explanation of the ticker
type ( Job struct { Name string Duration time.Duration Done chan struct{} } ) var ( // doWorker performs the actual "work" for a Job doWorker = func(job Job) { fmt.Printf("[worker] job \"%s\" STARTED\n", job.Name) time.Sleep(job.Duration) fmt.Printf("[worker] job \"%s\" FINISHED\n", job.Name) close(job.Done) } // queue be sent jobs, which will be received within the ticker queue = make(chan Job, 50) // doClient sends and waits for a job doClient = func(name string, duration time.Duration) { job := Job{name, duration, make(chan struct{})} ts := time.Now() fmt.Printf("[client] job \"%s\" STARTED\n", job.Name) queue <- job <-job.Done fmt.Printf("[client] job \"%s\" FINISHED\n", job.Name) t := time.Now().Sub(ts) d := t - job.Duration if d < 0 { d *= -1 } if d > time.Millisecond*50 { panic(fmt.Errorf(`job "%s" expected %s actual %s`, job.Name, job.Duration.String(), t.String())) } } // running keeps track of the number of running jobs running = func() func(delta int64) int64 { var ( value int64 mutex sync.Mutex ) return func(delta int64) int64 { mutex.Lock() defer mutex.Unlock() value += delta return value } }() // done will be closed when it's time to exit the ticker done = make(chan struct{}) ticker = NewTickerStopOnFailure( context.Background(), time.Millisecond, New( Sequence, New(func(children []Node) (Status, error) { select { case <-done: return Failure, nil default: return Success, nil } }), func() Node { // the tick is initialised once, and is stateful (though the tick it's wrapping isn't) tick := Background(func() Tick { return Selector }) return func() (Tick, []Node) { // this block will be refreshed each time that a new job is started var ( job Job ) return tick, []Node{ New( Sequence, Sync([]Node{ New(func(children []Node) (Status, error) { select { case job = <-queue: running(1) return Success, nil default: return Failure, nil } }), New(Async(func(children []Node) (Status, error) { defer running(-1) doWorker(job) return Success, nil })), })..., ), // no job available - success New(func(children []Node) (Status, error) { return Success, nil }), } } }(), ), ) wg sync.WaitGroup ) wg.Add(1) run := func(name string, duration time.Duration) { wg.Add(1) defer wg.Done() doClient(name, duration) } fmt.Printf("running jobs: %d\n", running(0)) go run(`1. 120ms`, time.Millisecond*120) time.Sleep(time.Millisecond * 25) go run(`2. 70ms`, time.Millisecond*70) time.Sleep(time.Millisecond * 25) fmt.Printf("running jobs: %d\n", running(0)) doClient(`3. 150ms`, time.Millisecond*150) time.Sleep(time.Millisecond * 50) fmt.Printf("running jobs: %d\n", running(0)) time.Sleep(time.Millisecond * 50) wg.Done() wg.Wait() close(done) <-ticker.Done() if err := ticker.Err(); err != nil { panic(err) }
Output: running jobs: 0 [client] job "1. 120ms" STARTED [worker] job "1. 120ms" STARTED [client] job "2. 70ms" STARTED [worker] job "2. 70ms" STARTED running jobs: 2 [client] job "3. 150ms" STARTED [worker] job "3. 150ms" STARTED [worker] job "2. 70ms" FINISHED [client] job "2. 70ms" FINISHED [worker] job "1. 120ms" FINISHED [client] job "1. 120ms" FINISHED [worker] job "3. 150ms" FINISHED [client] job "3. 150ms" FINISHED running jobs: 0
Example (Success) ¶
defer func() func() { start := runtime.NumGoroutine() return func() { end := runtime.NumGoroutine() if start < end { panic(fmt.Errorf("num. goroutines %v start < end %v", start, end)) } } }()() node := func() Node { tick := Background(Fork) return func() (Tick, []Node) { return tick, []Node{ New(func(children []Node) (Status, error) { fmt.Println(`start fork`) return Success, nil }), New(Async(func(children []Node) (Status, error) { time.Sleep(time.Millisecond * 100) return Success, nil })), New(Async(func(children []Node) (Status, error) { time.Sleep(time.Millisecond * 200) return Success, nil })), New(Async(func(children []Node) (Status, error) { time.Sleep(time.Millisecond * 300) fmt.Println(`end fork`) return Success, nil })), } } }() fmt.Println(node.Tick()) time.Sleep(time.Millisecond * 50) fmt.Println(node.Tick()) time.Sleep(time.Millisecond * 150) fmt.Println(node.Tick()) time.Sleep(time.Millisecond * 200) fmt.Println(node.Tick()) // will receive the first tick's status time.Sleep(time.Millisecond * 50) fmt.Println(node.Tick()) time.Sleep(time.Millisecond * 100) fmt.Println(node.Tick()) fmt.Println(node.Tick()) fmt.Println(node.Tick()) time.Sleep(time.Millisecond * 450) fmt.Println(node.Tick()) fmt.Println(node.Tick())
Output: start fork running <nil> start fork running <nil> start fork running <nil> end fork end fork success <nil> success <nil> end fork success <nil> start fork running <nil> start fork running <nil> end fork end fork success <nil> success <nil>
func Fork ¶ added in v1.1.0
func Fork() Tick
Fork generates a stateful Tick which will tick all children at once, returning after all children return a result, returning running if any children did so, and ticking only those which returned running in subsequent calls, until all children have returned a non-running status, combining any errors, and returning success if there were no failures or errors (otherwise failure), repeating this cycle for subsequent ticks
func Not ¶ added in v1.1.0
Not inverts a Tick, such that any failure cases will be success and success cases will be failure, note that any error or invalid status will still result in a failure
func RateLimit ¶ added in v1.1.0
RateLimit generates a stateful Tick that will return success at most once per a given duration
func Shuffle ¶ added in v1.3.0
Shuffle implements randomised child execution order via encapsulation, using the provided source to shuffle the children prior to passing through to the provided tick (a nil source will use global math/rand), note that this function will return nil if a nil tick is provided
Example ¶
rand.Seed(1231244) var ( newPrintlnFn = func(fn func() []interface{}) Tick { return func([]Node) (Status, error) { fmt.Println(fn()...) return Success, nil } } newPrintln = func(v ...interface{}) Tick { return newPrintlnFn(func() []interface{} { return v }) } done bool ticker = NewTickerStopOnFailure(context.Background(), time.Millisecond, New( Sequence, New(newPrintlnFn(func() func() []interface{} { var i int return func() []interface{} { i++ return []interface{}{`tick number`, i} } }())), New( Shuffle(Sequence, nil), New(newPrintln(`node 1`)), New(newPrintln(`node 2`)), New( Selector, New(func() func(children []Node) (Status, error) { remaining := 5 return func(children []Node) (Status, error) { if remaining > 0 { remaining-- return Success, nil } return Failure, nil } }()), New( Shuffle(Selector, nil), New(newPrintln(`node 3`)), New(newPrintln(`node 4`)), New(newPrintln(`node 5`)), New(newPrintln(`node 6`)), New(func([]Node) (Status, error) { done = true return Success, nil }), ), ), ), New(func([]Node) (Status, error) { if done { return Failure, nil } return Success, nil }), )) ) <-ticker.Done() if err := ticker.Err(); err != nil { panic(err) }
Output: tick number 1 node 1 node 2 tick number 2 node 2 node 1 tick number 3 node 1 node 2 tick number 4 node 2 node 1 tick number 5 node 2 node 1 tick number 6 node 1 node 2 node 5 tick number 7 node 6 node 1 node 2 tick number 8 node 2 node 5 node 1 tick number 9 node 3 node 2 node 1 tick number 10 node 2 node 1
type Ticker ¶
type Ticker interface { // Done will close when the ticker is fully stopped. Done() <-chan struct{} // Err will return any error that occurs. Err() error // Stop shutdown the ticker asynchronously. Stop() }
Ticker models a node runner
func NewTicker ¶
NewTicker constructs a new Ticker, which simply uses time.Ticker to tick the provided node periodically, note that a panic will occur if ctx is nil, duration is <= 0, or node is nil.
The node will tick until the first error or Ticker.Stop is called, or context is canceled, after which any error will be made available via Ticker.Err, before closure of the done channel, indicating that all resources have been freed, and any error is available.
func NewTickerStopOnFailure ¶
NewTickerStopOnFailure returns a new Ticker that will exit on the first Failure, but won't return a non-nil Err UNLESS there was an actual error returned, it's built on top of the same core implementation provided by NewTicker, and uses that function directly, note that it will panic if the node is nil, the panic cases for NewTicker also apply.
Example (Counter) ¶
ExampleNewTickerStopOnFailure_counter demonstrates the use of NewTickerStopOnFailure to implement more complex "run to completion" behavior using the simple modular building blocks provided by this package
var ( // counter is the shared state used by this example counter = 0 // printCounter returns a node that will print the counter prefixed with the given name then succeed printCounter = func(name string) Node { return New( func(children []Node) (Status, error) { fmt.Printf("%s: %d\n", name, counter) return Success, nil }, ) } // incrementCounter is a node that will increment counter then succeed incrementCounter = New( func(children []Node) (Status, error) { counter++ return Success, nil }, ) // ticker is what actually runs this example and will tick the behavior tree defined by a given node at a given // rate and will stop after the first failed tick or error or context cancel ticker = NewTickerStopOnFailure( context.Background(), time.Millisecond, New( Selector, // runs each child sequentially until one succeeds (success) or all fail (failure) New( Sequence, // runs each child in order until one fails (failure) or they all succeed (success) New( func(children []Node) (Status, error) { // succeeds while counter is less than 10 if counter < 10 { return Success, nil } return Failure, nil }, ), incrementCounter, printCounter("< 10"), ), New( Sequence, New( func(children []Node) (Status, error) { // succeeds while counter is less than 20 if counter < 20 { return Success, nil } return Failure, nil }, ), incrementCounter, printCounter("< 20"), ), ), // if both children failed (counter is >= 20) the root node will also fail ) ) // waits until ticker stops, which will be on the first failure of it's root node <-ticker.Done() // every Tick may return an error which would automatically cause a failure and propagation of the error if err := ticker.Err(); err != nil { panic(err) }
Output: < 10: 1 < 10: 2 < 10: 3 < 10: 4 < 10: 5 < 10: 6 < 10: 7 < 10: 8 < 10: 9 < 10: 10 < 20: 11 < 20: 12 < 20: 13 < 20: 14 < 20: 15 < 20: 16 < 20: 17 < 20: 18 < 20: 19 < 20: 20