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:

  1. 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.
  2. 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:

  1. 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.
  2. 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:

  1. Loop over all goals. Stop any goal via calling stop if 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
  2. 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
  3. 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 via stop:
    • 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 isInterruptible and have a higher priority value than the goal
  4. 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.