Skip to content

Instantly share code, notes, and snippets.

@ItsBlackGear
Last active July 25, 2023 02:22
Show Gist options
  • Save ItsBlackGear/c1be578b5bf8fc3f1f91b4cd66330d95 to your computer and use it in GitHub Desktop.
Save ItsBlackGear/c1be578b5bf8fc3f1f91b4cd66330d95 to your computer and use it in GitHub Desktop.

Brain AI!

Brain is a new system that has been implemented in the Village & Pillage update to expand the AI for certain mobs such as the Villager. nowadays, the entities using Brain are: Villager, Piglin, Axolotl, Goat, Tadpole, Frog, Warden and Camel.

it's an interesting system because unlike the goal system, it works using activities, sensors and memories! While goals run using tasks and priorities, Brain runs different behaviors for each activity.

Each Activity has defined behaviors that will run while the activity is active, these can be defined with memories so the entity knows the conditions to run certain tasks.

Activities

An Activity is a holder that stores the behaviors of the entity, an entity can run multiple activities and will run specific behaviors for each activity.

There's an special activity called CORE, which main function is to run their behaviors all the time. Unlike other activities, this one is always active.

public static final Activity LONG_JUMP = register("long_jump");
    
private static Activity register(String key) {
    // the access to Activity may require Access Widener/Transformer or a Mixin Accessor
    return Registry.register(Registry.ACTIVITY, key, new Activity(key));
}

doesn't do anything by themselves, because we have to register them on the brain!

// initialization of the CORE activity (from GoatAI)
private static void initCoreActivity(Brain<Goat> brain) {
    // adding an activity with behaviors in order of list.
    brain.addActivity(
        // Activity to be registered
        Activity.CORE,
        // Start of the priority list
        0,
        // Behaviors to be added
        ImmutableList.of(
            // each behavior runs in order of list, swimming being 0 and ram cooldown ticks being 6
            new Swim(0.8F),
            new AnimalPanic(2.0F),
            new LookAtTargetSink(45, 90),
            new MoveToTargetSink(),
            new CountDownCooldownTicks(MemoryModuleType.TEMPTATION_COOLDOWN_TICKS),
            new CountDownCooldownTicks(MemoryModuleType.LONG_JUMP_COOLDOWN_TICKS),
            new CountDownCooldownTicks(MemoryModuleType.RAM_COOLDOWN_TICKS)
        )
    );
}

// initialization of the IDLE activity (from TadpoleAI)
private static void initIdleActivity(Brain<Tadpole> brain) {
    // similar to the previous one, adds an activity with behaviors but at customizable order
    brain.addActivity(
        // Activity
        Activity.IDLE,
        // Behaviors
        ImmutableList.of(
            // Pair of { Priority, Behavior }
            Pair.of(0, new RunSometimes(new SetEntityLookTarget(EntityType.PLAYER, 6.0F), UniformInt.of(30, 60))),
            Pair.of(1, new FollowTemptation(entity -> 1.25F)),
            // GateBehavior executes a list of behaviors with custom settings
            Pair.of(2, new GateBehavior(
                // Condition
                ImmutableMap.of(MemoryModuleType.WALK_TARGET, MemoryStatus.VALUE_ABSENT), 
                // Removed memories on exit
                ImmutableSet.of(),
                // Determines the order of the behaviors to run: ORDERED, SHUFFLED
                GateBehavior.OrderPolicy.ORDERED,
                // Determines the amount of behaviors to run: RUN_ONE, TRY_ALL
                GateBehavior.RunningPolicy.TRY_ALL,
                // Behaviors to execute
                ImmutableList.of(
                    // Pair of { Behavior, Priority }
                    Pair.of(new RandomSwim(0.5F), 2), 
                    Pair.of(new SetWalkTargetFromLookTarget(0.5F, 3), 3), 
                    Pair.of(new RunIf(Entity::isInWaterOrBubble, new DoNothing(30, 60)), 5))
                )
            )
        )
    );
}

// initialization of the LONG_JUMP activity (from GoatAI)
private static void initLongJumpActivity(Brain<Goat> brain) {
    // adding an activity with conditions
    brain.addActivityWithConditions(
        // Activity to be registered
        Activity.LONG_JUMP,
        // Behaviors!
        ImmutableList.of(
            // Pair of { Priority, Behavior }
            Pair.of(0, new LongJumpMidJump(TIME_BETWEEN_LONG_JUMPS, SoundEvents.GOAT_STEP)),
            Pair.of(1, new LongJumpToRandomPos<>(TIME_BETWEEN_LONG_JUMPS, 5, 5, 1.5F, goat -> {
                return goat.isScreamingGoat() ? SoundEvents.GOAT_SCREAMING_LONG_JUMP : SoundEvents.GOAT_LONG_JUMP;
            }))
        ),
        // Conditions
        ImmutableSet.of(
            // Pair of { Memory, Status }
            // MemoryStatus can check if the memories are: Value Present, Value Absent or Registered
            Pair.of(MemoryModuleType.TEMPTING_PLAYER, MemoryStatus.VALUE_ABSENT),
            Pair.of(MemoryModuleType.BREED_TARGET, MemoryStatus.VALUE_ABSENT),
            Pair.of(MemoryModuleType.WALK_TARGET, MemoryStatus.VALUE_ABSENT),
            Pair.of(MemoryModuleType.LONG_JUMP_COOLDOWN_TICKS, MemoryStatus.VALUE_ABSENT)
        )
    );
}

// initialization for TONGUE activity (from FrogAI)
private static void initTongueActivity(Brain<Frog> brain) {
    // adds an Activity that's going to remove a Memory once finished
    brain.addActivityAndRemoveMemoryWhenStopped(
        // Activity
        Activity.TONGUE, 
        // Priority Start
        0, 
        // Behaviors in priority order
        ImmutableList.of(
            new StopAttackingIfTargetInvalid(), 
            new ShootTongue(SoundEvents.FROG_TONGUE, SoundEvents.FROG_EAT)
        ), 
        // Memory that's removed after the Activity stops
        MemoryModuleType.ATTACK_TARGET
    );
}

// initialization for PLAY_DEAD activity (from AxolotlAI)
private static void initPlayDeadActivity(Brain<Axolotl> brain) {
    // adds an Activity that's going to remove a list of Memories once finished, it also take conditions
    brain.addActivityAndRemoveMemoriesWhenStopped(
        Activity.PLAY_DEAD, 
        // Behaviors
        ImmutableList.of(
            // Pair of { Priority, Behavior }
            Pair.of(0, new PlayDead()), 
            // EraseMemoryIf checks for a condition to remove a memory
            Pair.of(1, new EraseMemoryIf(BehaviorUtils::isBreeding, MemoryModuleType.PLAY_DEAD_TICKS))
        ), 
        // Conditions
        ImmutableSet.of(
            // Pair of { Memory, Status }
            Pair.of(MemoryModuleType.PLAY_DEAD_TICKS, MemoryStatus.VALUE_PRESENT)
        ), 
        // Memories removed when stopped
        ImmutableSet.of(MemoryModuleType.PLAY_DEAD_TICKS)
    );
}

Behaviors

A Behavior is task that's defined by a priority and an Activity

Behaviors also run with some conditions that must be met in order to execute, also you can define the durability of the behavior but it's defined as 60 seconds by default.

Example From BabyFollowAdult

// Takes as argument an entity that extends AgeableMob
public class BabyFollowAdult<E extends AgableMob> extends Behavior<E> {
    private final UniformInt followRange;
    private final float speedModifier;

    public BabyFollowAdult(UniformInt followRange, float speedModifier) {
        // Behaviors also have conditions to be executed
        super(
            ImmutableMap.of(
                MemoryModuleType.NEAREST_VISIBLE_ADULT, MemoryStatus.VALUE_PRESENT,
                MemoryModuleType.WALK_TARGET, MemoryStatus.VALUE_ABSENT
            )
        );
        this.followRange = followRange;
        this.speedModifier = speedModifier;
    }

