Skip to content

Nrosa01/LOVElyTrees

Repository files navigation

LÖVEly Trees

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.

Show, don't tell

You can check it for yourself running this project, the main.lua file contains two examples and a simple camera to pan and zoom.

Installation

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.

The tree

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)

The 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.

Nodes

There are 3 types of nodes: compositors, decorators and actions. I will go about each one now:

Compositors

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.

Available Compositors

  • 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

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.

Available Decorators

  • 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

Actions are the leaves of the tree that perform specific tasks and interact with the blackboard.

Available Actions

  • 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.

Custom actions

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.

Nodes visual explanation

Compositors Visual Explanations

Sequence

Loading
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]

Selector

Loading
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]

RandomOnce

Loading
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]

ParallelSequence

Loading
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]

ParallelSelector

Loading
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]

RandomSelector

Loading
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]

RandomSequence

Loading
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]

Decorators Visual Explanations

Invert

Loading
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]

Repeat

Loading
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

Retry

Loading
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]

AlwaysSuccess

Loading
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]

AlwaysFail

Loading
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]

TimeLimit

Loading
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]

UntilFail

Loading
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

UntilSuccess

Loading
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

Actions Visual Explanations

Wait

Loading
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]

RandomWait

Loading
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]

SetBlackboard

Loading
graph TD
    A[Start SetBlackboard Action] --> B[Assign value to blackboard key]
    B --> C[Return SUCCESS]

CheckBlackboard

Loading
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]

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages