TreeAI - Easily create Behavior Trees for your NPCs

TreeAI

A Behavior Tree Implementation to create NPC AIs

TreeAI_Debug
TreeAI Editor Plugin | TreeAI Creator Module | Demo | Demo (File)


TreeAI is both a Module and a Plugin used to create any kind of AI following the Behavior Tree pattern. This system uses OOP, so if you are not familiar with what OOP is or what Behavior Trees are, I recommend you take a look at these two videos:


TreeAI, as said before, is composed of two parts: The TreeAI Editor Plugin and the TreeAICreator Module.
The TreeAI Editor Plugin is used to simplify the creation of Behavior Trees. It allows you to create trees by connecting tasks together. It makes creating Custom Tasks easier, automatically creating the Custom Task Class and the configurations with the click of a button. It contains documentations for each built-in task and instructions on how to use the system. Finally, it allows you to visually debug the tree.

The TreeAICreator Module is the core of the system. It is used to create the actual trees that will be used by your NPCs.

Note that the TreeAI Editor Plugin is not required to create Behavior Trees. Go to the usage section to see how you can create a tree without the plugin.


Features

  • Tree creation from scratch or with the TreeAI Editor Plugin
  • Easily build and visualize trees and actions
  • In-plugin documentation
  • Simple Custom Task creation to re-use tasks in your trees
  • Running Trees Debugger
  • Keybinds and tasks color customization in settings
  • One in a million chance for the plugin icon to become fih


Installation

For the complete system, install the Plugin. Once installed the plugin will prompt you to import the TreeAICreator Module.
If you don’t want to use the Editor Plugin, download only the TreeAI Creator Module.


Usage

Before you start, you need to understand how tasks work.
Each task is a class and there are three types of Task Classes: Composite, Decorator and Action Tasks.
The Composite Tasks specify the execution order of their children and can have multiple children.
Decorator Tasks alter the result of their child or change how it is processed. They must have one child.
Action Tasks are the one who perform the logic of the AI and cannot have any children.

Every task must return one of three results SUCCESS, FAILURE or RUNNING.
SUCCESS and FAILURE indicate the outcome of a task, while RUNNING is returned when a task requires more than one frame to complete its work.

An in-depth how to use and documentations for every task is available in the Editor Plugin.

To start creating your Behavior Tree, first create a script and require the TreeAI Creator Module.
A tree requires an Object and a Blackboard to work. Both the object and the blackboard are tables that contain data that will be used to interact with the Behavior Tree.
The blackboard should contain data that is used by the tree to perform checks, for example how much stamina the NPC has, or where it last see the player, etc.
The object should contain data that describes the NPC, if your NPC is created trough an NPC class, the object should be instance returned by the NPC class’ constructor. The data in the object can be the NPC’s model, the path created from the PathFindingService, and the actions that the NPCs can perform, like attack, find cover, etc.

You should have something like this:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PathfindingService = game:GetService("PathfindingService")

local TreeAICreator = require(ReplicatedStorage.Packages.TreeAICreator)

local model = script.Parent
local humanoid = model:FindFirstChildWhichIsA("Humanoid")
local path = PathfindingService:CreatePath()

local object = {
    model = model,
    humanoid  = humanoid 
    path = path,
    
    shootAtTarget = function(object, blackboard)
        -- shoot code
    end,
    
    findCover = function(object, blackboard)
        -- find cover code
    end,
}

local blackboard = {
    target = nil,
    ammo = 30
}

The next step is to create the Behavior Tree. You can do it in two ways

With the Editor Plugin

Select an instance in your project and click the Create new Tree button at the top-left corner of the Editor, a Configuration instance will be created under the previously selected instance. You can switch between trees simply by selecting the Configuration instances. If you want to unload a tree, go to settings and click Unload Current Tree.

Once you are happy with your tree, go back to the previous script.
Now you need to create the tree with the tree configuration made with the Editor Plugin

To do so, use the fromConfiguration function of the TreeAI Creator Module and pass the configuration instance, the blackboard and the object.

local tree = TreeAICreator.fromConfiguration(ServerScriptService.Trees.NPCTree, blackboard, object)

The last argument, debugEnabled, is true by default and it allows you to visually debug the tree in runtime. debugEnabled will be set to false automatically in a public game so that it doesn’t consume additional resources. But it is recommend that you set it to false if your project has a lot of NPCs with debugEnabled set to true. Or if you are sure that there is nothing wrong with the tree.

From scratch

If you don’t want to use the Editor Plugin, creating trees manually might be a bit more challenging. Additionally trees created from scratch can not be debugged using the Editor’s Debug system.

First create an empty tree with the TreeAI Creator’s new function.
Then you need to create each and every task you want your tree to have using TreeAICreator.Tasks.. Every task is listed here and calling the method returns a new instance of that task.

Once tasks are created you need to attach one task to the tree’s root. To do it call the tree’s method addChildToRoot and pass the task you want to connect.
To connect tasks to other tasks use the method addChild on the parent task, and pass the child task.

local forceFailure = TreeAICreator.Tasks.ForceFailure("Failure")

local print1 = TreeAICreator.Tasks.Print("Print1", "Value 1")
forceFailure:addChild(print1)
local print2 = TreeAICreator.Tasks.Print("Print2", "Value 2")

local selector = TreeAICreator.Tasks.Selector("Selector")
selector:addChild(forceFailure)
selector:addChild(print2)
local print3 = TreeAICreator.Tasks.Print("Print3", "Value 3")

local sequence = TreeAICreator.Tasks.Sequence("Sequence")
sequence:addChild(selector)
sequence:addChild(print3)

local tree = TreeAICreator.new(blackboard, object)
tree:addChildToRoot(sequence)

Another way you can create a tree is to create the tasks inside the addChild method:

local tree = TreeAICreator.new(blackboard, object)
tree:addChildToRoot(
    TreeAICreator.Tasks.Sequence("Sequence")
        :addChild(TreeAICreator.Tasks.Selector("Selector")
            :addChild(TreeAICreator.Tasks.ForceFailure("Failure")
                :addChild(TreeAICreator.Tasks.Print("Print1", "Value 1"))
            )
            :addChild(TreeAICreator.Tasks.Print("Print2", "Value 2"))
        )
        :addChild(TreeAICreator.Tasks.Print("Print3", "Value 3"))
)

Here you can better visualize the tree. An indented task means that it is a child of the previous one.

Finally you need to process the tree. You can use the method start to bind the tree’s process method to the RunService Heartbeat event. This will process the tree every frame.
If you don’t want to process the tree every frame, you can manually call the process method and pass the elapsed time since the last process call.

This will process the tree at 30 fps:

local processRate = 1 / 30
local delta = 0
RunService.Heartbeat:Connect(function(dt: number)
    delta += dt
    if delta < processRate then return end

    tree:process(delta)
    delta = 0
end)

And this will process the tree every 0.5 seconds:

while true do
    local delta = task.wait(.5)
    tree:process(delta)
end

Custom Tasks

Custom tasks are used to re-use functionality across multiple trees. To create a custom task, click the button at the top-right corner of the Editor Plugin, select a location where you want your task to be, and select what kind of task you want to create. Most Custom Tasks are Action tasks, but you can create any type of task you need.

Without the Editor Plugin

If you are not using the Editor Plugin, you can create a custom task by “inheriting” one of the three abstract classes in the TreeAI Creator Module. Inherit BaseTaskClass if you want to make an Action Task, CompositeTaskClass to make a Composite Task, and DecoratorTaskClass to make a Decorator Task.

To inherit a class follow the script below:

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local BaseTaskClass = require(ReplicatedStorage.Packages.TreeAICreator.Tasks.AbstractClasses.BaseTaskClass)

local CustomTask = {}
CustomTask.__index = setmetatable(CustomTask, BaseTaskClass)

function CustomTask.new(name: string)
    local self = setmetatable(BaseTaskClass.new(name), CustomTask)

    return self
end

return CustomTask

Change the abstract class with the one you need.

Each task is required to have a process method like this:

function CustomTask.process(self, treeResult, deltaTime, blackboard, object, ...)
    return treeResult:resultRunning()
end