    // Behaviors have an internal condition method as well, not limited to Memories
    @Override
    protected boolean checkExtraStartConditions(ServerLevel level, E entity) {
        // If the entity is a baby, it will start to follow as long as it is in range
        if (!entity.isBaby()) {
            return false;
        } else {
            AgableMob adult = this.getNearestAdult(entity);
            return entity.closerThan(adult, this.followRange.getMaxValue() + 1) && !entity.closerThan(adult, this.followRange.getMinValue());
        }
    }

    // Executes the contents of this method once the Behavior has started
    @Override
    protected void start(ServerLevel level, E entity, long gameTime) {
        // Looks at the nearest adult and starts walking towards it
        BehaviorUtils.setWalkAndLookTargetMemories(entity, this.getNearestAdult(entity), this.speedModifier, this.followRange.getMinValue() - 1);
    }

    private AgableMob getNearestAdult(E entity) {
        return entity.getBrain().getMemory(MemoryModuleType.NEAREST_VISIBLE_ADULT).get();
    }
}

but don't get limited to only that! Behaviors have useful methods with many uses

public class CustomBehavior extends Behavior<LivingEntity> {
    // Behaviors require 3 parameters: Condition, Min Duration and Max Duration
    public CustomBehavior(Map<MemoryModuleType<?>, MemoryStatus> conditions, int minDuration, int maxDuration) {
        super(conditions, minDuration, maxDuration);
    }

    // You can also define a Behavior with fixed duration!
    public CustomBehavior(Map<MemoryModuleType<?>, MemoryStatus> conditions, int duration) {
        super(conditions, duration);
    }

    // If no duration is specified, it will use the default; 60 seconds
    public CustomBehavior(Map<MemoryModuleType<?>, MemoryStatus> conditions) {
        super(conditions);
    }

    // You can also create a Behavior with no conditions!
    public CustomBehavior() {
        super(ImmutableMap.of());
    }

    // Checks for more specific conditions to validate the execution of the Behavior
    @Override
    protected boolean checkExtraStartConditions(ServerLevel level, LivingEntity entity) {
        return super.checkExtraStartConditions(level, entity);
    }

    // Executes the contents of this method once the Behavior has started
    @Override
    protected void start(ServerLevel level, LivingEntity entity, long gameTime) {
    }

    // Executes the contents of this method for each tick while the Behavior is running
    @Override
    protected void tick(ServerLevel level, LivingEntity owner, long gameTime) {
    }

    // Checks if the Behavior should continue running
    @Override
    protected boolean canStillUse(ServerLevel level, LivingEntity entity, long gameTime) {
        return super.canStillUse(level, entity, gameTime);
    }

    // Executes the contents of this method once the Behavior has stopped
    @Override
    protected void stop(ServerLevel level, LivingEntity entity, long gameTime) {
    }

    // Checks if the Behavior timestamp has reached the max duration and can no longer run
    @Override
    protected boolean timedOut(long gameTime) {
        return super.timedOut(gameTime);
    }
}

Memory Modules

Memory Modules are the ultimate controls for Brain and the ones that tell the entity what to do.

Memory Modules must be registered in order to be used

// Memories can be registered with both an Empty value, or an Expirable Value
public static final MemoryModuleType<Boolean> LONG_JUMP_MID_JUMP = register("long_jump_mid_jump");
// Expirable values will run for a certain amount of time before turning into empty ones, these are useful for cooldown memories and such!
public static final MemoryModuleType<Boolean> HAS_HUNTING_COOLDOWN = register("has_hunting_cooldown", Codec.BOOL);

private static <U> MemoryModuleType<U> register(String key, Codec<U> codec) {
    // Depending of the version, the access to MemoryModuleType may require an Access Widener/Transformer or a Mixin Accessor
    return Registry.register(Registry.MEMORY_MODULE_TYPE, new ResourceLocation(key), new MemoryModuleType(Optional.of(codec)));
}

private static <U> MemoryModuleType<U> register(String key) {
    return Registry.register(Registry.MEMORY_MODULE_TYPE, new ResourceLocation(key), new MemoryModuleType(Optional.empty()));
}

memories can be handled in many different ways

from AnimalMakeLove

// for both entities, sets the each other to be a breeding partner
protected void start(ServerLevel level, Animal animal, long gameTime) {
    Animal partner = this.findValidBreedPartner(animal).get();
    animal.getBrain().setMemory(MemoryModuleType.BREED_TARGET, partner);
    partner.getBrain().setMemory(MemoryModuleType.BREED_TARGET, animal);
    ...
}

private boolean hasBreedTargetOfRightType(Animal animal) {
    Brain<?> brain = animal.getBrain();
    // checks if the entity brain contains a breed target and checks if the breed target equals the partner entity type.
    brain.hasMemoryValue(MemoryModuleType.BREED_TARGET) && brain.getMemory(MemoryModuleType.BREED_TARGET).get().getType() == this.partnerType;
}

but there's more than just setting, getting and checking memories!

from Roar

protected void start(ServerLevel level, Warden warden, long gameTime) {
    Brain<Warden> brain = warden.getBrain();
    // wardens will delay the sound for 25 ticks before actually ROARING
    brain.setMemoryWithExpiry(MemoryModuleType.ROAR_SOUND_DELAY, Unit.INSTANCE, 25L);
    ...
}

Sensors

Sensors are functions that run independently of Activities, but they do require a condition in order to run

Similar to memory modules, Sensors must be registered to be used.

public static final SensorType<IsInWaterSensor> IS_IN_WATER = register("is_in_water", IsInWaterSensor::new);
  
private static <U extends Sensor<?>> SensorType<U> register(String key, Supplier<U> factory) {
    // the access to SensorType may require Access Widener/Transformer or a Mixin Accessor
    return Registry.register(Registry.SENSOR_TYPE, new ResourceLocation(key), new SensorType(factory));
}

From IsInWaterSensor

// This is a simple example of what can we do with Sensors
public class IsInWaterSensor extends Sensor<LivingEntity> {
    // Checks if the memories required to run are registered
    @Override
    public Set<MemoryModuleType<?>> requires() {
        // Checks if the memory IS_IN_WATER is registered to start running
        return ImmutableSet.of(MemoryModuleType.IS_IN_WATER);
    }

    // Starts ticking the sensor once the requirements were validated.
    @Override
    protected void doTick(ServerLevel level, LivingEntity entity) {
        // While the entity is in water, it will set the memory as present, otherwise it will remove the memory
        if (entity.isInWater()) {
            entity.getBrain().setMemory(MemoryModuleType.IS_IN_WATER, Unit.INSTANCE);
        } else {
            entity.getBrain().eraseMemory(MemoryModuleType.IS_IN_WATER);
        }
    }
}

after setting up the Sensor, you can initialize it on the entity!

protected static final ImmutableList<SensorType<? extends Sensor<? super Frog>>> SENSORS = ImmutableList.of(
    SensorType.NEAREST_LIVING_ENTITIES, 
    SensorType.HURT_BY, 
    SensorType.FROG_ATTACKABLES, 
    SensorType.FROG_TEMPTATIONS, 
    SensorType.IS_IN_WATER
);

@Override
protected Brain.Provider<Frog> brainProvider() {
    return Brain.provider(MEMORIES, SENSORS);
}

Brain instancing

after covering the basics, we're ready to start developing our own brain instance, for that we'll have to create a new class where we'll store our AI

From GoatAI

// initializes the cooldown ticks for LONG JUMPING and RAMMING using a random value
// OPTIONAL!!! you won't need this if your entity doesn't initialize with behaviors with cooldown or similar.
protected static void initMemories(Goat goat, Random random) {
    // gets the brain and sets a value to the memory, sampling a random value (IntProvider / FloatProvider)
    goat.getBrain().setMemory(MemoryModuleType.LONG_JUMP_COOLDOWN_TICKS, TIME_BETWEEN_LONG_JUMPS.sample(random));
    goat.getBrain().setMemory(MemoryModuleType.RAM_COOLDOWN_TICKS, TIME_BETWEEN_RAMS.sample(random));
}

// initializes the brain for the entity
protected static Brain<?> makeBrain(Brain<Goat> brain) {
    // determine behaviors for each activity
    initCoreActivity(brain);
    initIdleActivity(brain);
    initLongJumpActivity(brain);
    initRamActivity(brain);
    // determines the core activities, which are going to be always run
    brain.setCoreActivities(ImmutableSet.of(Activity.CORE));
    // determines and executes the activity that's going to be running after spawn.
    brain.setDefaultActivity(Activity.IDLE);
    brain.useDefaultActivity();
    return brain;
}
  
// initializes the behaviors for the CORE activity
private static void initCoreActivity(Brain<Goat> brain) {
    // determines the behaviors for a specific activity, selects a priority to start running each behavior in order.
    brain.addActivity(
        // Activity
        Activity.CORE,
        // Priority Start, priorities just determine which behaviors have preference over the others
        0,
        // Behaviors / Tasks
        ImmutableList.of(
            // priority: 0
            new Swim(0.8F),
            // priority: 1
            new AnimalPanic(2.0F),
            // priority: 2
            new LookAtTargetSink(45, 90),
            // priority: 3
            new MoveToTargetSink(),
            // priority: 4
            new CountDownCooldownTicks(MemoryModuleType.TEMPTATION_COOLDOWN_TICKS),
            // priority: 5
            new CountDownCooldownTicks(MemoryModuleType.LONG_JUMP_COOLDOWN_TICKS),
            // priority: 6
            new CountDownCooldownTicks(MemoryModuleType.RAM_COOLDOWN_TICKS)
        )
    );
}

// initializes the behaviors for the IDLE activity
private static void initIdleActivity(Brain<Goat> brain) {
    // determines the behaviors for a specific activity, takes conditions required to run this activity.
    brain.addActivityWithConditions(
        // Activity
        Activity.IDLE,
        // Behaviors / Tasks
        ImmutableList.of(
            // Pair of { Priority, Behavior }
            // each pair counts as a behavior that runs with a certain priority
            // RunSometimes will take a behavior and execute at certain interval determined by an UniformInt
            Pair.of(0, new RunSometimes<>(new SetEntityLookTarget(EntityType.PLAYER, 6.0F), UniformInt.of(30, 60))),
            Pair.of(0, new AnimalMakeLove(EntityTypes.GOAT, 1.0F)),
            Pair.of(1, new FollowTemptation(entity -> 1.25F)),
            Pair.of(2, new BabyFollowAdult<>(ADULT_FOLLOW_RANGE, 1.25F)),
            // RunOne will take one of many behaviors and execute it.
            Pair.of(3, new RunOne<>(ImmutableList.of(
                // Pair of { Behavior, Priority }
                Pair.of(new RandomStroll(1.0F), 2),
                Pair.of(new SetWalkTargetFromLookTarget(1.0F, 3), 2),
                // DoNothing will prevent the entity from executing any task and idle for a certain amount of time
                Pair.of(new DoNothing(30, 60), 1))))
        ),
        // Conditions, if these aren't met then the activity won't be used.
        ImmutableSet.of(
            Pair.of(MemoryModuleType.RAM_TARGET, MemoryStatus.VALUE_ABSENT),
            Pair.of(MemoryModuleType.LONG_JUMP_MID_JUMP, MemoryStatus.VALUE_ABSENT)
        )
    );
}
    
// Determines the activity that's going to be used if valid 
// conditional activities must meet the requirements to be valid
public static void updateActivity(Goat goat) {
    goat.getBrain().setActiveActivityToFirstValid(ImmutableList.of(Activity.RAM, Activity.LONG_JUMP, Activity.IDLE));
}

