beehave icon indicating copy to clipboard operation
beehave copied to clipboard

Add ParallelNode composite

Open monkeez opened this issue 1 year ago • 14 comments

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() with get_running_actions() array, as with parallel nodes multiple nodes can be running at the same time.

monkeez avatar Nov 22 '22 02:11 monkeez

@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 avatar Nov 22 '22 09:11 bitbrain

@bitbrain I stumbled upon a situation that I think a parallel node would solve my problem. image 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 avatar Feb 25 '23 15:02 Ohan17

@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 avatar Feb 25 '23 16:02 GeroVeni

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

Ohan17 avatar Feb 25 '23 19:02 Ohan17

Any updates on this? Very interested.

feelingsonice avatar Mar 17 '23 23:03 feelingsonice

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!

bitbrain avatar Mar 19 '23 11:03 bitbrain

~I am working on a pull request for this!~ EDIT unfortunately, I got distracted here so no PR right now.

bitbrain avatar Jun 14 '23 07:06 bitbrain

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 😅

lostptr avatar Aug 13 '23 01:08 lostptr

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

Ohan17 avatar Aug 13 '23 15:08 Ohan17

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.

xaqbr avatar Oct 17 '23 09:10 xaqbr

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 avatar Oct 22 '23 05:10 eterlan

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

bitbrain avatar Oct 22 '23 12:10 bitbrain

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.

Vukbo avatar Dec 02 '23 21:12 Vukbo

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

bitbrain avatar Dec 03 '23 08:12 bitbrain

Done thanks to #332

bitbrain avatar Apr 23 '24 17:04 bitbrain