Skip to Content
DocsSystemsBehavior System

Overview

The behavior system in Better Ecology consists of multiple interconnected components:

  • Goal-based AI: Minecraft’s goal selector system for decision-making
  • Steering behaviors: Craig Reynolds’ boids algorithm for movement
  • Behavior handles: Modular subsystems that register and manage goals
  • Data-driven configuration: All parameters controlled via JSON/YAML

The system bridges the gap between scientific animal behavior research and Minecraft’s entity AI system.

Goal-Based AI System

Better Ecology uses Minecraft’s existing Goal system to implement behaviors. Each behavior is a goal that can be activated, deactivated, and prioritized against other goals.

Goal Registration

Goals are registered via handles during entity initialization:

public class ForagingHandle implements EcologyHandle { @Override public void registerGoals(Mob mob, GoalSelector goalSelector, GoalSelector targetSelector) { // Get configuration values float hungerThreshold = profile.getFloat("foraging.hunger_threshold", 50f); float searchRadius = profile.getFloat("foraging.search_radius", 8f); // Register foraging goal at priority 5 goalSelector.addGoal(5, new SeekFoodGoal(mob, hungerThreshold, searchRadius)); } }

How registration works:

  1. Entity spawns or is loaded from disk
  2. MobEcologyMixin calls EcologyHooks.onRegisterGoals()
  3. Each handle that supports the entity’s profile registers its goals
  4. Goals are added to the entity’s goal selector with priorities
  5. Minecraft’s AI system runs goals based on priority and conditions

Goal Priority System

Goals are executed in priority order (lower number = higher priority):

PriorityPurposeExamples
0SurvivalFloatGoal (swimming)
1-2Critical needsPanic, flee from predators, mother protecting baby
3-4ImportantHunting prey, seeking food when starving
5-6NormalGrazing, rooting, pecking, breeding
7-8Low prioritySocial behaviors, wandering, resting
9-10IdleRandom looking, ambient sounds

Example priority configuration:

// Sheep goal priority list goalSelector.addGoal(0, new FloatGoal(sheep)); // Survival goalSelector.addGoal(1, new FleeFromPredatorGoal(sheep)); // Critical goalSelector.addGoal(2, new MotherProtectBabyGoal(sheep)); // Critical goalSelector.addGoal(5, new SheepGrazeGoal(sheep)); // Normal goalSelector.addGoal(6, new HerdCohesionGoal(sheep)); // Normal goalSelector.addGoal(7, new WanderGoal(sheep)); // Low priority

Creating Custom Goals

Goals extend Minecraft’s Goal class with lifecycle methods:

public class SheepGrazeGoal extends Goal { private final Sheep sheep; private BlockPos targetGrass; private int eatTimer; public SheepGrazeGoal(Sheep sheep) { this.sheep = sheep; // MOVE flag prevents simultaneous movement goals this.setFlags(EnumSet.of(Goal.Flag.MOVE)); } /** * Determines if this goal can start executing. * Called every tick until it returns true. */ @Override public boolean canUse() { // Only activate if hungry if (!AnimalNeeds.isHungry(sheep)) { return false; } // Only activate if grass is nearby this.targetGrass = findNearbyGrass(); return this.targetGrass != null; } /** * Determines if this goal should continue executing. * Called every tick while the goal is active. */ @Override public boolean canContinueToUse() { // Stop if no longer hungry if (!AnimalNeeds.isHungry(sheep)) { return false; } // Stop if reached grass and finished eating return eatTimer < 40; // 2 seconds } /** * Called once when the goal starts executing. */ @Override public void start() { // Navigate to grass block sheep.getNavigation().moveTo( targetGrass.getX(), targetGrass.getY(), targetGrass.getZ(), 1.0 // Speed modifier ); eatTimer = 0; } /** * Called every tick while the goal is active. */ @Override public void tick() { // Check if reached grass if (sheep.blockPosition().equals(targetGrass)) { eatTimer++; // Play eating animation every 10 ticks if (eatTimer % 10 == 0) { AnimalAnimations.playGrazingAnimation(sheep); } // Consume grass and restore hunger if (eatTimer >= 40) { consumeGrass(); AnimalNeeds.modifyHunger(sheep, 20f); // Restore 20 hunger } } } /** * Called once when the goal stops executing. */ @Override public void stop() { // Clean up state sheep.getNavigation().stop(); targetGrass = null; eatTimer = 0; } private BlockPos findNearbyGrass() { // Implementation: Search for grass blocks within radius // See: ForagingBehavior.findFoodSource() } private void consumeGrass() { // Remove grass block or mark as depleted // Based on: Marginal Value Theorem (Charnov, 1976) } }

Goal flags:

  • MOVE: Prevents multiple movement goals from conflicting
  • LOOK: Prevents multiple look-at goals from conflicting
  • JUMP: Prevents multiple jump goals from conflicting
  • TARGET: Prevents multiple targeting goals from conflicting

Steering Behaviors

Steering behaviors control how animals move. These are based on Craig Reynolds’ 1987 “Boids” algorithm and expanded with additional behaviors.

Core Steering Behaviors

BehaviorPurposeResearch Basis
SeparationAvoid crowding neighborsReynolds (1987), Couzin et al. (2002)
AlignmentMatch heading with neighborsReynolds (1987), Ballerini et al. (2008)
CohesionMove toward group centerReynolds (1987), Hemelrijk & Hildenbrandt (2008)
FleeMove away from threatsYdenberg & Dill (1986)
SeekMove toward targetsOptimal foraging theory
WanderRandom explorationLevy flight patterns

Implementing Steering Behaviors

Steering behaviors calculate velocity adjustments:

public class FlockCohesionGoal extends Goal { private final Animal animal; private final float cohesionWeight; private final int topologicalNeighbors; @Override public void tick() { // Get nearby animals of the same type List<Animal> neighbors = findTopologicalNeighbors( animal, topologicalNeighbors // Typically 6-7 (Ballerini et al., 2008) ); if (neighbors.isEmpty()) { return; } // Calculate center of mass Vec3 centerOfMass = calculateCenterOfMass(neighbors); // Calculate steering force toward center Vec3 desiredVelocity = centerOfMass.subtract(animal.position()) .normalize() .scale(animal.getSpeed()); Vec3 steering = desiredVelocity.subtract(animal.getDeltaMovement()); // Apply weighted steering force Vec3 force = steering.scale(cohesionWeight); // Update entity velocity animal.setDeltaMovement( animal.getDeltaMovement().add(force.scale(0.1)) ); } /** * Find N nearest neighbors (topological interaction). * Based on: Ballerini et al., "Interaction ruling animal collective behavior * depends on topological rather than metric distance" (2008) */ private List<Animal> findTopologicalNeighbors(Animal animal, int count) { return animal.level() .getEntitiesOfClass( animal.getClass(), animal.getBoundingBox().inflate(16.0), // Search radius e -> e != animal ) .stream() .sorted(Comparator.comparingDouble(e -> e.distanceTo(animal))) .limit(count) .toList(); } }

Combining Steering Behaviors

Multiple behaviors are combined using weighted sums:

public class FlockingGoal extends Goal { private final Animal animal; private final float separationWeight; private final float alignmentWeight; private final float cohesionWeight; @Override public void tick() { List<Animal> neighbors = findTopologicalNeighbors(animal, 7); // Calculate individual steering forces Vec3 separation = calculateSeparation(neighbors); Vec3 alignment = calculateAlignment(neighbors); Vec3 cohesion = calculateCohesion(neighbors); // Combine with weights Vec3 totalSteering = Vec3.ZERO .add(separation.scale(separationWeight)) // 1.8 .add(alignment.scale(alignmentWeight)) // 1.0 .add(cohesion.scale(cohesionWeight)); // 1.5 // Normalize and apply Vec3 force = totalSteering.normalize().scale(0.1); animal.setDeltaMovement(animal.getDeltaMovement().add(force)); } }

Weight tuning:

  • separation > cohesion: Loose flocks (birds in flight)
  • cohesion > separation: Tight groups (schooling fish)
  • alignment high: Synchronized movement (starling murmurations)

See: Flocking Research for parameter derivation.

Behavior Implementation Packages

Flocking (behavior/flocking/)

Implements boids algorithm for bird-like flocking:

Key features:

  • Topological neighbor tracking (6-7 neighbors)
  • Separation, alignment, cohesion forces
  • V-formation for energy efficiency
  • Scale-free velocity correlation (Cavagna et al., 2010)

Configuration:

{ "flocking": { "enabled": true, "topological_neighbors": 7, "separation": 1.8, "alignment": 1.0, "cohesion": 1.5, "max_speed": 0.3 } }

Research basis: Flocking Research

Herding (behavior/herd/)

Implements herd movement for ungulates:

Key features:

  • Quorum-based movement initiation (Couzin et al., 2005)
  • Leadership dynamics (informed individuals)
  • Selfish herd positioning (Hamilton, 1971)
  • Collective decision-making

Example:

public class HerdCohesionGoal extends Goal { private final Animal animal; private final float quorumThreshold; // 0.3-0.7 @Override public boolean canUse() { List<Animal> herd = findHerdMembers(animal, 16.0); // Count how many are moving long movingCount = herd.stream() .filter(a -> a.getDeltaMovement().lengthSqr() > 0.01) .count(); // Activate if quorum threshold met float movingRatio = (float) movingCount / herd.size(); return movingRatio >= quorumThreshold; } }

Research basis: Herd Movement Research

Fleeing (behavior/fleeing/)

Implements escape behaviors:

Key features:

  • Flight initiation distance (Ydenberg & Dill, 1986)
  • Zigzag evasion for unpredictability
  • Freezing response (tonic immobility)
  • Stampede coordination

Escape strategies:

public enum EscapeStrategy { STRAIGHT, // Run directly away (high speed, predictable) ZIGZAG, // Evasive maneuvers (lower speed, unpredictable) FREEZE // Freeze before fleeing (crypsis) }

Example:

public class FleeFromPredatorGoal extends Goal { private final Animal animal; private final float flightInitiationDistance; // 8-32 blocks private LivingEntity predator; @Override public boolean canUse() { // Find nearest predator this.predator = findNearestPredator(animal, flightInitiationDistance); return this.predator != null; } @Override public void tick() { // Calculate flee vector (directly away from predator) Vec3 fleeDirection = animal.position() .subtract(predator.position()) .normalize(); // Apply zigzag if configured if (escapeStrategy == EscapeStrategy.ZIGZAG && animal.tickCount % 20 < 10) { fleeDirection = fleeDirection.add( getPerpendicularVector().scale(zigzagIntensity) ).normalize(); } // Move in flee direction at high speed Vec3 targetPos = animal.position().add(fleeDirection.scale(2.0)); animal.getNavigation().moveTo(targetPos.x, targetPos.y, targetPos.z, 1.5); } }

Research basis: Fleeing Research

Foraging (behavior/foraging/)

Implements feeding behaviors:

Key features:

  • Patch selection based on resource density
  • Marginal Value Theorem (Charnov, 1976)
  • Giving-up density threshold
  • Area-restricted search

Example:

public class SeekFoodGoal extends Goal { private final Animal animal; private final float hungerThreshold; private BlockPos foodPatch; private int foragingTime; @Override public boolean canUse() { float hunger = AnimalNeeds.getHunger(animal); if (hunger >= hungerThreshold) { return false; } // Find food patch within search radius this.foodPatch = findBestFoodPatch(animal, searchRadius); return this.foodPatch != null; } @Override public void tick() { if (animal.blockPosition().closerThan(foodPatch, 2.0)) { foragingTime++; // Check giving-up density (MVT) float patchDensity = calculatePatchDensity(foodPatch); if (patchDensity < givingUpDensity) { // Patch depleted, abandon and search for new patch stop(); return; } // Consume food periodically if (foragingTime % 40 == 0) { // Every 2 seconds consumeFood(); AnimalNeeds.modifyHunger(animal, 15f); } } else { // Navigate to patch animal.getNavigation().moveTo(foodPatch.getX(), foodPatch.getY(), foodPatch.getZ(), 1.0); } } /** * Calculate resource density of a food patch. * When density falls below giving-up threshold, abandon patch. * Based on: Marginal Value Theorem (Charnov, 1976) */ private float calculatePatchDensity(BlockPos patch) { // Count available food items within patch radius // Return ratio of available / maximum } }

Research basis: Foraging Research

Parent-Offspring (behavior/parent/)

Implements parental behaviors:

Key features:

  • Following (offspring follow parents)
  • Protection (parents defend offspring)
  • Separation distress calls
  • Age-based weaning

Example:

public class FollowParentGoal extends Goal { private final Animal baby; private Animal parent; private final double followDistance = 8.0; @Override public boolean canUse() { if (!baby.isBaby()) { return false; // Only babies follow parents } this.parent = findNearestAdult(baby, followDistance); return this.parent != null; } @Override public void tick() { double distance = baby.distanceTo(parent); // Stay within following distance if (distance > followDistance) { baby.getNavigation().moveTo(parent, 1.2); // Faster to catch up } else if (distance < 3.0) { baby.getNavigation().stop(); // Close enough } // Emit distress call if too far (>16 blocks) if (distance > 16.0 && baby.tickCount % 40 == 0) { playDistressCall(baby); } } }

Research basis: Parent-Offspring Research

Behavior Handles

Handles integrate behaviors with the data-driven configuration system.

Handle Structure

Every handle implements the EcologyHandle interface:

public class FlockingHandle implements EcologyHandle { // Cached configuration values (parsed at reload) private int topologicalNeighbors; private float separationWeight; private float alignmentWeight; private float cohesionWeight; @Override public boolean supports(EcologyProfile profile) { // Only activate if flocking is enabled in config return profile.has("flocking.enabled") && profile.getBool("flocking.enabled"); } @Override public void registerGoals(Mob mob, GoalSelector goals, GoalSelector targets) { // Cache configuration values this.topologicalNeighbors = profile.getInt("flocking.topological_neighbors", 7); this.separationWeight = profile.getFloat("flocking.separation", 1.8f); this.alignmentWeight = profile.getFloat("flocking.alignment", 1.0f); this.cohesionWeight = profile.getFloat("flocking.cohesion", 1.5f); // Register flocking goals goals.addGoal(5, new FlockCohesionGoal(mob, cohesionWeight, topologicalNeighbors)); goals.addGoal(5, new FlockSeparationGoal(mob, separationWeight)); goals.addGoal(5, new FlockAlignmentGoal(mob, alignmentWeight, topologicalNeighbors)); } @Override public void tick(Mob mob) { // Optional: Update flock state // Most work is done in individual goals } }

Registering Handles

Handles are registered during mod initialization:

public class EcologyBootstrap { public static void initialize() { EcologyHandleRegistry.register("hunger", new HungerHandle()); EcologyHandleRegistry.register("flocking", new FlockingHandle()); EcologyHandleRegistry.register("fleeing", new FleeingHandle()); EcologyHandleRegistry.register("foraging", new ForagingHandle()); EcologyHandleRegistry.register("parent_offspring", new ParentOffspringHandle()); } }

Spatial Queries and Neighbor Detection

Efficient neighbor queries are critical for flocking and herding behaviors.

Neighbor Query Example

/** * Find topological neighbors (N nearest entities). * More efficient than radius-based queries for flocking. */ public List<Entity> findTopologicalNeighbors(Entity entity, int count) { // Use AABB (bounding box) for initial filtering AABB searchBox = entity.getBoundingBox().inflate(16.0); return entity.level() .getEntitiesOfClass( entity.getClass(), searchBox, e -> e != entity // Exclude self ) .stream() .sorted(Comparator.comparingDouble(e -> e.distanceToSqr(entity))) .limit(count) .toList(); }

Spatial Partitioning

For large numbers of entities, use spatial partitioning:

// Grid-based spatial index (conceptual) public class SpatialIndex { private final Map<ChunkPos, List<Entity>> grid = new HashMap<>(); public List<Entity> getNeighbors(Vec3 position, double radius, EntityType<?> type) { // Get chunk coordinates ChunkPos chunkPos = new ChunkPos(BlockPos.containing(position)); // Check this chunk and 8 surrounding chunks return getAdjacentChunks(chunkPos).stream() .flatMap(chunk -> grid.getOrDefault(chunk, List.of()).stream()) .filter(e -> e.getType() == type) .filter(e -> e.position().distanceTo(position) < radius) .toList(); } }

Performance Considerations

Tick Interval Optimization

Expensive behaviors should run on intervals:

@Override public void tick(Mob mob) { // Run every 20 ticks (1 second) for expensive checks if (mob.tickCount % 20 == 0) { updateFlockState(mob); recalculateNeighbors(mob); } // Run every tick for cheap operations applySteeringForces(mob); }

Caching Strategies

Cache neighbor lists to avoid repeated queries:

public class FlockingGoal extends Goal { private List<Animal> cachedNeighbors; private long lastNeighborUpdate; @Override public void tick() { long currentTick = animal.level().getGameTime(); // Update neighbors every 10 ticks if (currentTick - lastNeighborUpdate >= 10) { cachedNeighbors = findTopologicalNeighbors(animal, 7); lastNeighborUpdate = currentTick; } // Use cached neighbors for steering calculations applySteering(cachedNeighbors); } }

Early Exit Conditions

Check cheap conditions before expensive ones:

@Override public boolean canUse() { // Cheap: Check if enabled if (!isEnabled()) return false; // Cheap: Check cooldown if (isOnCooldown()) return false; // Cheap: Check hunger if (!AnimalNeeds.isHungry(mob)) return false; // Expensive: Search for food return findFood() != null; }

Research References

All behaviors are based on academic research:

Research AreaKey CitationParameter Derived
FlockingReynolds (1987)Separation, alignment, cohesion
Topological InteractionBallerini et al. (2008)6-7 nearest neighbors
FleeingYdenberg & Dill (1986)Flight initiation distance
ForagingCharnov (1976)Giving-up density, patch departure
Collective MovementCouzin et al. (2005)Quorum threshold (0.3-0.7)
Selfish HerdHamilton (1971)Positioning in groups

For full citations and implementation notes, see:

See Also

Last updated on