Behaviour tree implementation, fully built with Kotlin's type-safe builders, which allow for creating Kotlin-based domain-specific languages (DSLs). Perfect for implementing systematic AI for games, robotics, logical procedures, etc.
The main idea behind Behavior Trees is to break down the decision-making process into a tree-like structure of nodes, each representing a specific behavior or action. For more information, please refer to the following references:
Behaviour tree implementations can vary between different libraries and contexts. The nodes and features currently supported are:
- Composites
- Sequence
- Selector
- Decorators
- Inverter
- Succeeder
- RepeatUntil
- RepeatWhen
- Gate
- Actions
- Perform
- Condition
Due to the behaviour tree's DSL, it is a breeze to implement and reuse behaviour trees. It is possible to create a tree from any node, be it a composite, decorator, or action node.
val helloWorld = sequence {
+perform { print("Hello, ") }
+perform { print("world") }
}
val root = sequence {
+helloWorld
}
Notice the +
operator, this is a mandatory operator to add children to the composite node, it supports any tree node
and therefore also
subtrees as shown in the example above. This operator is not necessary for the decorator and action nodes.
Executing the tree is as easy as calling the execute
function, which returns a result for the entirety of the tree
call stack.
val result = root.execute()
// Output: Hello, world
The outcome comprises the status recorded at each individual node present in the call stack during that particular execution.
private fun TreeNodeResult.print(indent: Int = 0) {
println("${"\t".repeat(indent)}> ${parent.javaClass.simpleName} :${parent.name} ${status.name}")
// Append children if they exist
children?.forEach {
it.print(indent + 1)
}
}
result.print()
// Output:
// > Sequence : SUCCESS
// > Sequence : SUCCESS
// > Perform : SUCCESS
// > Perform : SUCCESS
These nodes represent actual actions that the agent can perform. These could be things like moving, attacking, or interacting with objects.
val example = action {
println("Hello, I will return a success status")
Status.SUCCESS
}
In addition to the generic action nodes, there are also helpers such as the condition action node.
It is responsible for turning a boolean result into a status, true
being successful and false
being failure.
fun Door.isReachable(): Boolean {
// Check if the door is reachable, pathfinding etc.
return true
}
val example = condition {
Door.isReachable()
}
Furthermore, if you just want to perform a simple action without any logical statusses, you can opt for the perform action node.
val example = perform {
println("Hello, I will always succeed!")
}
Sequences execute their child nodes in order until one fails or all succeed, similar to an AND
operator.
val example = sequence {
+condition {
println("Hello, ")
false
}
+perform { println("world") }
}
// Output: Hello,
The first condition returns a failure, thus the sequence exits early.
Selectors execute their child nodes in order until one succeeds or all fail, similar to an OR
operator.
val example = selector {
+condition {
print("Hello, ")
false
}
+perform { print("world") }
+perform { print("example") }
}
// Output: Hello, world
The first condition returns a failure, thus the selector continues. The second node will succeed, thus selector exits early and the third node is not be executed.
Decorator nodes modify the behaviour of their child node in some way.
Inverts the result of its child node (success becomes failure, and vice versa).
val example = sequence {
+inverter {
condition {
println("Hello, ")
false
}
}
+perform { println("world") }
}
// Output: Hello, world
Using the sequence example and applying the inverter on the node that previously failed, we can alter its outcome to achieve a success status. As a result, the second node within the sequence will also be executed.
Repeats the execution of its child node a certain number of times until the specified status is met.
fun Door.isReachable(): Boolean {
// Check if the door is reachable, pathfinding etc.
return false
}
val example = repeatUntil(Status.SUCCESS) {
selector {
+condition { Door.isReachable() }
+condition {
//Solve parkour, or something else
Door.isReachable()
}
}
}
In this theoretical scenario, the agent will engage in a continuous loop, repeating its actions until the door becomes reachable. If the door is initially not within reach, the agent will attempt to either navigate through a parkour challenge or perform other relevant actions to enable access to the door. This looping behavior will persist indefinitely until the objective of making the door reachable is achieved.
Repeats the execution of its child node a certain number of times given the condition holds true.
fun needsHealing(): Boolean {
// Check if an actor needs healing
return false
}
val example = repeatWhen({ needsHealing() }) {
perform {
// Some healing action
}
}
In this theoretical scenario, the agent will engage in a continuous loop, repeating its actions given that the actor healing. If the actor does the healing action, and is still in need of healing, it will repeat it until it no longer needs healing.
Similar to the perform
action node, the succeeder will turn the status of any child into a success status.
val example = sequence {
+succeeder {
condition {
println("Hello, ")
false
}
}
+perform { println("world") }
}
// Output: Hello, world
Regardless of what status the condition returns, it will always return a success status and therefore finish the entire sequence.
Executes child only when condition is met, return status of the child if executed, otherwise failure.
val example = gate(validate = { true }) {
perform {
println("Hello, world")
}
}
// Status: Success (child return status),
// Output: Hello, world
val example = gate(validate = { false }) {
perform {
println("Hello, world")
}
}
// Status: Failure
// Output:
This project is licensed under the terms of GPL v3. See LICENSE for details.