beehave
beehave copied to clipboard
Add ParallelNode composite
I was trying out beehave and I've been enjoying it for creating AI. But it got me thinking about if it'd be possible to reuse AI actions and conditions for a player character controller too. This is where I ran into issues as dealing with player inputs in a behaviour tree gets messy pretty fast (very deep nested node trees).
While looking around I noticed a description here of a parallel composite node (which can run in selector or sequence mode). This would make it possible to make a behviour tree more flat which would work better when you're dealing with lots of different player inputs.
I think I know how to implement this in a simple way, but it wouldn't be truly parallel. It would run each child tree collecting their statuses and after all children have been processed it the Parallel node could return success or failure based on the collected results.
Todo
- [ ] introduce
SequenceParallelComposite
- [ ] introduce
SelectorParallelComposite
- [ ] write tests for
SequenceParallelComposite
- [ ] write tests for
SelectorParallelComposite
- [ ] update documentation
- [ ] 💥 BREAKING: replace
get_running_action()
withget_running_actions()
array, as with parallel nodes multiple nodes can be running at the same time.
@monkeez thank you for your suggestion. Could you perhaps update the description with more details on:
- an example of what the problem currently is (screenshot?)
- an example of how the parallel sequence/parallel selector can solve this issue / how will that node be used
@bitbrain I stumbled upon a situation that I think a parallel node would solve my problem. Here's an example, I have an AI that roam and idle respectively so I use a SequenceStarComposite, I also whant my AI to discharge every x seconds while roaming/idling. Basically I need 2 actions running along side each other that is Roam/Discharge. A Parallel Sequence/Selector should continue to process its children even if one of its child is in RUNNING state.
@Ohan17 I didn't put a lot of thought in it, but on first sight it seems that since Roam and Discharge are independent actions maybe they could be different behavior trees all together. And if they need to share data you can assign a common blackboard to both of them. The only issue with that is that it kind of works if your independent nodes are children of the first node I guess. If they are further down the tree, maybe you can have conditions that check the status and running action of the other tree maybe, without having to replicate the whole tree up to that point.
@GeroVeni This while could work, I consider this as a workaround since this is not very flexible. The best solution would be introduce new nodes like I mentioned which will be closer to existing composite nodes or completely parallel using threads like @monkeez proposed.
Any updates on this? Very interested.
My understanding after reading this is that there cannot be a "parallel" version of sequence and selector, but it is rather its own type of composite node with a policy. We can therefore introduce some sort of enum and call it 'ExecutionPolicy' that can either be SEQUENCE or SELECTOR. In case you want to introduce two nodes, feel free to do so. It is up to the implementer to decide.
In addition, the implementer needs to consider the table on that page and replicate its behavior by using Godot threads. Also, we require extensive unit tests (it is documented here how to do that). Also do not forget to write documentation!
~I am working on a pull request for this!~ EDIT unfortunately, I got distracted here so no PR right now.
I would just like to add my two cents to this discussion...
How is using two AI trees for two different behaviors less flexible than creating a whole new node for just this case?
If the two behaviors need to share data you can use the same blackboard, as it has already been said.
By breaking the single responsibility principle, we actually make it less flexible.
I will give you an example that is an extreme edge case but is just to prove my point: imagine you want to run 2 random actions in parallel from a collection of 3 possible actions. This is possible to do it with 3 trees for each behavior, sharing the same blackboard. Or we would need to make an entirely new RandomParallelCompositeNode
along with tests to make sure it works and added complexity (ie.: theres already an issue for weighted random selectors , imagine we would now need to make weighted random parallel actions as well)
Sure, maybe creating a new ParallelComposite feels easier than creating a whole new tree but I think that in the provided case is just a matter of choosing the right tool for the job. The discharge part is so simple it doesnt even need a beehave tree. I could be just a node with a script and a child Timer. (From what I could gather from the comments, maybe theres more to it)
Sorry for the wall of text 😅
The thing with splitting to another behavior tree in this usecase is it not being a composite node where it can be used in sub branches. My example may not be best to describe the issue but the general usage is still run this only when that is running. This is what I came up with using multiple trees approach, I would need a condition to enter Task2
and remember to SetParallelFalse
in any child of any Task to interrupt the whole thing. Editing parallel tasks are even more troublesome, thus making it not very flexible.
Here's a workaround I've managed for now, inspired by https://gdscript.com/solutions/godot-behaviour-tree/
class_name Parallel
extends Composite
enum Policy { SEQUENCE, SELECTOR }
@export var policy: Policy = Policy.SEQUENCE
var finish_count := 0
func tick(actor: Node, blackboard: Blackboard) -> int:
for c in get_children():
if c != running_child:
c.before_run(actor, blackboard)
var response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
_cleanup_running_task(c, actor, blackboard)
c.after_run(actor, blackboard)
finish_count += 1
if policy == Policy.SEQUENCE:
if finish_count >= get_child_count():
finish_count = 0
return SUCCESS
else:
return SUCCESS
FAILURE:
_cleanup_running_task(c, actor, blackboard)
c.after_run(actor, blackboard)
finish_count += 1
if policy == Policy.SELECTOR:
if finish_count >= get_child_count():
finish_count = 0
return FAILURE
else:
return FAILURE
RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
func after_run(actor: Node, blackboard: Blackboard) -> void:
finish_count = 0
super(actor, blackboard)
func interrupt(actor: Node, blackboard: Blackboard) -> void:
finish_count = 0
super(actor, blackboard)
## Changes `running_action` and `running_child` after the node finishes executing.
func _cleanup_running_task(finished_action: Node, actor: Node, blackboard: Blackboard):
var blackboard_name = str(actor.get_instance_id())
if finished_action == running_child:
running_child = null
if finished_action == blackboard.get_value("running_action", null, blackboard_name):
blackboard.set_value("running_action", null, blackboard_name)
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"ParallelComposite")
return classes
Doesn't support multithreading obviously but I just need basic parallelism here. If I have time later I will try to make a PR.
Sequence policy fails when any child fails, and succeeds only when all succeed. Selector policy succeeds when any child succeeds, and fails only when all fail.
May I ask how do you do two running task at the same time without parallel node? Just think about this a few days and cannot figure it out..
@eterlan the idea is that they are not truly running in parallel (multi-threaded) but effectively every frame multiple taks in a RUNNING
state are executed.
May I ask what state this feature is currently in? I stumbled in a situation where I would need to run two actions at the same time.
@bozoVuksan architecturally, beehave 2.x has been designed to only ever have one running node at any time. This feature would require the need to support multiple running nodes. Our current assumption is that this breaks compatibility (beehave node methods need changing) so currently it is aimed for beehave 3.x
In case anyone finds a way to make this work for 2.x without breaking the compatibility, let us know.
Done thanks to #332