BehaviorTrees3 + BTrees Visual Editor v3.0

https://www.roblox.com/library/5872051766

Intro

Behavior trees are super handy tools, usually used to help organize and design complex AI behavior flow. If you don’t understand behavior trees, none of the rest of this post will make any sense. Here’s a great article explaining what they are, how they work, and why they’re useful for AI development:

This is an extension of the awesome work by @tyridge77 and @oniich_n. Read their posts to get fully caught up to speed:

The BehaviorTrees2 module and tyridge’s node editor are awesome, but I ran into some limitations that I wanted to fix, so I started forking. Three days later, the changes are substantial enough that tyridge and I think this is worth its own post.

Note: Trees created with the BTrees Editor 2.0 are not compatible with the BTrees Editor 3.0, and BehaviorTrees3 has slightly different constructor API than BehaviorTrees2.

Functional Overview

BehaviorTrees3 is the module that compiles and runs behavior trees in the game. If you wish, you could use it directly to construct behavior trees manually through code by setting up a bunch of tables, but this isn’t easy or intuitive.

BTrees Visual Editor v3.0 is a plugin that you can use to visually set up behavior trees in a node based graphical environment. It’s a lot more intuitive and fun than writing out tables! It represents behavior trees with folders instances, which have a bunch of other instances in them to hold the tree data. Don’t mess with those descendant instances if you want to stay out of trouble.

BehaviorTreeCreator is a module that is the bridge between the visual editor and the BehaviorTrees3 module. Pass it a Tree folder that you made with the plugin, and it will give you a tree object that you can :run().

Main Takeaways

Check it out! Curvey lines! Node paremeters! Repeat node! Tree node! The repeat node can repeat its child actions. The tree node can be used to link to another tree, and will pass the status of that tree. Tree reusability will be super handy to reuse logic for lots of NPC classes with similar AI.

All of these trees, which previously did not run as expected because of how Decorators would only work when directly parented to a Task, are now all valid and work as expected:

Noteable QoL improvements:

  • Super easy to add new node types to the editor if you make a local fork of the plugin. Nodes are automatically added to the help menu + add node menu.
  • Root nodes are explicit nodes in the editor, so you can change the tree root without needing to destroy a bunch of nodes until the autoselected root happens to be the one you want. You can add multiple root nodes, but only the first will be the real active root. The other root nodes will be grayscaled.
  • Dragging a node will also move all of its descendants as a group.
  • After dragging a node, it and its siblings index order is automatically resorted based on X position. This alleviates a common design failure mode in which you don’t realize that the index order isn’t what you expected based on the visual graph.

Changes / Improvements

This is a mostly comprehensive list all the updates I’ve made:

BTrees Visual Editor 3.0

  • Added NodeInfo module to make adding node types to the editor easier. This does not include actual node functionality; this must be implemented in BehaviorTrees3.
  • Support for nodes with string/number/boolean parameters
  • Explicit root node
  • Nodes are colored by category (composite/decorator/leaf/root) rather than by individual node type. I think this is easier to read at a glance.
  • Dragging will drag descendants
  • Automatic index ordering after dragging
  • Behavior tree folders can be created anywhere, instead of locked to a single directory for all of them. When you click the Create toolbar button, it will create a tree in your current selection.
  • The node info in the help window is dynamically populated from the list of node types in the NodeInfo module.
  • Random weight inputs will round up decimal inputs, as these aren’t supported with the random weighting implementation
  • Pretty bezier curves

BehaviorTrees3

  • Previously, decorators could only be adorned directly to tasks, and they would have no function if adorned anywhere else. They now can be put anywhere or even chained together, and will work as expected. I changed the way decorators work internally, but preserved the pretty clever and efficient tree traversal system that BehaviorTrees2 already had, so it should still be just as fast.
  • Trees return their result when you call :run()
  • Added Repeat node
  • Added Tree node, which will process and pass a result from another tree.
  • Small refactor in the ProcessNode algorithm to make it easier to read and more intuitive to add new node types. Mainly, consolidated the node traversal nested loops which appeared in a few places into a single interateNodes() iterator, and added a addNode() function.
  • Success/fail nodes can act as a standalone leaf if left dangling
  • Changed success/fail/running node states to number enums instead of strings, so the tree doesn’t have to do string comparisons. Should be a tiny bit faster.
  • Changed tasks to report their status by returning a status number enum, instead of calling a function on self
  • Slightly changed node constructors
  • Added better assertions to tell you if you try to construct an invalid tree
  • Changed “Task”/“Selector” language to more conventional “Leaf”/“Composite”
  • Improved commenting/documentation

