How do Goal Selectors Work - Tslat/SmartBrainLib GitHub Wiki
Goals and Goal selectors are the current 'standard' of entity AI in Minecraft. It's the system most of us are already familiar with, and it's the simplest system we have.
Even with that being the case however, there's still some nuances in the way the GoalSelector system works that might be worth knowing for those who use it.
Usage
All Mobs are given two goal selectors by default:
- The primary goal selector. This selector is responsible for the main "AI" of the mob. Normally you would add all of your primary goals to this selector.
- The targeting goal selector. This selector is almost exclusively responsible for finding and setting the attack target of the mob. Normally you would add your anger, hurt by, and target goals to this selector.
Neither of these selectors are specialised, they are just two instances of the same thing so that different goals can be used without conflicting with one-another. Goals in one selector have no direct impact on goals in another selector.
You can add additional goal selectors if you wish, but you will need to tick them manually to make them function.
Goal selectors are instantiated in the entity's constructor, with the goals added in the registerGoals method, which is called at the end of the Mob constructor
Priorities
When adding a goal to a goal selector, it is paired with an int priority value. This value effectively declares your intended 'prioritisation' of goals when it comes to interrupting one that is running. It does not affect the order in which the goal is ticked or checked.
The only place it is used is when attempting to interrupt an existing running goal with another one yet to start. When this is happening, the goal with the lowest value will be prioritised.
Flags
Flags are the functional marking system of goal selectors that tell the selector what kind of functionality the goal handles. There are only four options available: MOVE, LOOK, JUMP, TARGET.
A goal can set any combination (or none) of these flags as relevant to itself via the setFlags method, in the constructor of the goal.
Interestingly, these flags actually serve two different purposes simultaneously:
- They tell the goal selector that when attempting to start a new goal, if any currently running goals have a flag that the new goal has, the new goal should not run.
- They tell the goal selector that if it has been told not to run any goals with a certain flag, then all currently running goals with that flag should be stopped, and any new goals with that flag should not be run.
Most people are aware of #1, but not a lot of people are aware of #2.
#2 is handled by control flags. These are flags set in GoalSelector#disableControlFlag that identify functions of AI that the should should not perform. By default, this is used by Minecraft to prevent entities moving if they have been leashed, are being ridden by another entity, or are in a boat. Interestingly, the same does not apply to Minecarts.
The control flags can be similarly removed via GoalSelector#enableControlFlag. This does not immediately restart any goals that had been previously stopped due to a control flag's activation.
Operation
Every second tick (starting on the 1st tick of the mob), in the mob's serverAiStep method, both goal selectors will have their tick method called.
The goal selector then does the following, in this order:
- Loop over all goals. Stop any goal via calling
stopif the goal meets the below criteria:- The goal is currently running, and;
- The goal either:
- Uses a control flag that has been disabled in the selector, or;
- Returns false to
canContinueToUse
- Loop over all currently running goals (even if they had been stopped in #1). If the goal's wrapper returns false to
isRunning(I.E. the goal is stopped); remove the control flag that goal is responsible for so that other goals can take its place - Loop over all goals. If the goal meets the below conditions, start the goal via
start, and stop any other goals that had matching control flags viastop:- The goal wrapper returns false to
isRunning(I.E. the goal is stopped), and; - The goal doesn't use a control flag that has been disabled, and;
- The goal returns true to
canUse, and; - The goal either:
- Doesn't use any control flags that currently running goals also use, or;
- All currently running goals that use control flags that the goal also uses return true to
isInterruptibleand have a higher priority value than the goal
- The goal wrapper returns false to
- Loop over all goals. If the goal is running, tick it via
tick
Every other tick (starting on the 2nd tick of the mob if the mob's numeric ID is an even number), both goal selectors instead have their tickRunningGoals method called directly. In this tick, only goals that return true in their requiresUpdateEveryTick method will be ticked.
Functionally this means that goals will only tick every second tick if requiresUpdateEveryTick returns true. This is done to reduce CPU cost of goals.
Flowchart
flowchart TD
A{Mob.serverAiStep}
B(WrappedGoal.stop)
C(WrappedGoal.tick)
subgraph GoalSelector.goalCanBeReplacedForAllFlags
A2(Loop over WrappedGoal.getFlags) --> B2[Get goal from flag->goal map]
end
subgraph GoalSelector.goalContainsAnyFlags
A3(Loop over WrappedGoal.getFlags) --> B3[Flag set contains matching flag?]
end
subgraph WrappedGoal.canBeReplacedBy
A4[WrappedGoal.isInterruptible?] -.-> B4[WrappedGoal.getPriority lower than matched goal?]
end
subgraph GoalSelector.tick
subgraph Step-1
A5(Loop over GoalSelector.availableGoals) --> B5[WrappedGoal.isRunning?]
end
subgraph Step-2
D5(Loop over GoalSelector.lockedFlags) --> E5[!WrappedGoal.isRunning?]
E5 -.-> F5(Remove from GoalSelector.lockedFlags)
end
subgraph Step-3
G5(Loop over GoalSelector.availableGoals) --> H5[!WrappedGoal.isRunning?]
H5 -.-> |For GoalSelector.disabledFlags| I5[!GoalSelector.goalContainsAnyFlags?]
I5 -.-> |For GoalSelector.lockedFlags| J5[GoalSelector.goalCanBeReplacedForAllFlags?]
J5 -.-> K5[WrappedGoal.canUse?]
K5 -.-> L5(Loop over WrappedGoal.getFlags)
L5 --> M5(Add new goal to GoalSelector.lockedFlags)
K5 -.-> N5(WrappedGoal.start)
end
end
subgraph GoalSelector.tickRunningGoals
A6(Loop over GoalSelector.activeGoals) --> B6[WrappedGoal.isRunning?]
end
A --> |If Entity.tickCount + Entity.id is odd| GoalSelector.tickRunningGoals
A --> |If first tick or Entity.tickCount + Entity.id is even| GoalSelector.tick
B5 -.-> |If GoalSelector.goalContainsAnyFlags for disabledFlags| B
B5 -.-> |If !WrappedGoal.canContinueToUse| B
L5 --> |For matching goal in GoalSelector.lockedFlags| B
Step-3 --> GoalSelector.tickRunningGoals
J5 <--> GoalSelector.goalCanBeReplacedForAllFlags
I5 <--> GoalSelector.goalContainsAnyFlags
GoalSelector.goalCanBeReplacedForAllFlags --> WrappedGoal.canBeReplacedBy
GoalSelector.tickRunningGoals -.-> |Called by GoalSelector.tick?| C
GoalSelector.tickRunningGoals -.-> |WrappedGoal.requiresUpdateEveryTick?| C
Step-1 --> Step-2 --> Step-3
Additional
There are a few additional things to note about goal selectors.
Adding and Removing Goals
GoalSelectors can have goals dynamically added or removed via addGoal and removeGoal respectively.
Add goal is a simple addition to the current goals set in the selector.
Remove goal loops over all goals in the selector and checks for reference parity (I.E. must be the same instance of the goal you're trying to remove), stopping it if it's currently running, then looping again and removing it.
WrappedGoals
When a goal is added to the goalselector's goals set, it is first wrapped in an instance of WrappedGoal. This is a simple wrapper class that passes on most of the goal's functional methods, and additionally containing the goal's priority.
This doesn't really impact modders much, but it is worth noting that the isRunning method that the goalselector uses is part of this wrapper class, and not the goal itself.
Goal Iteration
When the goal selector loops over goals for starting/stopping/ticking, the iteration order is in order of insertion. This means that setting a lower priority does not mean that your goal will be ticked first. There is no native way to insert a goal to be iterated before any other after the initial instantiation of the selector, so if you are adding a goal after instantiation of the entity, you can guarantee your goal will be checked more or less last.