Back to Knowledge Base

Hytale Flock System: Group AI Coordination and Pack Behavior

A deep dive into Hytale's flock system covering group formation, leader election, flocking steering behaviors, shared damage tracking, and data-driven flock configuration.

Player Games··hytale

Hytale's flock system enables NPCs to form coordinated groups with leader/member hierarchies, shared damage awareness, and classic flocking steering behaviors (cohesion, separation, alignment). Rather than each mob acting independently, flocked NPCs move as a unit, share combat intelligence, and dynamically elect leaders when the current one dies. The entire system is data-driven through JSON flock definitions. This reference is based on Hytale server version 2026.03.26-89796e57b.

Architecture Overview

The flock system lives in the com.hypixel.hytale.server.flock package and is registered by FlockPlugin. It introduces three ECS components:

  • Flock — attached to a virtual “flock entity” (not a visible NPC), holds the group's shared state
  • FlockMembership — attached to each NPC in the group, references the flock entity by UUID
  • PersistentFlockData — serialized flock metadata (max grow size, allowed roles, current size)

A flock entity is an invisible ECS entity that owns an EntityGroup (the member list), a Flock component (damage tracking, removal status), and a UUIDComponent (for cross-reference). Individual NPCs point to this entity via their FlockMembership component.

Flock Definitions: Data-Driven Configuration

Flock assets are loaded from NPC/Flocks/ JSON files. Two sizing strategies are available:

RangeSizeFlockAsset — picks a random size from a min/max range:

public class RangeSizeFlockAsset extends FlockAsset {
    protected int[] size;  // e.g. [2, 4] = 2 to 4 members

    public int pickFlockSize() {
        return RandomExtra.randomRange(Math.max(1, this.size[0]), this.size[1]);
    }
}

WeightedSizeFlockAsset — uses weighted probabilities for each possible size:

public class WeightedSizeFlockAsset extends FlockAsset {
    protected int minSize;
    protected double[] sizeWeights;  // e.g. [25, 75] = 25% chance of minSize, 75% chance of minSize+1

    public int pickFlockSize() {
        int index = RandomExtra.pickWeightedIndex(this.sizeWeights);
        return Math.max(this.minSize, 1) + index;
    }
}

Both types support MaxGrowSize (caps how large a flock can grow after spawning) and BlockedRoles (prevents specific NPC roles from joining):

public abstract class FlockAsset {
    protected int maxGrowSize = 8;
    protected String[] blockedRoles;  // Roles excluded from this flock
}

Spawning a Flock

When the spawning system creates a group of NPCs, FlockPlugin.trySpawnFlock() handles the entire process:

  1. Picks a flock size from the flock definition
  2. Creates the invisible flock entity with createFlock()
  3. Joins the first NPC as the leader
  4. Spawns additional members in a small radius around the leader, randomly rotating them
  5. Each member gets a FlockMembership component pointing to the shared flock entity
// Members spawn in a small cluster around the leader
for (int i = 1; i < flockSize; i++) {
    // Pick member role (random or round-robin from allowed types)
    if (randomSpawn) {
        memberRoleIndex = roles[RandomExtra.randomRange(rolesSize)];
    } else {
        memberRoleIndex = roles[index];
        index = (index + 1) % rolesSize;
    }

    // Spawn and position near leader with slight offset
    Pair<Ref<EntityStore>, NPCEntity> memberPair = NPCPlugin.get()
        .spawnEntity(store, memberRoleIndex, position, rotation, ...);

    // Randomize facing and offset position
    memberTransform.getRotation().setYaw(yaw + RandomExtra.randomRange(-PI/4, PI/4));
    memberTransform.getPosition().assign(
        x + RandomExtra.randomRange(-0.5, 0.5),
        offsetY,
        z + RandomExtra.randomRange(-0.5, 0.5)
    );

    FlockMembershipSystems.join(memberRef, flockReference, store);
}

Flocks support mixed-role groups. The leader's role definition specifies flockSpawnTypes (an array of role indices) and isFlockSpawnTypesRandom to control whether extra members are picked randomly or cycled round-robin. This means a wolf pack could have one alpha (leader role) and several regular wolves (member role).

Membership and Leader Election

FlockMembership.Type defines four states:

TypeActs as LeaderDescription
JOININGNoNewly added, not yet fully integrated
MEMBERNoRegular flock member
LEADERYesDesignated leader
INTERIM_LEADERYesTemporary leader after original dies

When an NPC wants to join a flock, it must pass two checks in canJoinFlock():

public static boolean canJoinFlock(Ref<EntityStore> reference, Ref<EntityStore> flockReference, Store<EntityStore> store) {
    PersistentFlockData flockData = flockComponent.getFlockData();
    // Check 1: Flock hasn't reached maximum grow size
    if (entityGroupComponent.size() >= flockData.getMaxGrowSize()) return false;
    // Check 2: NPC's role is in the allowed roles list
    String roleName = npcComponent.getRoleName();
    return roleName != null && flockData.isFlockAllowedRole(roleName);
}

Leadership is determined by which NPC can lead (role.isCanLeadFlock()). When two unaffiliated NPCs meet and one triggers ActionFlockJoin, the system intelligently handles three cases:

  1. Joiner has a flock, target doesn't → target joins the joiner's flock
  2. Target has a flock, joiner doesn't → joiner joins the target's flock
  3. Neither has a flock → creates a new flock; the leader-capable NPC joins first (becoming leader), then the other joins