BehaviorTreeCreator

  • Added TreeCreator:SetTreeID(treeId, treeFolder), to associate trees with Tree nodes based on the TreeID string parameter.
  • Changed TreeCreator:Create parameters from obj, treeFolder to treeFolder, obj. Made obj optional.
  • Trees are created and cached from the tree folder directly, rather than a string treeindex.
  • Added commenting/documentation

That’s it! Let me know if you have feedback for UX improvements, new nodes with common use cases, etc. Also, this isn’t fully battle tested yet, so let me know if you find any bugs or if you find yourself with a tree that isn’t behaving as you expect.


Edit Dec 2 2020: @tyridge77 just made some big updates, see below. Note that if you move to this version, there is slightly different API; tree:setObject() is no longer used, and the object is run with the object as a parameter. BehaviorTrees3 + BTrees Visual Editor v3.0 - #23 by tyridge77

262 Likes

This is really cool, I’ll definitely try it out but is there any chance we can get some video tutorials on how to implement it?

27 Likes

I don’t know am I dumb or what why this extension is used for​:sweat_smile::sweat_smile: I haven’t heard of old extensions too.

1 Like

I would love to see an tutorial about this on how it works, it‘s very interesting.

11 Likes

Really incredible improvements over my original. I highly recommend if you work with a lot of AI agents, simple or complex.

15 Likes

awesome asset! it’d be cool if we’d get video tutorials on how to implement this in a proper way. i’ve personally worked with behaviour trees a lot, and not until now discovering something like this exists made me to the least rather infuriated.

3 Likes

I seem to be having issues with some of the example code from the repository’s readme. In particular, I have made a tree with a random node and it doesn’t seem to be using the weights properly. Currently debugging the issue and I will post once I know more.

