Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interrupting flow #6

Open
SaracenOne opened this issue Mar 15, 2021 · 11 comments
Open

Interrupting flow #6

SaracenOne opened this issue Mar 15, 2021 · 11 comments
Labels
enhancement New feature or request
Projects
Milestone

Comments

@SaracenOne
Copy link

This may already be covered, but the current implementation doesn't provide any obvious solution on how to do this. If you an AI agent perform a sequence using things like wait timers or other yields, there doesn't seem to be any way to get them to re-evaluate from a higher level node based on external stimuli. An example might be an AI who simply wanders around, but if they get attacked, you would want their wander sequence interrupted and may instead go into an attack sequence.

@kagenash1
Copy link
Owner

kagenash1 commented Mar 15, 2021

I see what you mean.
If you want to make a branch conditional, you put it under a decorator, extend the decorator and define your condition there (as it's done in the example), but if your sequence (or any other node) is in a running state it can't be interrupted until completion, at least not in a straightforward way.

I will definitely work on making this possible, maybe with an event driven approach as it's done in Unreal.
Thank you for bringing it up, this is a work in progress and I'm trying to improve it everyday!

Allowing abortion will require some design changes so it may take a bit longer than other smaller improvements I have planned.
The best thing you could do until I implement this, is to put your 'states' (wandering, attacking, etc.) under a Parallel node and enclose them in a decorator. A parallel node allows you to execute all the children regardless of result and without waiting for execution.
Something like this:
Screenshot 2021-03-15 082938

Obviously there are a few extra steps to this solution: because it won't abort the previous sequence, you may have overlapping behavior. So it depends on how things are on your end, but if you are just waiting you can put another condition after the wait is completed to check if you are still in, say, 'wandering state' and return fail() if you are not, so the sequence doesn't continue.

Another thing is putting a while loop inside your decorator and keep ticking as long as 'wandering' is true.

So ultimately, you have a lot of control over how you design it: my suggestion is to prefer short, repeated actions rather than tasks that take very long to complete. This is the way I see a behavior tree should be designed, but what you suggested is definitely something that other implementations have and that should be possible for the user to do.

Also keep in mind that using the Behavior Tree like this makes it kinda like a state machine. So if you wanna design things in that way, a state machine is probably more suitable (I recommend https://gitlab.com/atnb/xsm)

@SaracenOne
Copy link
Author

Yeah, trying parallel nodes, I was able to develop some AI which addressed my problem reasonably well. I think taking your advice of thinking about behaviour trees less in the terms of finte state machines helps too. I am fairly new to them. That said, I still think there's value in the Unreal-style approach of branch aborting, especially as trees start to balloon in complexity.

@kagenash1
Copy link
Owner

Good :) Parallels are the "worst" in terms of performance so, if you can, think of other ways.
Always happy to provide support if you need! I know it's not very user friendly yet but I'm actively trying to improve it and make it as complete and powerful as possible.
I'm onto the flow abortion now so it will be a thing in the next update. Also, the way running states are handled will change with it, so I'm gonna have to make sure the update doesn't break your behavior trees.

@MadFlyFish
Copy link

Thanks for your great addons! I have being using it for a couple of weeks, and still looking forward to more future updates.
Recently I am developing a tool, which uses Xmind and excel as an tree editor and create behavior tree dynamically in run-time, mostly based on your addons.
That helps my work a lot. Therefore I am introducing your addons to my Godot developer friends in China, and they show great interest too.
May I have your email or other contacts and learn some more about AI? Thanks.

image
image
image
image
image
image

@kagenash1
Copy link
Owner

kagenash1 commented Mar 19, 2021

It's amazing to see this! I'm glad my addon turned out useful and I'm excited to see what you've done with it.
My email is gabriele.torini1@gmail.com or if you want you can find me on Discord at kagenashi#8224

(By the way, this is not really an issue nor it is related to the issue above, so next time open a new discussion instead)

@kagenash1 kagenash1 added this to the 1.2.0 milestone Mar 19, 2021
@kagenash1 kagenash1 added the enhancement New feature or request label Mar 19, 2021
@kagenash1 kagenash1 added this to To do in 2.0 Mar 19, 2021
@kagenash1
Copy link
Owner

This issue will be addressed in the 1.2.rc4 version.

@kagenash1 kagenash1 removed this from To do in 2.0 Apr 28, 2021
@kagenash1 kagenash1 modified the milestones: 2.0, 1.2 Apr 28, 2021
@SaracenOne
Copy link
Author

Nice!

@kagenash1
Copy link
Owner

kagenash1 commented Apr 28, 2021

I have started updating the 1.2.rc4 branch. There are some changes already, although the features is not there yet. However, I made a much more complex example to show how to achieve some of the behaviors you people were talking about!

That shows how the tree can be made responsive and flexible. I think that even without a "interrupt flow" function this is getting the job done nicely and encapsulates very complex logic with just a few nodes.

@kagenash1 kagenash1 modified the milestones: 1.2, 1.3.0 Apr 30, 2021
@kagenash1 kagenash1 modified the milestones: 1.3, 2.0 May 9, 2021
@kagenash1 kagenash1 added this to To do in 2.0 May 9, 2021
@mcccclean
Copy link

Hello, weighing in on this quite late - just here to say that I've had some success implementing this locally without too much refactoring, and I wanted to share my approach in case it is useful toward the official implementation (or to others finding this thread).

In bt_node.gd (unrelated sections elided):

signal completed(result)
const ABORTED = -1;

func abort():
  fail()
  complete(ABORTED)

func complete(value):
  if running():
    return
  emit_signal("completed", value)

func tick(agent: Node, blackboard: Blackboard) -> bool:
  # .... etc ...
  if result is GDScriptFunctionState:
    assert(running(), "BTNode execution was suspended but it's not running. Did you succeed() or fail() before yield?")
    result.connect("completed", self, "complete")
    result = yield(self, "completed")
    if result is int and result == ABORTED:
	    return

and then in BehaviourTree:

# halt the behaviour tree
func interrupt() -> void:
  bt_root.propagate_call("abort")

# halt the behaviour tree and restart it
# (useful for scenarios where an agent is surprised, eg taking damage, enemy arrives in range etc)
func hard_restart() -> void:
  interrupt()
  is_active = true
  start()

The central change there is that the yield inside tick can now be terminated early by calling complete on the node, rather than always having to wait for the underlying function call to finish. This allows for mechanisms that short circuit the node into a success or failure state as well as aborting it.

Of course there are some downsides to this approach - the function that the node was waiting on will still continue its execution uninterrupted, which might have strange side effects. This won't come up if nodes are designed to only do one thing (like, a node whose job is to wait and then execute a side effect should probably be split up into a waiter and an executor node) but perhaps this is too big of a tripping hazard to include without further safeguards. I also don't know how performant it is; my project only includes a handful of actors so it might have additional problems when dozens/hundreds of agents are in the scene.

Thanks @GabrieleTorini for the great module - it's so useful and extensible, it's been a great help to my hobby projects. I hope this is useful to you!

@kagenash1
Copy link
Owner

Thanks! This is definitely a great addition.
Would you like to make a PR for this? I'll try to see if it works with the sample project.
(I'm not a geet (git geek) so the whole process of the PR is probably gonna be clunky)

I'm glad you're happy with the plugin. It was a big experiment, and thinking back I think it could be done in an entirely different way, but as long as it's stable and gets you to make your AI, I'm satisfied!

@ATHellboy
Copy link

Hello, weighing in on this quite late - just here to say that I've had some success implementing this locally without too much refactoring, and I wanted to share my approach in case it is useful toward the official implementation (or to others finding this thread).

In bt_node.gd (unrelated sections elided):

signal completed(result)
const ABORTED = -1;

func abort():
  fail()
  complete(ABORTED)

func complete(value):
  if running():
    return
  emit_signal("completed", value)

func tick(agent: Node, blackboard: Blackboard) -> bool:
  # .... etc ...
  if result is GDScriptFunctionState:
    assert(running(), "BTNode execution was suspended but it's not running. Did you succeed() or fail() before yield?")
    result.connect("completed", self, "complete")
    result = yield(self, "completed")
    if result is int and result == ABORTED:
	    return

and then in BehaviourTree:

# halt the behaviour tree
func interrupt() -> void:
  bt_root.propagate_call("abort")

# halt the behaviour tree and restart it
# (useful for scenarios where an agent is surprised, eg taking damage, enemy arrives in range etc)
func hard_restart() -> void:
  interrupt()
  is_active = true
  start()

The central change there is that the yield inside tick can now be terminated early by calling complete on the node, rather than always having to wait for the underlying function call to finish. This allows for mechanisms that short circuit the node into a success or failure state as well as aborting it.

Of course there are some downsides to this approach - the function that the node was waiting on will still continue its execution uninterrupted, which might have strange side effects. This won't come up if nodes are designed to only do one thing (like, a node whose job is to wait and then execute a side effect should probably be split up into a waiter and an executor node) but perhaps this is too big of a tripping hazard to include without further safeguards. I also don't know how performant it is; my project only includes a handful of actors so it might have additional problems when dozens/hundreds of agents are in the scene.

Thanks @GabrieleTorini for the great module - it's so useful and extensible, it's been a great help to my hobby projects. I hope this is useful to you!

Hey there,

I'm using this piece of code for hard restarting the bt but it doesn't work for me. At least it doesn't work when the current leaf is under RepeatUntil. Have you checked it with RepeatUntil too ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
2.0
To do
Development

No branches or pull requests

5 participants