behaviortree

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 12, 2019 License: Apache-2.0 Imports: 6 Imported by: 0

README

go-behaviortree

Package behaviortree provides a simple and powerful Go implementation of behavior trees without fluff.

Go doc: https://godoc.org/github.com/joeycumines/go-behaviortree

Wikipedia: Behavior tree - AI, robotics, and control

go test: 100% coverage | go vet: pass | golint: pass

type (
	// Node represents an node in a tree, that can be ticked
	Node func() (Tick, []Node)

	// Tick represents the logic for a node, which may or may not be stateful
	Tick func(children []Node) (Status, error)

	// Status is a type with three valid values, Running, Success, and Failure, the three possible states for BTs
	Status int
)

// Tick runs the node's tick function with it's children
func (n Node) Tick() (Status, error)

Features

  • Core implementation as above
  • Sequence and Selector also provided as per the Wikipedia page
  • Async and Sync wrappers allow for the definition of time consuming logic that gets performed in serial, but without blocking the tick operation, and can be used to implement complex behavior such as conditional exit of running nodes
  • Implementations to actually run behavior trees are provided, and include a Ticker and Manager, but both are defined by interfaces and are entirely optional
  • I use this implementation for several personal projects and will continue to add functionality after validating it's overall value (NewTickerStopOnFailure used in the example below was added in this way)

For the specific use case I built it for I have yet to find anything remotely comparable. It's also very unlikely this will ever see a V2, but quite likely that I will be supporting V1 for years.

Example Usage

The example below is straight from example_test.go.

TODO: more complicated example using Async and Sync

// 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
func ExampleNewTickerStopOnFailure_counter() {
	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 single root node at
		// most once per millisecond 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
}

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

type Node func() (Tick, []Node)

Node represents an node in a tree, that can be ticked

func New

func New(tick Tick, children ...Node) Node

New constructs a new behavior tree aliasing NewNode with vararg support for less indentation

func NewNode

func NewNode(tick Tick, children []Node) Node

NewNode constructs a new node out of a tick and children

func Sync

func Sync(nodes []Node) []Node

Sync will wrap a set of nodes in such a way that their real ticks will only be triggered when either the node being ticked was previously running, or no other nodes are running.

func (Node) Tick

func (n Node) Tick() (Status, error)

Tick runs the node's tick function with it's children

type Status

type Status int

Status is a type with three valid values, Running, Success, and Failure, the three possible states for BTs

const (

	// Running indicates that the Tick for a given Node is currently running
	Running Status = iota
	// Success indicates that the Tick for a given Node completed successfully
	Success
	// Failure indicates that the Tick for a given Node failed to complete successfully
	Failure
)

func Selector

func Selector(children []Node) (Status, error)

Selector is a tick implementation that will succeed if no children fail, returning running if any children return running, propagating any error

func Sequence

func Sequence(children []Node) (Status, error)

Sequence is a tick implementation that will succeed if any children succeed, returning running if any children return running, propagating any error

func (Status) Status

func (s Status) Status() Status

Status returns the status value, but defaults to failure on out of bounds

func (Status) String

func (s Status) String() string

String returns a string representation of the status

type Tick

type Tick func(children []Node) (Status, error)

Tick represents the logic for a node, which may or may not be stateful

func Async

func Async(tick Tick) Tick

Async wraps a tick so that it runs asynchronously, note nil ticks will return nil

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

func NewTicker(ctx context.Context, duration time.Duration, node Node) Ticker

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

func NewTickerStopOnFailure(ctx context.Context, duration time.Duration, node Node) Ticker

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL