Open davebaol opened 10 years ago
Just completed parallel description in the wiki page.
Also, I've updated all behavior tree tests. Now they use the tree viewer that is a good learning tool for beginners IMO.
There's just a little problem in the SemaphoreGuardTest
. Currently the user can save and load the two trees independently from each other, meaning that he can easily break the semaphore and get both WalkTask
running at the same time. I think I will inhibit load and save buttons for this test.
I'd like to add the capability to clone trees with external libraries like kryo in order to free the user from the responsibility of overriding copyTo
properly for each task.
I'm thinking of something like this:
public interface TaskCloner {
public <T> Task<T> cloneTask (Task<T> task);
}
and in Task
class
public static TaskCloner TASK_CLONER = null;
/** Clones this task to a new one.
* @return the cloned task
* @throws TaskCloneException if the task cannot be successfully cloned. */
@SuppressWarnings("unchecked")
public Task<E> cloneTask () {
if (TASK_CLONER != null) {
try {
return TASK_CLONER.cloneTask(this);
} catch (Throwable t) {
throw new TaskCloneException(t);
}
}
try {
return copyTo(ClassReflection.newInstance(this.getClass()));
} catch (ReflectionException e) {
throw new TaskCloneException(e);
}
Actually, copyTo
is still the default but you can easily use kryo (no GWT support), for example:
// Use kryo to clone tasks
Task.TASK_CLONER = new TaskCloner() {
Kryo kryo;
@Override
public <T> Task<T> cloneTask (Task<T> task) {
if (kryo == null) {
kryo = new Kryo();
kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
}
return kryo.copy(task);
}
};
However, I'm not very happy with the static TASK_CLONER
field.
Any cleaner solutions?
Why don't use cloner as parameter for method cloneTask?
In this case developer can decide is cloner static or something else, also if parameter is null standard cloner could be used
@sergeyzhulin Thanks, your idea is interesting but looks like there are 2 drawbacks:
Static feels like only options that doesnt require adding another param to a lot of things.
OT: BranchTask.removeChild(int)
?
@piotr-j
Static feels like only options that doesnt require adding another param to a lot of things.
Ok then static will be fine
OT: BranchTask.removeChild(int) ?
Why not Task.removeChild(int)
? Then you want BehaviorTree.Listener.childRemoved(Task<E> task, int index)
I guess.
EDIT: Hmmm.... Maybe I'm exaggerating :)
I guess it could be in Task
, but most of the things can have only 0/1 children anyway. It would be more consistent i guess.
Damn stupid javadoc! It's so fucking hard to get embedded code shown properly.
I had to use a combination of <pre>
, <code>
, {@lileral ...}
and finally {@code ...}
for generics :flushed:
https://github.com/libgdx/gdx-ai/blob/5839294958e78c49dfe2dfb0ec9b7188c6114adb/gdx-ai/src/com/badlogic/gdx/ai/btree/Task.java#L55-L70
Added test "Predators: Parallel vs. Sequence" that visually shows through steering behaviors the difference between sequence task and parallel task with sequence policy.
Damn, I'm really tempted to change the API. Especially, I'd like to remove success
, fail
and running
methods.
Here's my story. I'm working on the behavior tree of dogs in this project and I was getting crazy because tree evaluation suddenly messed up. Then, after some hours, I found out the problem in the code below.
@Override
public void run () {
DogCharacter dog = getObject();
if (......) {
dog.calculateNewPath();
success();
}
fail();
}
When the if condition is true
both success
and fail
methods are executed. This was messing up the parent branch task. Simply adding the missing return
after success
fixed the problem.
Also, if you forget to call success
, fail
or running
you're messing up things once again.
So, the problem with current API is that breaking the whole tree evaluation is too easy and very hard to debug.
In order to minimize error probability, I think that previous code should change like that
@Override
public Task.Status run () {
DogCharacter dog = getObject();
if (......) {
dog.calculateNewPath();
return Task.Status.SUCCESS;
}
return Task.Status.FAILED;
}
What do you think?
Bit me as well. Forcing return sounds like a decent solution. Alternatively it could throw an exception if it is changed more then once per run.
We should also throw an exception if none of success()
, fail()
and running()
has been called.
I think I found a simpler solution. Since most if not all tasks written by developers are leaf tasks, we can fix the issue in the LeafTask
class and keep the rest of the API unchanged:
public abstract class LeafTask<E> extends Task<E> {
...
/** This method contains the update logic of this leaf task. The actual implementation MUST return one of {@link Status#RUNNING},
* {@link Status#SUCCEEDED} or {@link Status#FAILED}. Other return values will cause an {@code IllegalStateException}.
* @return the status of this leaf task */
public abstract Status execute();
@Override
public final void run () {
Status result = execute();
if (result == null) throw new IllegalStateException("Invalid status 'null' returned by the update method");
switch (result) {
case SUCCEEDED:
success();
return;
case FAILED:
fail();
return;
case RUNNING:
running();
return;
default:
throw new IllegalStateException("Invalid status '" + result.name() + "' returned by the update method");
}
}
}
Done :)
Guys, I'd recommend you to take a look at the open source project GdxDemo3D if you're interested in non-trivial use of behavior trees. Dog's brain is not complete yet (it will just piss instead of taking the stick and bringing it back, for instance), but already able to coordinate several 3D animations and behaviors properly. @jsjolund and me are still working on it. Also, that guy is great when it comes to 3D modelling :sunglasses:
Updated the wiki page with the link to the new code I added to the GdxDemo3D in order to combine behavior trees and state machines based on approach 1 described here.
I have to admit that I'm very happy with the new API. It has proven to be really versatile during the development of dog's behaviors in the GdxDemo3D.
New release coming soon.
gdx-ai 1.7.0 is out
From the JBT user manual:
- Dynamic Priority List: task that executes the child with the highest priority whose guard is evaluated to true. At every AI cycle, the children's guards are re-evaluated, so if the guard of the running child is evaluated to false, it is terminated, and the child with the highest priority starts running. The Dynamic Priority List task finishes when no guard is evaluated to true (thus failing) or when its active child finishes (returning the active child's termination status).
- Static Priority List: task that executes the child with the highest priority whose guard is evaluated to true. Unlike the Dynamic Priority List, the Static Priority List does not keep evaluating its children's guards once a child is spawned. The Static Priority List task finishes when no guard is evaluated to true (thus failing) or when its active child finishes (returning the active child's termination status).
These tasks solve a typical issue with behavior trees, i.e. they tend to be reasonably clunky when representing state-based behaviors. One possible solution is to combine behavior trees and state machines as described here.
With priority lists you can do all of that quite comfortably with behavior trees.
For instance, in GdxDemo3D the dog uses these 3 trees and a state machine to determine which behavior tree is currently running. These are the 3 states deciding when to switch behavior tree (note the overriden canEnter
methods which are the condition guards you would use with a priority list task).
In short, the 3 trees and the state machine above might become something like that
import calculatePathToHuman:"com.mygdx.game.objects.dog.CalculatePathToHumanTask"
import calculatePathToStick:"com.mygdx.game.objects.dog.CalculatePathToStickTask"
import followPath:"com.mygdx.game.objects.dog.FollowPathTask"
import lieDown:"com.mygdx.game.objects.dog.LieDownTask"
import piss:"com.mygdx.game.objects.dog.PissTask"
import setThrowButton:"com.mygdx.game.objects.dog.SetThrowButtonTask"
import sit:"com.mygdx.game.objects.dog.SitTask"
import spinAround:"com.mygdx.game.objects.dog.SpinAroundTask"
import spinAroundToFaceHuman:"com.mygdx.game.objects.dog.SpinAroundToFaceHumanTask"
import stand:"com.mygdx.game.objects.dog.StandTask"
import stateMachineTransition:"com.mygdx.game.objects.dog.StateMachineTransition"
import stickThrown?:"com.mygdx.game.objects.dog.StickThrownCondition"
import wander:"com.mygdx.game.objects.dog.WanderTask"
import whine:"com.mygdx.game.objects.dog.WhineTask"
import alreadyCriedForHumanDeath?:"com.mygdx.game.objects.dog.AlreadyCriedForHumanDeathCondition"
import isHumanDead?:"com.mygdx.game.objects.dog.IsHumanDeadCondition"
import isHumanInRange?:"com.mygdx.game.objects.dog.IsHumanInRangeCondition"
import humanWantToPlay?:"com.mygdx.game.objects.dog.HumanWantToPlayCondition"
subtree name:"feelSadForHumanDeathGuard"
sequence
invert
alreadyCriedForHumanDeath?
isHumanDead?
isHumanInRange? meters:20
subtree name:"feelSadForHumanDeath"
sequence
calculatePathToHuman
followPath gait:"run"
whine
parallel policy:"selector"
wait seconds:"uniform,10,25"
lieDown
whine
parallel policy:"selector"
wait seconds:"uniform,5,9"
sit
setAlreadyCriedForHumanDeath # this makes the task's guard fail; the dog will start acting on his own
subtree name:"playWithManGuard"
humanWantToPlay?
subtree name:"playWithMan"
sequence
calculatePathToHuman
followPath gait:"run"
spinAroundToFaceHuman
setThrowButton enabled:true
parallel policy:"selector" # wait for the man to throw the stick
stickThrown?
repeat
sequence
parallel policy:"selector"
wait seconds:"uniform,3,5"
randomSelector
stand
sit
spinAround
setThrowButton enabled:false
calculatePathToStick # actually we just calculate a random point
followPath gait:"run"
subtree name:"actOnYourOwnGuard"
invert
selector
humanWantToPlay?
isHumanDead?
subtree name:"actOnYourOwn"
selector
sequence
random success:0.1
piss
parallel policy:"selector"
wait seconds:"uniform,3,6"
randomSelector
wander gait:"run"
wander gait:"walk"
lieDown
sit
root
dynamicPriority
[$feelSadForHumanDeathGuard] $feelSadForHumanDeath
[$playWithManGuard] $playWithMan
[$actOnYourOwnGuard] $actOnYourOwn
where the main tree is at the end of the file (identified by the keyword root
) and each child of the dynamicPriority
task has a guard in square brackets (actually a previously defined subtree
that models the conditions of the canEnter
method in the corresponding state) followed by the actual task (which is another previously defined subtree
).
You can think of the $subtreeName
syntax like a kind of macro.
An important aspect to support priority lists is to keep the text format as simple as possible, since I don't want to get crazy while writing the parser. So, if you have better ideas, I'm all ears :ear:
Also if I got it right the static priority
staticPriority
[guard1] task1
[guard2] task2
[guard3] task3
acts like the more verbose tree below
selector
sequence
guard1
alwaysSucceed
task1
sequence
guard2
alwaysSucceed
task2
sequence
guard3
alwaysSucceed
task3
with the difference that the latter always returns SUCCEEDED
when its active child task finishes, instead of returning the active child task's termination status.
I've just realized that, at the API level, we can take into consideration the idea to add a guard to the Task
base class, meaning that any task in any part of the tree can be conditioned.
In normal situations, if the task's guard succeeds the task is evaluated otherwise FAILURE
status is returned.
On the other hand, if the task is a child of a priority branch, either dynamic or static, its guard is evaluated according to its parent policy.
For instance, in absence of a priority branch parent, the tree below
sequence
random success:0.1
piss
can be expressed simply by
[random success:0.1] piss
Also, task's guards are optional, meaning that they implicity evaluate to SUCCESS
.
So, the tasks piss
, [] piss
and [success] piss
are unconditioned and totally equivalent
What do you think?
@implicit-invocation I'd like to ear your opinion too :)
BTW, since guards are nothing more than good old tasks, guarding the guard would come for free. I mean something like
[[[guard0] guard1] guard2] task
or maybe
[guard0] [guard1] [guard2] task
This would provide a simple inline syntax for guard sequences, which are a viable alternative to a guard tree whose root is a sequence. As long as you use short guard sequences (for instance, 0 to 4 guards) this construct remains pretty readable. But there's nothing - apart from common-sense, of course - stopping you from guarding dozens of guards.
To keep the logic simple for the user (and the implementation simple for me LOL) I'd just impose the limitation that guards can't run over multiple ticks, meaning that they must terminate with SUCCEEDED
or FAILED
immediately while RUNNING
is an illegal status (should likely cause a runtime exception).
I got task guards and dynamic priorities working at the API level just like described above. Now the most boring part will be writing the parser. :expressionless:
Also, since any task can have a guard, I think that a specific task for staticPriority
is no longer necessary because now you can easily get the same result with selector
by guarding its children. In text format it would be something like that
selector
[guard1] task1
[guard2] task2
[guard3] task3
Since guards can be either something simple like a single task or something complex like a whole tree, I'm convinced that AI designers and developers will really benefit from such a feature. :bowtie:
Dropping by from the Reddit thread ...
Interesting that you got the guards working already. I'm not sure I explained the priorities idea properly though, so let's try again, this time with an example.
Boring old Java code stuff. First, a leaf task which supplies its own priority.
class PlayHappyAnimation extends LeafTask<MyBlackboard> {
@Override public Status execute() {
// Run the animation and so on
// Always succeed
return success();
}
// This one is new, defaults to returning 0
@Override public int getPriority() {
// Needs return values 1-10
return 100 / getObject().needForFood() / getObject().needForShelter() - 10;
}
}
This one calculates some priority for another task (or branch)
class NeedPriority implements TaskPrioritizer<MyBlackboard> {
@TaskAttribute
public String need;
@Override public int getPriority(Task task, MyBlackboard blackboard) {
switch( need ) {
case "food": return blackboard.needForFood();
case "shelter": return blackboard.needForShelter();
default: return Integer.MIN_VALUE;
}
}
}
Let's put it all together. Priorities are in parentheses before the tasks. Default priority is 0.
prioritySelector type:static runUntil:success
(NeedPriority need:food) sequence
# Search for food sequence here
(NeedPriority need:shelter) sequence
# Search for shelter sequence here
PlayHappyAnmation # returns its own priority
Now, if the priorities for food or shelter search are above those for playing a happy animation, the one with the bigger need (= priority) gets used. If the need for both isn't that great, the happy animation plays, and the whole task succeeds. If they are great, but fail, PlayHappyAnimation gets played anyway. The AI might be hungry and without a roof above their head, but it can at least still smile.
I see your point and I think that my proposal is a superset of yours :) For instance, you should be able to implement the desired behavior like that
class CheckPriorityCondition extends LeafTask<MyBlackboard> {
public enum Need {
Food, Shelter
};
@TaskAttribute(required=true) public Need need;
public CheckPriorityCondition () {
}
@Override public Status execute() {
return needs(getObject(), need) ? Status.SUCCEEDED : Status.FAILED;
}
// No @Override; this method only exists for this task
public boolean needs(MyBlackboard blackboard, Need need) {
// Use whatever formula you want and determine whether the given need is actually needed or not
}
}
dynamicGuardSelector
[checkPriority need:"food"] sequence
# Search for food sequence here
[checkPriority need:"shelter"] sequence
# Search for shelter sequence here
PlayHappyAnimation
What do you think?
Ok, so ...
The idea with the guards is roughly the same for a static tree, though the needs(...)
method gets quite a bit more complicated (it has to check for all the needs and compare them, not just the one passed).
And when you add another need, the formula needs to get updated as well, to pick whichever need is the strongest and if it is the one we're checking for, and if all the needs together are strong enough to not rather play a happy animation instead.
And then you try to add a need at runtime, which means that you suddenly have to write needs(...)
to be able to deal with a variable number of needs, not just an enum of them.
And then you decide to add another bunch of potential subtrees, each of them evaluating the needs and other information to decide on priority, and each needing to know the exact formula for priority all the other, potentially many and changing at runtime branches use.
Essentially: Replacing priorities (which each subtree can calculate for itself not caring what other subtrees are doing, or which subtrees there are in the first place) with guards requires that the guards are way more complicated and tightly coupled with each other.
Here's an idea how guards and priorities can work together to their strengths: A "planning selector".
Assume the selector has a bunch (more than 10, possibly more than 100) different possible branches. Assume we can also estimate the cost of executing each branch (either by querying each of them for the estimated cost, or via some external cost estimation class). The algorithm is thus:
runUntil:success
) Repeat until a branch returns with a success or running out of branches. Else (if runUntil:finish
) just return the result of the first branch.To deal with changes at runtime, should we just allow referencing callbacks/functors for attributes (with an additional paramater in the annotation as well)?
@davebaol great work :+1: I've been through some kind of crisis :smile: fortunately I'm alive again
@implicit-invocation
To deal with changes at runtime, should we just allow referencing callbacks/functors for attributes (with an additional paramater in the annotation as well)?
Sounds like a nice idea for future extensions.
I've been through some kind of crisis :smile: fortunately I'm alive again
Nice to hear it. Welcome back! :smiley:
@MartinSojka TBH I'm not sure I've fully understood your use case. Maybe I just need a more concrete example. Anyways, I've used enum just for the sake of clarity. There's nothing stopping you from using a HashMap or any other data structure (which you usually access to through the blackboard). Also, there are some aspects I really like in this solution:
SUCCEDED
and FAILED
i.e. true
and false
respectively). And, since tasks are written in code, there's virtually no limit to what you can do. You might even use a scripting language like Lua to let the AI designer specify the guard's logic (well, this goes for all tasks, not just guards).Done! Here is a working tree using guards, DynamicGuardSelector and subtree references. I'll update the wiki page after the end of the LibGDX Jam, I guess. :smiley:
@davebaol, sorry to hijack the thread, but how's the best way to do a "while" loop using behavior trees? I want to iterate over a list of possible targets and apply a selector over them. UntilFail and UntilSuccess won't resolve the problem since they can possibly end on a infinite loop.
@scooterman I don't see what's the problem with UntilFail/UntilSuccess.
untilFail
sequence
next
processItem
where
next
advances the iterator and stores current item into the blackboard. It fails if there is not a next element.processItem
does something with the current item taken from the blackboard. This is your selector or whatever.@davebaol thanks for the reply. If processItem does run and I return success (meaning that, for example, I can execute an attack on the selected item, hence making sequence return true) untilFail will exit? I can let the iteration end but the ideal (in my case) would be to stop right after a successful sequence.
@scooterman
No, untilFail
will exit only if the child sequence
fails. You can decorate processItem
with ìnvert
, alwaysFail
or alwaysSucceed
based on your needs.
great, thanks for the clarification.
in fact, it won't work @davebaol. take a look on this snippet:
subtree name:"seekAndDestroy"
sequence
untilSuccess # loop through every entity in range
sequence
nextEnemyInRange
canAttack?
attackEnemy # try to attack
Suppose I have iterated over all my enemies in range and haven't attacked any of them. How I'm supposed to exit this loop since selectEnemyInRange will return false and the condition too?
Also it would be interesting have something like a "nop" task that wouldn't interfere in the current processing result, so I could use nextEnemyInRange on both selectors and sequences without having to invert it according to the logic.
Well, you have to distinguish between end-of-iteration condition and in-range condition:
subtree name:"seekAndDestroy"
untilFail # loop through every entity
sequence
nextEntity # only fails if there's no next entity
alwaysSucceed
sequence
isEnemyInRange?
canAttack?
attackEnemy # try to attack
BTW, in your tree the first sequence
(the root of the subtree) is useless.
@davebaol sorry for annoying, if you prefere another conversation mechanism like irc just say :smile:
The problem with this approach is that I lose information on one thing: if my attack succeed or not for that specific entity. My blackboard is the brain for one entity, and this tree is to check if I can attack another one in range. The ideal for this subtree was to shortcut or break the result of attackEnemy returning SUCCESS if one of it managed to attack, otherwise iterate over the next entity until exhausted. If exhausted and no one managed to attack, the subtree should return FAIL meaning that no enemy was found to attack. I could add a check after alwaysSucceed and then one after the untilFail but that's not composable, say I decide to implement a new verification for a different kind of operation. Imo this is looking like I'm trying to program procedurally which it's not desirable, but I can't see another way to do that without iteration, specially when starting to compose behaviors.
This may be related to the priority discussion above, imo.
@scooterman
subtree name:"seekAndDestroy"
sequence
setFlag value:false
untilFail
sequence
isFlag? value:false
nextEntity # only fails if there's no next entity
alwaysSucceed
sequence
isEnemyInRange?
canAttack?
attackEnemy
setFlag value:true
isFlag? value:true
@davebaol I ended implementing an "exhaust" branch that simplified this case. Do you have any interest on an pull request for this?
@scooterman Sure, if it's general enough I'll merge it.
I don't know if it's the right place to post but I have some suggestions :
First of all I think current API is complete specially with guards and dynamic guard selector, I personally always found a way to implement logic with provided generic tasks without hacking a re-code things (nice job BTW).
My concern right now is about pooling strategy :
My conclusion is that pooling strategy is game specific : recycle whole trees, recycle tasks, don't recycle is a design choice. But we need a mechanism to allow individual tasks recycling though and I think using Poolable interface is the best choice we have.
@davebaol What do you think ? Did you faced this problem ? I could provide a PR if you don't have time to implement it but I need your approval first.
@mgsx-dev
@davebaol What do you think ? Did you faced this problem ? I could provide a PR if you don't have time to implement it but I need your approval first.
I've never needed to instantiate/destroy trees in game. I always do it during the initialization phase of the level, but I do recognize your use case. So, yes, PR is welcome. 😄
It seams impossible to use LibGDX Poolable interface with Task because of API conflicts (both reset methods have same signature but not the same contract). The only way I found so far would be to rename Task.reset to Task.resetTask and let Task implements Poolable interface. This change the current API.
Sounds good to me
there is no way to remove children from task in order to recycle. Note that possibility to remove children would be a useful feature for editors as well.
Yeah, this would be an interesting feature. Just notice that when you remove a child the parent task MUST update his own internal status accordingly. This operation is task-specific, of course.
My conclusion is that pooling strategy is game specific : recycle whole trees, recycle tasks, don't recycle is a design choice. But we need a mechanism to allow individual tasks recycling though and I think using Poolable interface is the best choice we have.
Couldn't agree more
I've opened this to discuss behavior trees API enhancements.
@implicit-invocation Resuming discussion https://github.com/libgdx/gdx-ai/pull/4#issuecomment-56509640 ... Not sure why you don't like the XML format, but I think that it has some advantages:
So I'm playing with the XML format just to see what can be done. Currently I can successfully load a behavior tree from this file:
I added the "Import" tag to improve readability. It allows you to use the given alias in place of the fully qualified class name of the task. Actually Selector, Parallel, etc.. are predefined imports. Also, the "as" attribute is optional, meaning that the simple class name is used as the alias, i.e.
creates the task alias "BarkTask". Also, I added task parameters, see urgentProb in CareTask and times in BarkTask. The attribute value is parsed according to the type of the corresponding field of the task class. For example, urgentProb is a float and times is an int. Supported types are: int, Integer, float, Float, boolean, Boolean, long, Long, double, Double, short, Short, char, Character, byte, Byte, and String.
Of course, we can maintain both formalisms as long as they have the same structural features. I mean, unlike task parameters, imports are just a syntactic sugar so they are not mandatory for the inline tab-based formalism.
I think we can use a "btree" branch in order to experiment with BT improvements while keeping the master branch clean.