The parameters passed to the process method are:

  • treeResult: Used to return one of the three results SUCCESS, FAILURE or RUNNING. If debugEnabled is true, the results will be logged and displayed when debugging the tree
  • deltaTime: The time passed since the last process call
  • blackboard: The tree’s blackboard
  • object: The tree’s object
  • ...: Any additional variable you pass in the tree’s process method. Empty if you are processing the tree with the start method

In addition, custom Action Tasks should override the reset method

function CustomTask.reset(self)
    -- Reset code or empty if you don't need to reset the task
end

This is used if you need to clean attributes of the task.

If you use the Editor Plugin, you’ll most likely want to view your new Custom Tasks in the editor.
To do this you need to setup some configuration to visualize the tasks exactly how you want them.
TreeAI_CustomTasks
When you create a new task, other than the Custom Task class itself, a configuration module should have opened as well. If it didn’t open automatically, you can click on Highlight Custom Tasks Data in the editor’s settings. Or simply go to ServerStorage.CustomTasksData.
In there you need to write the configuration on how the task should look in the editor. This is how the three tasks above are configured:

return {
    {
        Name = "Shoot",
        Arguments = {
            {
                Name = "bulletType",
                Type = {"Fireball", "Missile", "ElectricBolt"}
            }
        }
    },

    {
        Name = "GetClosestTarget",
        Arguments = {
            {
                Name = "range",
                Type = "number"
            },
            {
                Name = "requireLineOfSight",
                Type = "boolean"
            }
        }
    },

    {
        Name = "Move"
    }
}

You can follow the custom task template at the bottom of the script to see all the different types of configurations you can set.

After a custom task is configured, you need to reload the Editor Plugin. To do so, click on Reload Custom Tasks and Editor in the Editor’s settings.


A Demo Project is available so feel free to check it out. It uses both OOP and normal scripting to create NPCs, tho it is written using a module loader / Single Script Architecture. The demo consists of 3 different AIs that work together.
The NoobPath module is used in the demo for the NPCs pathfinding.

If you find any issues or you want to leave suggestions such as adding new built-in tasks. Please let me know here or send me a private message. I’ll try to fix or update it in a reasonable amount of time.

39 Likes

This is super interesting, I’ll have to look at this in the future, I hate making AI and custom behavior trees but this plugin might be the solution!

1 Like

Hello. First of all, thank you for TreeAI!!

I have found a bug in the plugin when trying to debug a behavior tree.

Stacktrace :
cloud_133974536197155.TreeAIBehaviorTreeEditor.src.Tree.DebugManager:70: invalid argument #1 to ‘clear’ (table expected, got nil) - Servidor
Stack Begin - Studio
Script ‘cloud_133974536197155.TreeAIBehaviorTreeEditor.src.Tree.DebugManager’, Line 70 - function debugTree - Studio
Script ‘cloud_133974536197155.TreeAIBehaviorTreeEditor.src.EditorUI.TopBar.Debug.Debug’, Line 48 - Studio
Stack End - Studio

The print and wait work fine

local ReplicatedFirst = game:GetService("ReplicatedFirst")
local ServerStorage = game:GetService("ServerStorage")
local TreeAICreator = require(ReplicatedFirst:WaitForChild("libs"):WaitForChild("External"):WaitForChild("TreeAICreator"))

local npc1_configuration = ServerStorage:WaitForChild("Trees"):WaitForChild("npc_1")
local npc1_model = workspace:WaitForChild("NPC_1")
local npcSpawn = workspace:WaitForChild("NPC_SPAWN")

local self = {}
type ModuleObject = typeof(self)

function self._init()
   
    local model: Model = npc1_model:Clone()
    local object = { }
    local blackboard = { }
    local tree = TreeAICreator.fromConfiguration(npc1_configuration, blackboard, object, true)
    model:PivotTo(npcSpawn:GetPivot())
    model.Parent = workspace.NPCs
    tree:start()
end

return self

1 Like

Hey! Thanks for the report. Sorry I took a while to respond, I was pretty sick.
I pushed an update for the plugin. If you continue to find this issue please let me know.

2 Likes

For the trigger modules, will you add support for a start and end method? I think behaviortree3 has that.

No, TriggerModule is not supposed to contain big AI logic. It’s supposed to contain reusable checks, or small and quick actions. So I don’t think there’s a need for a start and finish method.