Brain Initialization

we've created so far our brain instance, but we must initialize it in our entity class in order to use it.

From Goat

// determines the list of sensors for the entities to execute
protected static final ImmutableList<SensorType<? extends Sensor<? super Goat>>> SENSORS = ImmutableList.of(
    SensorType.NEAREST_LIVING_ENTITIES, 
    SensorType.NEAREST_PLAYERS, 
    SensorType.NEAREST_ITEMS, 
    SensorType.NEAREST_ADULT, 
    SensorType.HURT_BY, 
    SensorType.GOAT_TEMPTATIONS
);
// determines the list of memories for the entities to execute
protected static final ImmutableList<MemoryModuleType<?>> MEMORIES = ImmutableList.of(
    MemoryModuleType.LOOK_TARGET, 
    MemoryModuleType.VISIBLE_LIVING_ENTITIES, 
    MemoryModuleType.WALK_TARGET, 
    MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE, 
    MemoryModuleType.PATH, 
    MemoryModuleType.ATE_RECENTLY, 
    MemoryModuleType.BREED_TARGET, 
    MemoryModuleType.LONG_JUMP_COOLDOWN_TICKS, 
    MemoryModuleType.LONG_JUMP_MID_JUMP, 
    MemoryModuleType.TEMPTING_PLAYER, 
    MemoryModuleType.NEAREST_VISIBLE_ADULT, 
    MemoryModuleType.TEMPTATION_COOLDOWN_TICKS, 
    MemoryModuleType.IS_TEMPTED, 
    MemoryModuleType.RAM_TARGET, 
    MemoryModuleType.IS_PANICKING
);

// creates a specific brain provider that gathers the MEMORIES and SENSORS.
@Override
protected Brain.Provider<Goat> brainProvider() {
    return Brain.provider(MEMORIES, SENSORS);
}

// creates the brain instance for the entity, using the class that we just created for it
@Override
protected Brain<?> makeBrain(Dynamic<?> dynamic) {
    return GoatAi.makeBrain(this.brainProvider().makeBrain(dynamic));
}

// casts the brain to be used properly
@Override
public Brain<Goat> getBrain() {
    return (Brain<Goat>)super.getBrain();
}

@Override
protected void customServerAiStep() {
    this.level.getProfiler().push("goatBrain");
    // start ticking the brain! just like turning on the engine.
    this.getBrain().tick((ServerLevel)this.level, this);
    this.level.getProfiler().popPush("goatActivityUpdate");
    // update the activities depending on their validation.
    GoatAi.updateActivity(this);
    this.level.getProfiler().pop();
    super.customServerAiStep();
}

@Override
public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType reason, @Nullable SpawnGroupData spawnData, @Nullable CompoundTag dataTag) {
    // initialize the cooldown tick memories for RAMMING and LONG JUMPING after spawn, remember that this is OPTIONAL
    GoatAi.initMemories(this, random);
    ...
}

@Nullable @Override
public AgableMob getBreedOffspring(ServerLevel level, AgableMob mate) {
    Goat goat = EntityTypes.GOAT.create(level);
    if (goat != null) {
        // initialize the cooldown tick memories for RAMMING and LONG JUMPING after spawn, remember that this is OPTIONAL
        GoatAi.initMemories(goat, level.getRandom());
    ...
}

// If you are wondering that's this used for, it's for gametesting purposes, mojang developers use this to debug their entities
// but for default users, the method is empty so there's not really an use to us on using this... 
@Override
protected void sendDebugPackets() {
    super.sendDebugPackets();
    DebugPackets.sendEntityBrain(this);
}

also if you're wondering, here's a video on how mojang developers debug their entities with brain like Piglins, not relevant but still cool to watch: https://www.youtube.com/watch?v=unMIhRivDWQ

piglins show above their head the current activities that they are running, CORE is constant while AVOID, FIGHT and IDLE change depending on the conditions.

now it's your turn!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment