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 stateFlockMembership— attached to each NPC in the group, references the flock entity by UUIDPersistentFlockData— 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:
- Picks a flock size from the flock definition
- Creates the invisible flock entity with
createFlock() - Joins the first NPC as the leader
- Spawns additional members in a small radius around the leader, randomly rotating them
- Each member gets a
FlockMembershipcomponent 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:
| Type | Acts as Leader | Description |
|---|---|---|
JOINING | No | Newly added, not yet fully integrated |
MEMBER | No | Regular flock member |
LEADER | Yes | Designated leader |
INTERIM_LEADER | Yes | Temporary 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:
- Joiner has a flock, target doesn't → target joins the joiner's flock
- Target has a flock, joiner doesn't → joiner joins the target's flock
- 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 ifleaderOnlyis 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
- Flocks are invisible ECS entities, not a property of NPCs. Query for
Flock+EntityGroupto find all active flocks. - Use flock definitions in
NPC/Flocks/JSON to configure group sizes. ChooseRangeSizeFlockAssetfor simple min/max orWeightedSizeFlockAssetfor precise probability control. - Mixed-role groups are first-class. Set
flockSpawnTypeson the leader role to spawn diverse packs. - Shared damage data means attacking one flock member alerts the whole group. Use
SensorFlockCombatDamagein NPC behavior trees to create coordinated threat responses. FlockSizeConditionlets NPCs make decisions based on group strength, enabling emergent pack tactics like ganging up on threats or scattering when outnumbered.