local node1 = BehaviorTree3.Task({
	weight = 10,
	module = {
		run = function(task, object)
			print("Weight: 10")
			return SUCCESS
		end,
	},
})
local node2 = BehaviorTree3.Task({
	weight = 10,
	module = {
		run = function(task, object)
			print("Also weight: 10")
			return SUCCESS
		end,
	},
})
local node3 = BehaviorTree3.Task({
	weight = 200,
	module = {
		run = function(task, object)
			print([[You probably won't see "Weight: 10" printed.]])
			return SUCCESS
		end,
	},
})

local Tree = BehaviorTree3.new({
	tree = BehaviorTree3.Sequence({
		nodes = { BehaviorTree3.Random({
			nodes = { node1, node2, node3 },
		}) },
	}),
})
local treeStatus = Tree:run()

EDIT: In my above code, the sequence doesn’t appear to get parsed correctly. The random node is never Processed via ProcessNode and doesn’t have the weights set properly. Also note that I have changed the :new method to .new in the module itself and that is why my call in the example is to BehaviorTree3.new(). It’s possible that I am using the methods wrong but I am not sure how if I am.

1 Like

Sorry about that, it looks like the weight parameter of the node wasn’t where I thought it was in ProcessNode; should have been in node.params. I updated the repo and your code seems to work now, can you try again?

2 Likes

It’s all good! I will try it tonight and update this reply with the results.

Thank you for taking a look!

EDIT: I can confirm the issue with Random is now fixed! Thanks again!

1 Like

Love the plugin. But I noticed that when you reopen your saved file, you can’t use the Plugin UI with previously made btrees. It would be nice if you can open previously made btrees.

This is correct; the trees are saved in a slightly different format. If someone wants to write a command that can convert trees from the old format to the new format that would be awesome, but I don’t have time atm. The differences are:

  • The tree folder needs a “_BTree” collection service tag
  • There needs to be a new folder in each node folder called “Parameters”
  • The “Index” value should be moved from the node folder into the Parameters folder
  • The “Root” node folder needs to be renamed to something arbitrary like “OldRoot”
  • A new node called “Root” needs to be created with Type “Root” and with one output pointing to the old root

I believe that with these changes you should be compatible, let me know if that still doesn’t work.

Ah, I see.
Regardless, the plugin is amazing. You did an amazing job with it. Hopefully someone can create a tutorial for how to use it. If not then I’ll look into making one. It really makes creating enemy bots so much easier. An example is right there

https://streamable.com/l4jj3q

8 Likes

Someone should totally make a vocal scripting tutorial showing how to use them in a practical manner. We’d love that :slight_smile: it’d help out a lot of people. I would myself if I had the time, I should’ve probably made one for the original plugin

3 Likes

Great work! This makes me excited to work on my NPCs. I had one idea. What if instead of jumping back to the same task when RUNNING is returned, it traversed through the tree again. task.start and task.end can fire depending on if another task returns RUNNING during the traversal.

Say an enemy NPC is in a wondering state and a player walks into their attack range. The NPC can’t detect any state changes from doing a tree traversal because it’s already running a task. The task itself has to listen for the state change which just seems unnecessary.

I’m new to behavior trees so I’m probably missing something.

3 Likes

Good question, I’m also not super experienced with behavior trees but this is what I think I know.

From what I understand, the reentry point of a running tree is an implementation detail that seems to vary, and oniich and tyridge decided to implement it this way. As you described, it seems like the main advantage of reevaluating the entire tree is that it allows higher priority tasks to interrupt the current running task, and the main disadvantage is that tree retraversal could be expensive. I’m also not exactly sure how previous tasks would behave when being iterated over on the way back to the current running task.

I’m sure this could be implemented as an optional mode, but I think in the case of the example you described it would not be necessary. I’d design any idle tasks to always return success, and reserve the running state for tasks that run over a defined bounded period of time.

4 Likes

Incredible plugin, I’ve used the extremely old behaviour tree module originally ported by oniich and I got to say, using this is a major step up in terms of workflow and enjoyment.

A new AI I've been working on using this BTrees, quite simple but effective

Video Footage: https://imgur.com/KFYetQG


If I may make a few suggestions, there are a few minor workflow problems that I’ve encountered with this plugin that I haven’t encountered with any other plugin and I’m unsure what causes it. When trying to select/edit a BTree folder in a relatively large place, my studio will hard freeze for a good 10 - 25 seconds before it becomes responsive again (which then will finally open up the editor). Deselecting the folder will do another 10 - 25 seconds of freezing before it returns back to normal Roblox studio view. My computer is quite at the top end so I doubt that is the issue.

Due to the issue above, I’ve been using the Btrees plugin in an empty baseplate and then copying over the tree to the main place once I am finished. This then poses another problem where the tree tag doesn’t carry over and then the plugin doesn’t recognize that it’s a tree (until I add back the tag manually). Would be incredible if the plugin would recognize the btree folders automatically in different places (or has a way to easily add the tags itself!). The only way for the plugin to recognize it is a tree again is to restart studio or to keep using the original place it came from.

3 Likes

Yikes, sorry about the freezing. This is caused by this issue from the 2.0 thread:

I left that in despite the issue because I couldn’t find a better solution around the problem. This would be worth writing a studio bug report for. For now, you can comment out that line if you want to work on your main place, but accidentally pressing undo can delete or revert your trees.

2 Likes

Hey, I just want to point some troubles I had while updating from BT2 to BT3. Just to be clear, I am not using the plugin, I am creating the nodes using the BT3 module.

I ran into a problem while using the nodes table like this:

newSequence = BehaviorTree3.Sequence({
    nodes = {
        -- Node 1
        node1 = BehaviorTree3.Task({
            run = function(object)
                --Code
            end
        }),
        
        -- Node 2
        node2 = BehaviorTree3.Task({
            run = function(object)
                -- Code
            end
        })
	}
})

Since the module is using:

assert(#node.params.nodes >= 1, "Can't process tree; sequence composite node has no children")

I am getting a minor error but it can be easily bypassed by not giving nodes a specific key in the table like this:

-- Node 1
BehaviorTree3.Task({
    run = function(object)
        --Code
    end
})

And now the reason why I am posting is that I’m getting an error due to “task leaf node has no linked task module”, and to be honest I have no idea why is that. Is there something I’m missing?

btw, great update, I really like the new task status.

The nodes table needs to be a list, the way you had it set up in your first snippet was a dictionary. This would be the correct setup:

newSequence = BehaviorTree3.Sequence({
    nodes = {
        -- Node 1
        BehaviorTree3.Task({
            run = function(object)
                --Code
            end
        }),
        
        -- Node 2
       BehaviorTree3.Task({
            run = function(object)
                -- Code
            end
        })
	}
})

Good catch on that linked module error, that was an oversight with assuming you’re using the plugin editor. I’ve fixed the module so the Task constructor matches the documentation.

1 Like

Hey everyone, I spent the last couple weeks making a big update to the plugin that adds new content and fixes some issues.

If you update anything you should update everything, including the BehaviorTreeCreator and BehaviorTree3 modules otherwise things may not work properly!

Here’s a rather large list of all the changes

Added live tree debugging to assist in figuring out what your tree is doing !

  • Only works in studio

  • To use , join a game and click on the debug button under the plugin buttons

    • This will open a window of trees which have ran at least once.

    • Each debuggable tree in the window will have its name set to the object’s table name, unless the object has an index called name or Name, which it will then use

    • To debug a tree, click on the debug button.

      • This will open up the tree similar to the editor, only you can’t edit anything
      • The currently running task/tree will be higlighted in yellow, with a yellow path dictating how it got there from the root

Changed how trees are created and ran

  • Trees are now decoupled from instances, acting more as pure functions not necessarily attached to any specific object - kind of like module scripts. This is how behavior trees are typically implemented in the game industry because it’s more sensible and has less performance + memory overhead

  • Trees are created with BehaviorTreeCreator:Create(treefolder). If a tree is already created for that folder it will return that tree

  • Trees are now run using Tree:Run(object,…)

    • object must be a table

Here’s an example of how you’d now set up and run a tree

local BehaviorTreeCreator = require(game.ReplicatedStorage.BehaviorTreeCreator)

local IdleTree = BehaviorTreeCreator:Create(game.ReplicatedStorage.Idle)
local Object = {
	Name = "Bob",
	model = workspace.Bob,
	human = workspace.Bob.Humanoid,
}

-- In some update function
	IdleTree:run(object)
--

Added new tree method, Tree:Abort()

  • Calls finish() on the running task of the tree, and sets the tree index back to 1
  • Should be used if you want to cancel out of a tree to swap to another(for instance in the case of a state change if your tree is driven by states)

Added Blackboards (Partially inspired by UE4s blackboard system)

  • Literally just a table that trees can read from and write to via tasks

    • Injected into the object passed into a tree via Tree:Run(object), if it doesn’t already exist
  • Commonly used to see if a value is set or not in order to dictate the flow of relevant logic in a tree

  • Shared Blackboards are also a thing. You shouldn’t write to these in a tree, but they are useful for reading shared states(global stuff, player stuff, are the lights off in a scene)

  • To register a Shared Blackboard use BehaviorTreeCreator:RegisterSharedBlackboard(name,table)

function task.run(obj)
	local Blackboard = obj.Blackboard -- Use this if you want to read and write values which your "Blackboard" nodes can reference	

Added new Leaf node, Blackboard Query

  • These are used to perform fast and simple comparisons on a specific key in a blackboard

  • For instance, if you wanted a sequence to execute only if the entity’s “LowHealth” state was set to true, or if a world’s “NightTime” state was set to true(Shared Blackboard)

  • You can do this with tasks , but it’s a bit faster if you only need to perform a simple boolean or nil check

  • You can only read from a blackboard using this node.
    Behavior Trees aren’t meant to be visual scripting - just a way to carry out plans

  • Parameters:

    • Board: string that defaults to Entity if no value is specified.

      • Entity will reference the object’s blackboard passed into the tree via tree:Run(object)
      • If a value is given, say “WorldStates”, it will attempt to grab the Shared Blackboard to use with the same name. You can register these via BehaviorTreeCreator:RegisterSharedBlackboard(name,table)
    • Key: the string index of the key you’re trying to query(for instance, “LowHealth”)

    • Value: string which specifies what kind of query you’re trying to perform.

      • You can choose true,false,set,or unset to perform boolean/nil checks. Alternatively you can specify a string of your choice to perform a string comparison

Here’s an example of using a Shared blackboard

Here’s an example of an Entity blackboard(default)

Added new composite node, While

  • Only accepts two children, a condition(1st child), and an action(2nd child)
  • Repeats until either
    • condition returns fail, wherein the node itself returns fail

    • MaxAttempts is reached, wherein the node itself returns fail

    • action returns success, wherein the node itself returns success

    • Used for processing stacks of items. Not sure if there are any other use cases

      • Say you want an NPC to create a stack of nearby doors,
        then try to enter each door until there are no doors left to try,
        or the NPC got through a door successfully.

        If the NPC got through a door successfully, the node would return success. Otherwise, if there were no doors that were able to be entered, the node will return fail

        Here’s how you would do that

This makes it much easier to do behavior on a set of objects without having to hard code it all within a task, which can get way uglier especially when you have to introduce fallback conditions.

Added comment nodes!

These serve no functional purpose but allow you to comment areas in your tree for organization or mnemonic purposes

  • To edit a comment click the edit button on top of the comment node
  • Two other toggle buttons are TextScaled and Text X Alignment

Misc/QOL Updates and Fixes

  • The add node window is now properly clamped to the extents of your screen

  • Fixed issue where repeater node didn’t function properly

  • You can no longer edit trees while a game is running

  • Index resorting happens automatically when you add a new node, before it only happened when you released drag

  • Nodes with more than one parameter are now scaled up a bit more by default

34 Likes