Simple to use yet powerful behaviour tree system for your games. The behaviour tree system itself is LÖVE independant, only the tree_render.lua uses LÖVE stuff.
You can check it for yourself running this project, the main.lua
file contains two examples and a simple camera to pan and zoom.
Just copy LOVElyTree
into your project. It can be anywhere, it uses the module operator internally so it doesn't depend of being in the root.
To create a behaviour tree first require the lib
local LT = require("LOVELyTree") ---@module "LOVElyTree"
Then just create an instance passing a root node and an optional table that works as blackboard.
local shared_blackboard = {}
local branch1 = LT.Compositors.Sequence({
LT.Actions.Wait(2),
LT.Decorators.Invert(LT.Actions.Wait(2))
})
local branch2 = LT.Compositors.Sequence({
LT.Compositors.RandomOnce({
LT.Actions.Wait(2),
LT.Decorators.AlwaysFail(LT.Actions.Wait(2))
}),
})
local root = LT.Compositors.Selector({ branch1, branch2 })
local tree = LT.BehaviorTree(root, shared_blackboard)
In behavior trees, a blackboard is simply a shared object accessible by all nodes. It serves as a persistent storage for state information, maintaining values even across tree resets. It also helps decouple some information from the actions themselves and further modularize the system.
There are 3 types of nodes: compositors, decorators and actions. I will go about each one now:
Compositors arrange multiple child nodes and determine their combined execution flow. They are used to implement control-flow logic based on the results of one or more children.
-
Sequence
Executes its child nodes in order. The Sequence returns FAILURE as soon as one child fails and only returns SUCCESS when all children have succeeded. -
Selector
Checks each child in order and returns SUCCESS if any child succeeds. It fails only when every child returns FAILURE. -
RandomOnce
Selects one child at random and executes it. The chosen child’s result becomes the result of the node. -
ParallelSequence
Executes all children in parallel. If any child fails immediately, the node fails; it only succeeds when all children have succeeded. -
ParallelSelector
Executes all children in parallel and returns SUCCESS if any child succeeds immediately. It only fails when all children have failed.
Basically parallel nodes are the same as their non parallel counterparts but they might have more than one child running in the concurrently.
-
RandomSelector
Executes children in a random order and returns SUCCESS on the first child that succeeds; otherwise, it fails if all children fail. -
RandomSequence
Executes children in a random order and only returns SUCCESS if all children succeed.
Decorators are nodes that wrap a single child node to modify its behavior or result. They can alter outcomes such as success/failure, add retry logic, impose execution limits, or delay execution.
-
Repeat
Repeats execution of its child node a fixed number of times until the count is reached. If the child fails at any repetition, the counter is reset. -
Invert
Inverts the child node's result so that SUCCESS becomes FAILURE and FAILURE becomes SUCCESS, while RUNNING remains unchanged. -
Retry
Retries executing its child node when it fails, up to a specified maximum number of attempts. -
AlwaysSuccess
Forces the result of its child to be SUCCESS, unless the child is still running. -
AlwaysFail
Forces the result of its child to be FAILURE, unless the child is running. -
TimeLimit
Limits the total execution time for the child node. If the cumulative update time exceeds a specified limit, the node returns FAILURE. -
ExecutionLimit
Restricts the number of times the child node can be executed over multiple updates. -
UntilFail
Repeatedly executes the child node until it fails. If the child returns SUCCESS, it is immediately reset and executed again. -
UntilSuccess
Repeatedly executes the child node until it succeeds. If the child returns FAILURE, it is reset and executed again.
Actions are the leaves of the tree that perform specific tasks and interact with the blackboard.
-
Wait
Waits a fixed amount of time before returning SUCCESS. -
RandomWait
Waits for a random duration between a minimum and maximum value. -
SetBlackboard
Sets a specific key-value pair in the blackboard. -
CheckBlackboard
Checks if a blackboard key holds an expected value, returning SUCCESS or FAILURE.
There are two ways to define custom actions.
--- Define action, this is obligatory
function CustomWaitAction(duration)
local action = {}
action.timer = 0
action.name = "WaitAction (" .. duration .. "s)"
function action:update(dt)
self.timer = self.timer + dt
return (self.timer >= duration) and LT.RESULT.SUCCESS or LT.RESULT.RUNNING
end
--- This method is optional.
--- When it doesn't exist, the internal action will just call the function again
--- To generate a new clean state. If you need a custom cleanup of the node or need more performance
--- Just define this function. Returning false means that the object won't be recreated
function action:reset()
self.timer = 0
return false
end
return action
end
--- Optional wrapped layer
local WaitActionWrapped = LT.WrapAction(CustomWaitAction)
How the action is used is different depending on if you wrap it or not.
local branch1 = LT.Compositors.Sequence({
WaitActionWrapped(1.5),
LT.Action(CustomWaitAction, 2),
})
You can see here that the wrapped action can be called directly as a the function it is wrapping, while the other way needs the use lf LT.Action that internally will create the wrapped action. Both work the same, it just depends on which way of writing you prefer.
You might be wondering... Why do I need to wrap actions? Well, internally, the reset of a node sets its state to idle
, and I don't want the user to code that for each node as it's error prone. And what's more, you don't even need to write a reset function, the wrapper will just call the inner function to generate a new object with fresh data. Of course, writing a custom reset function that returns false is the most performant solution, but you have to make sure to reset all the state correctly or you'll have a hard time trying to find out why some behaviour is weird or wrong.
graph TD
A[Start] --> B[Execute first child]
B --> C{Child result?}
C -- SUCCESS --> D[More children?]
C -- RUNNING --> E[Wait for result]
C -- FAILURE --> F[Return FAILURE immediately]
D -- Yes --> B
D -- No --> G[Return SUCCESS]
graph TD
A[Start] --> B[Execute first child]
B --> C{Child result?}
C -- SUCCESS --> D[Return SUCCESS immediately]
C -- FAILURE --> E[More children?]
C -- RUNNING --> F[Wait for result]
E -- Yes --> G[Execute next child]
E -- No --> H[Return FAILURE]
graph TD
A[Start] --> B[Select a random child]
B --> C[Execute selected child]
C --> D{Child result?}
D -- SUCCESS --> E[Return SUCCESS]
D -- FAILURE --> F[Return FAILURE]
D -- RUNNING --> G[Wait for result]
graph TD
A[Start Parallel Sequence] --> B[Execute all children simultaneously]
B --> C{Any child fails?}
C -- Yes --> D[Return FAILURE immediately]
C -- No --> E{All children succeeded?}
E -- Yes --> F[Return SUCCESS]
E -- No --> G[Wait for remaining children]
graph TD
A[Start Parallel Selector] --> B[Execute all children simultaneously]
B --> C{Any child succeeds?}
C -- Yes --> D[Return SUCCESS immediately]
C -- No --> E{All children finished?}
E -- Yes --> F[Return FAILURE]
E -- No --> G[Wait for remaining children]
graph TD
A[Start Random Selector] --> B[Shuffle children order]
B --> C[Execute children sequentially]
C --> D{Child result?}
D -- SUCCESS --> E[Return SUCCESS immediately]
D -- FAILURE --> F[Check next child]
F --> G{More children?}
G -- Yes --> C
G -- No --> H[Return FAILURE]
graph TD
A[Start Random Sequence] --> B[Shuffle children order]
B --> C[Execute children sequentially]
C --> D{Child result?}
D -- SUCCESS --> E[More children?]
D -- FAILURE --> F[Return FAILURE immediately]
E -- Yes --> C
E -- No --> G[Return SUCCESS]
graph TD
A[Start Invert Decorator] --> B[Execute child node]
B --> C{Child returns?}
C -- SUCCESS --> D[Return FAILURE]
C -- FAILURE --> E[Return SUCCESS]
C -- RUNNING --> F[Return RUNNING]
graph TD
A[Start Repeat Decorator] --> B[Execute child node]
B --> C{Child returns?}
C -- SUCCESS --> D[Increment count]
C -- FAILURE --> E[Reset count; Return FAILURE]
D --> F{Repeat limit reached?}
F -- Yes --> G[Return SUCCESS]
F -- No --> B
graph TD
A[Start Retry Decorator] --> B[Execute child node]
B --> C{Child returns?}
C -- SUCCESS --> D[Return SUCCESS]
C -- FAILURE --> E[Increment retry count]
E --> F{Retries left?}
F -- Yes --> B
F -- No --> G[Return FAILURE]
graph TD
A[Start AlwaysSuccess Decorator] --> B[Execute child node]
B --> C{Child returns?}
C -- SUCCESS --> D[Return SUCCESS]
C -- FAILURE --> E[Return SUCCESS]
C -- RUNNING --> F[Return RUNNING]
graph TD
A[Start AlwaysFail Decorator] --> B[Execute child node]
B --> C{Child returns?}
C -- SUCCESS --> D[Return FAILURE]
C -- FAILURE --> E[Return FAILURE]
C -- RUNNING --> F[Return RUNNING]
graph TD
A[Start ExecutionLimit Decorator] --> B[Execute child node]
B --> C{Is child running?}
C -- No --> D[Return child's result]
C -- Yes --> E[Increment execution count]
E --> F{Limit reached?}
F -- Yes --> G[Return FAILURE]
F -- No --> H[Allow next execution]
graph TD
A[Start UntilFail Decorator] --> B[Execute child node]
B --> C{Child returns?}
C -- FAILURE --> D[Return FAILURE]
C -- SUCCESS --> E[Reset child and retry]
C -- RUNNING --> F[Return RUNNING]
E --> B
graph TD
A[Start UntilSuccess Decorator] --> B[Execute child node]
B --> C{Child returns?}
C -- SUCCESS --> D[Return SUCCESS]
C -- FAILURE --> E[Reset child and retry]
C -- RUNNING --> F[Return RUNNING]
E --> B
graph TD
A[Start Wait Action] --> B[Check timer]
B --> C{Timer < Wait Time?}
C -- Yes --> D[Increment timer, return RUNNING]
C -- No --> E[Return SUCCESS]
graph TD
A[Start RandomWait Action] --> B[Generate random wait time]
B --> C[Check timer]
C --> D{Timer < Wait Time?}
D -- Yes --> E[Increment timer, return RUNNING]
D -- No --> F[Return SUCCESS]
graph TD
A[Start SetBlackboard Action] --> B[Assign value to blackboard key]
B --> C[Return SUCCESS]
graph TD
A[Start CheckBlackboard Action] --> B[Retrieve value from blackboard]
B --> C{Value equals expected?}
C -- Yes --> D[Return SUCCESS]
C -- No --> E[Return FAILURE]