Hey, this is really nice! Do you think it’s possible to use this to create a combat-oriented npc tree? Like detect player>stalk>attack for example

Yeah totally. You can create all the logic with custom tasks and create any type of npc you want.
You can make a lamp turn on and off if you really want. Don’t know why you’d do that, but you definitely can.

As somebody who previously worked on Tyridge’s BTree module, it’s cool to see a newer module come out. Will def poke around with this later!

Does this support external tasks? E.g. Modules you can link to? I had them hacked into BTrees as kind of an extra added thing, but it’d be cool to see something like this natively supported.

1 Like

Thanks for checking this out.
Yes, you can add a TriggerModule task and link it to an external module to execute it. You can also use the TriggerMethod task to call a method inside of the object table.

local bindable: BindableEvent = Instance.new("BindableEvent")
local current_task: thread = nil

local function change_task(callback : Function)
	bindable.Event:Once(function()
		task.cancel(current_task) -- cancel runned task
		current_task = task.spawn(callback) -- starting a new task
	end)
end

--Example of logic construction--

function first_function()
	--code--
	change_task(second_function)
end

function second_function()
	--another code--
	change_task(first_function)
end

--It will work cyclically, and I can, for example, stop it from the outside at any time.

I used BindableEvent to stop the thread from inside while it was running. But many people told me not to do that. I decided to switch to Tyridge’s BehaviorTree. And I realized that I really miss the node “Listener”, which will wait for a signal, and if a signal is received, change the current task… I tried to make a flag that is checked during the process between nodes, but it does not respond immediately. What can you advise me???

If you want to change the processed task via bindable event I would do something like this:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")

local TreeAICreator = require(ReplicatedStorage.TreeAICreator)

local obj = {}
local blackboard = {
	processIndex = 1
}

local tree = TreeAICreator.fromConfiguration(ServerScriptService.Trees.BehaviorTree9, blackboard, obj, true)

local bindableEvent = Instance.new("BindableEvent")
bindableEvent.Event:Connect(function(processIndex: number)
	blackboard.processIndex = processIndex
	tree:process(0)
end)

bindableEvent:Fire(1)
bindableEvent:Fire(2)
bindableEvent:Fire(1)

The processIndex in the blackboard is used by the tree to select which actions to perform.


I’ve added CheckBlackboardValue tasks at the start of each sequence to check if the index is different from the one required. If it’s different, the process sequence will not be executed.

Output: 

process1_1
process1_2
	
process2_1
process2_2
	
process1_1
process1_2

In the bindable event I change the processIndex and I process the tree once.

This method works but only for very simple AIs. It’s not recommended since you basically lose the ability to use tasks that need more than one frame to process, like Repeat, Delay, Wait, etc.
You can work around this by doing some additional hacky stuff. But still, I don’t recommended this method.

This is really cool, reminds me of BTrees. Feel like this is a more modern adaptation of that whole thing. Where was you earlier last year :sob:

really annoying bug which results in you needing to delete the tree to fix (i think). No idea why it happens, happens when adding a new thing and does not matter what i believe.


After looking at the logs (I forgot) here is the error:

The bellow console error happens when trying to add a boolean action:


 12:19:00.153  cloud_133974536197155.TreeAIBehaviorTreeEditor.src.EditorUI.Tasks.TasksManager:139: Unable to create task Spawning: Unable to assign property Text. string expected, got boolean  -  Edit
  12:19:00.153  Stack Begin  -  Studio
  12:19:00.154  Script 'cloud_133974536197155.TreeAIBehaviorTreeEditor.src.EditorUI.Tasks.TasksManager', Line 139  -  Studio
  12:19:00.154  Script 'cloud_133974536197155.TreeAIBehaviorTreeEditor.Packages.SignalPlus', Line 108 - function reusableThreadCall  -  Studio
  12:19:00.154  Script 'cloud_133974536197155.TreeAIBehaviorTreeEditor.Packages.SignalPlus', Line 113 - function reusableThread  -  Studio
  12:19:00.154  Stack End  -  Studio
  12:19:04.136  Unable to record history
cloud_133974536197155.TreeAIBehaviorTreeEditor.src.Utility.HistoryHandler:13 function StartRecording
cloud_133974536197155.TreeAIBehaviorTreeEditor.src.EditorUI.Tasks.TasksManager:176 function createTaskFromMouse
cloud_133974536197155.TreeAIBehaviorTreeEditor.src.EditorUI.Tasks.TasksManager:138
cloud_133974536197155.TreeAIBehaviorTreeEditor.Packages.SignalPlus:108 function reusableThreadCall
cloud_133974536197155.TreeAIBehaviorTreeEditor.Packages.SignalPlus:113 function reusableThread
  -  Edit
  12:19:04.136  cloud_133974536197155.TreeAIBehaviorTreeEditor.src.EditorUI.Tasks.TasksManager:139: Unable to create task Spawning: Unable to assign property Text. string expected, got boolean  -  Edit
  12:19:04.136  Stack Begin  -  Studio
  12:19:04.137  Script 'cloud_133974536197155.TreeAIBehaviorTreeEditor.src.EditorUI.Tasks.TasksManager', Line 139  -  Studio
  12:19:04.137  Script 'cloud_133974536197155.TreeAIBehaviorTreeEditor.Packages.SignalPlus', Line 108 - function reusableThreadCall  -  Studio
  12:19:04.137  Script 'cloud_133974536197155.TreeAIBehaviorTreeEditor.Packages.SignalPlus', Line 113 - function reusableThread  -  Studio
  12:19:04.137  Stack End  -  Studio
  12:19:08.736  Unable to record history
cloud_133974536197155.TreeAIBehaviorTreeEditor.src.Utility.HistoryHandler:13 function StartRecording
cloud_133974536197155.TreeAIBehaviorTreeEditor.src.EditorUI.Tasks.TasksManager:176 function createTaskFromMouse
cloud_133974536197155.TreeAIBehaviorTreeEditor.src.EditorUI.Tasks.TasksManager:138
cloud_133974536197155.TreeAIBehaviorTreeEditor.Packages.SignalPlus:108 function reusableThreadCall
cloud_133974536197155.TreeAIBehaviorTreeEditor.Packages.SignalPlus:113 function reusableThread
  -  Edit


Docs would be stupidly helpful, I feel lost as shit. I cant use something I dont fully understand how to.

Yeah I was using BTrees before and thought that the editor was a bit janky. I don’t know if it’s just an issue on my end, but simply holding down LMB made studio lag at 20fps or lower.
Also adding tasks that required to wait before executing was kinda tricky, since you had to store all the counters inside the tree’s blackboard and stuff like that. Or at least that’s what I tried.
So I thought, why not create a new system myself? And so I made this.

1 Like

Thats a you issue but I disliked the fact it was outdated and just had 0 life support at all. It was kind of a make do thing for me. I was wondering when someone would make an up to date version of it. Which this is.

I personally kinda hated working with it too, it felt a bit janky to get behaviours to function smoothly.

Never seen this before. How did you add the task? By dragging or right clicking?

You can check everything from the Editor controls to what every task does in the info page. Open it by clicking the button at the top right corner

1 Like

As nice as it is it wouldnt take much editing to covert into a markdown and to host a docs site using github hosting. It helps alot if developers can read and work on something at the same time. It kind of just blocks the entire workflow. I wouldnt say kill but def can slow it a little bit.

I also feel it just helps more if there is a website. Also could open source the plugin too on github.

Another thing, I use vinegar, which is doing pretty shit in terms of alot of things right now so I am unable to access the demo place using vinegar for studio as they broke the link handling for opening with studio. So it would help users in my boat if you could provide the place file with it.

fresh tree, went to add a selector and did this with 0 console output as seen.

x/pcalls are handy and it is also useful if the errors were printed too.

I already tought to move everything to github, turn the creator module into a wally package and add rojo support. But I’ll do that after I rewrite the plugin with Fusion, since the plugin code is a bit messy right now.
I think you can still download it with BTRoblox.

I think it’s either a problem with vinegar not registering the inputs right. I tried it now and it worked correctly on my studio.
Did you add the task by dragging from the root task?

I’ll add the demo project file to the first post. Try to move the tasks there and see if they move at all.