Flocking Steering: Cohesion, Separation, and Alignment

BodyMotionFlock implements classic Craig Reynolds flocking behaviors as an NPC body motion component. Each tick, it computes a steering vector from three forces:

public boolean computeSteering(Ref<EntityStore> ref, Role role, InfoProvider sensorInfo,
                                double dt, Steering desiredSteering, ComponentAccessor<EntityStore> accessor) {
    // Accumulate positions, velocities, and distances of nearby flock members
    groupSteeringAccumulator.setMaxRange(role.getFlockInfluenceRange());
    groupSteeringAccumulator.begin(ref, accessor);
    entityGroup.forEachMemberExcludingSelf(
        (member, entity, accumulator, store) -> accumulator.processEntity(member, store),
        ref, groupSteeringAccumulator, accessor
    );
    groupSteeringAccumulator.end();

    // Three steering forces:
    // 1. Cohesion: steer toward average position of flock mates
    sumOfPositions.subtract(leaderPos).normalize().scale(weightCohesion);
    // 2. Separation: steer away from nearby flock mates (inverted)
    sumOfDistances.normalize().scale(-weightSeparation);
    // 3. Leader following: steer toward the leader
    toLeader.subtract(leaderPos).normalize().scale(0.5);

    // Combine all forces
    sumOfPositions.add(sumOfDistances).add(toLeader).normalize();
    desiredSteering.setTranslation(sumOfPositions);

    // Alignment: match the group's average heading
    desiredSteering.setYaw(PhysicsMath.headingFromDirection(
        sumOfVelocities.getX(), sumOfVelocities.getZ()));
}

The GroupSteeringAccumulator processes each member within flockInfluenceRange, accumulating their positions, velocities, and distance vectors. Members outside this range don't influence the steering calculation, creating natural subgroup behavior in larger flocks.

Shared Damage Tracking

Flocks share combat intelligence through a double-buffered damage tracking system. The Flock component maintains two DamageData instances (current and next) for both the whole group and the leader specifically:

public class Flock implements Component<EntityStore> {
    private DamageData nextDamageData = new DamageData();
    private DamageData currentDamageData = new DamageData();
    private DamageData nextLeaderDamageData = new DamageData();
    private DamageData currentLeaderDamageData = new DamageData();

    // Swapped each tick
    public void swapDamageDataBuffers() {
        DamageData temp = this.nextDamageData;
        this.nextDamageData = this.currentDamageData;
        this.currentDamageData = temp;
        this.nextDamageData.reset();
        // ... same for leader data
    }
}

Two sensors read this shared data:

  • SensorFlockCombatDamage — detects the most damaging attacker to any flock member (or leader only if leaderOnly is set). This lets all members know who is threatening the group, even if they weren't personally attacked.
  • SensorInflictedDamage — tracks damage the flock has dealt, enabling retreat or celebration behaviors.

When an entity dies, FlockDeathSystems.EntityDeath notifies the killer's flock via onTargetKilled(), which records the kill location in the damage data for strategic awareness.

Flock Lifecycle

Dissolution: When a flock entity is removed (RemoveReason.REMOVE), the EntityRemoved system dissolves it—removing FlockMembership from all members and marking chunks dirty so they save correctly:

case REMOVE:
    entityGroup.setDissolved(true);
    for (Ref<EntityStore> memberRef : entityGroup.getMemberList()) {
        commandBuffer.removeComponent(memberRef, FlockMembership.getComponentType());
        transformComponent.markChunkDirty(commandBuffer);
    }
    flock.setRemovedStatus(FlockRemovedStatus.DISSOLVED);

Unloading: When the flock's chunk unloads, the flock is marked UNLOADED rather than dissolved. Members retain their FlockMembership with a UUID reference, allowing the flock to reconnect when the chunk reloads.

Death handling: When an NPC dies, it's removed from its flock unless role.isCorpseStaysInFlock() is true. Players are always removed from flocks on death. Changing game mode away from Adventure also triggers flock removal.

Decision Making: FlockSizeCondition

NPC decision trees can branch based on flock size using FlockSizeCondition, which extends the curve-based condition system:

public class FlockSizeCondition extends ScaledCurveCondition {
    protected double getInput(...) {
        FlockMembership membership = archetypeChunk.getComponent(selfIndex, FlockMembership.getComponentType());
        Ref<EntityStore> flockReference = membership.getFlockRef();
        return commandBuffer.getComponent(flockReference, EntityGroup.getComponentType()).size();
    }
}

This enables behaviors like: “become aggressive when flock size is 3+” or “flee when alone.” The scaled curve maps flock size to a 0–1 utility score, integrating naturally with Hytale's utility-based AI decision system.

Key Takeaways for Modders

  1. Flocks are invisible ECS entities, not a property of NPCs. Query for Flock + EntityGroup to find all active flocks.
  2. Use flock definitions in NPC/Flocks/ JSON to configure group sizes. Choose RangeSizeFlockAsset for simple min/max or WeightedSizeFlockAsset for precise probability control.
  3. Mixed-role groups are first-class. Set flockSpawnTypes on the leader role to spawn diverse packs.
  4. Shared damage data means attacking one flock member alerts the whole group. Use SensorFlockCombatDamage in NPC behavior trees to create coordinated threat responses.
  5. FlockSizeCondition lets NPCs make decisions based on group strength, enabling emergent pack tactics like ganging up on threats or scattering when outnumbered.