AnimalManager
AnimalManager (1101 lines in Unturned/Managers/AnimalManager.cs) manages the passive wildlife population of the Unturned world. It controls the lifecycle of all Animal instances: spawning from weighted tables within tiered spawn zones, pack-based flocking AI, wander-angle rotation, player proximity startle/panic/attack responses, and loot drops (meat and pelt) on death. Animals are organized into PackInfo groups that share a wander direction and average position calculation.
Unlike other manager classes, AnimalManager exposes no static delegate hooks. Custom animal behavior is implemented through the Animal component directly or by replacing animal prefabs. The dropLoot method is the most commonly called external API.
Source code location: Unturned/Managers/AnimalManager.cs
Architecture Overview
AnimalManager extends SteamCaller and manages a flat list of Animal instances organized into PackInfo groups. The architecture follows the standard sequenced-batch replication pattern used by ZombieManager and VehicleManager, with state updates batched and gated by a sequence number.
Key architectural components:
List<Animal> animals: Flat list of all animal instances.List<PackInfo> packs: Pack groups that organize animals by spawn zone.- Sequence number (
seq): Batched-state replication gate that discards out-of-order updates. LevelAnimals: Separate class that loads spawn table data fromSpawns/Fauna.dat.
Pack System
PackInfo
Animals are organized into PackInfo groups. Each pack has its own wander behavior:
csharp
public class PackInfo
{
public List<AnimalSpawnpoint> spawns { get; private set; }
public List<Animal> animals { get; private set; }
private Vector3 wanderNormal;
public float wanderAngle { get; set; }
}The wander direction is computed as a 2D normal vector from the angle:
csharp
wanderNormal = new Vector3(Mathf.Cos(Mathf.Deg2Rad * wanderAngle), 0.0f,
Mathf.Sin(Mathf.Deg2Rad * wanderAngle));The pack's wanderAngle is initialized as Random.Range(0.0f, 360.0f) when the pack is created.
Average Point Calculations
Each pack has two average-point calculations:
- Average spawn point (
getAverageSpawnPoint): the centroid of all spawn points assigned to the pack, cached at construction time (computed once, never recalculated). - Average animal point (
getAverageAnimalPoint): the centroid of all live animal positions in the pack, recalculated once per frame (cached viaTime.frameCount).
csharp
public Vector3 getAverageAnimalPoint()
{
if (Time.frameCount > avgAnimalPointRecalculation)
{
avgAnimalPoint = Vector3.zero;
for (int animalIndex = 0; animalIndex < animals.Count; animalIndex++)
{
Animal animal = animals[animalIndex];
if (animal == null) continue;
avgAnimalPoint += animal.transform.position;
}
avgAnimalPoint /= animals.Count;
avgAnimalPointRecalculation = Time.frameCount;
}
return avgAnimalPoint;
}The average animal point is used to test whether the pack has drifted too far from its spawn area — if the average animal point diverges significantly from the average spawn point, the pack is steered back.
Flocking Behavior
Animals within a pack tend to move in the shared wander direction, creating the flocking behavior observable in-game. The wanderNormal is the same for all animals in a pack, so they drift as a group rather than scattering individually.
Spawning
giveAnimal (Admin/Plugin)
csharp
public static bool giveAnimal(Player player, ushort id)Raycasts down from 16 units above the player's forward offset to find ground level, then calls spawnAnimal. Returns false if no suitable ground is found.
spawnAnimal (General)
csharp
public static void spawnAnimal(ushort id, Vector3 point, Quaternion angle)First attempts to find a dead animal with the same ID to respawn it (via sendRevive), which avoids creating new Animal instances. If no dead animal is available, instantiates a new one, creates a temporary pack for it, and broadcasts via SendSingleAnimal.
Animal Revive and Respawn
When spawnAnimal finds a dead animal to respawn:
csharp
existingAnimal.sendRevive(point, Random.Range(0, 360f));sendRevive repositions the animal and sets isDead = false. This is more efficient than destroying and recreating the GameObject.
Max Instances
The per-level-size instance limit is read from Provider.modeConfigData.Animals:
| Level Size | Config Key |
|---|---|
| TINY | Max_Instances_Tiny |
| SMALL | Max_Instances_Small |
| MEDIUM | Max_Instances_Medium |
| LARGE | Max_Instances_Large |
| INSANE | Max_Instances_Insane |
AI States and Replication
Four Primary States
| State | RPC | Method | Description |
|---|---|---|---|
| Startle | SendAnimalStartle / ReceiveAnimalStartle | PlayStartleAnimation(animationIndex) | Brief hesitation animation triggered by close approach (~10m) |
| Attack | SendAnimalAttack / ReceiveAnimalAttack | askAttack(animationIndex) | Move toward player and bite (dangerous animals: wolf, bear) |
| Panic | SendAnimalPanic / ReceiveAnimalPanic | askPanic() | Immediate flee at max speed (damage or sustained proximity) |
| State update | SendAnimalStates / ReceiveAnimalStates | tellState(position, yaw) | Batched position and yaw replication |
Batched State Updates
State replication uses the same sequenced-batch pattern as zombies:
csharp
private static uint seq;
uint newSeq;
reader.ReadUInt32(out newSeq);
if (newSeq <= seq) return;
seq = newSeq;Each animal in the batch is serialized with ushort index, compressed Vector3 position, and float yaw (degrees). The sequence number ensures out-of-order packets are discarded.
Replication Frequency
SendAnimalStates is sent at a fixed interval (every lastTick delta). The batch contains only animals that have moved since the last update:
- Fast-moving animals get updated every tick.
- Idle animals are excluded from batches (no state change to send).
- Out-of-order packets are discarded by the sequence gate.
Loot Drops
csharp
public static void dropLoot(Animal animal)Two loot paths exist based on the animal asset:
- Reward table (
animal.asset.rewardID != 0): resolves items throughSpawnTableTool.ResolveLegacyId. Drop count isRandom.Range(asset.rewardMin, asset.rewardMax + 1), clamped to[0, 100]. - Meat/pelt system: drops
Random.Range(2, 5)ofanimal.asset.meatandanimal.asset.pelt(if non-zero) as separate item spawns.
Serialization
Initial Replication
SendMultipleAnimals serializes every animal in the list with asset ID, position, yaw, and alive flag. Sent via SendInitialGlobalState when a client connects.
csharp
internal static void SendInitialGlobalState(ITransportConnection transportConnection)
{
SendMultipleAnimals.Invoke(ENetReliability.Reliable, transportConnection,
SendMultipleAnimals_Write);
}The writer serializes:
ushort count
for each animal:
ushort assetId
Vector3 position (compressed)
float yaw (degrees)
bool isAliveThe client deserializes each animal via ReadSingleAnimal and calls manager.addAnimal(assetId, position, yaw, isAlive). If isAlive is false, the animal is immediately marked dead.
Dead/Alive State
SendAnimalAlive / SendAnimalDead replicate individual state transitions — used for revive and death events.
Animal Component Lifecycle
The Animal component tracks:
| Field | Type | Purpose |
|---|---|---|
id | ushort | Animal asset ID |
index | ushort | Index in AnimalManager.animals |
pack | PackInfo | Reference to the owning pack |
asset | AnimalAsset | Resolved asset reference |
isDead | bool | Alive/dead state |
Level Data — Fauna.dat
Table Structure
LevelAnimals.load() reads Spawns/Fauna.dat:
byte version
byte tableCount
for each table:
Color color
string name
ushort tableID
byte tierCount
for each tier:
string tierName
float chance
byte spawnCount
for each spawn:
ushort animalID
ushort spawnpointCount
for each spawnpoint:
byte type
Vector3 pointEach table has:
- Color and name (editor identification)
- Table ID for spawn table resolution
- Tier list: each tier has a name, weighted chance, and a list of animal IDs
Spawnpoints
Animal spawnpoints are stored as (type, position) pairs in the same file. Unlike zombie spawnpoints, animal spawnpoints are stored in a flat list, not a 2D region grid. The type field indexes into the table list.
Animal AI Behavior Details
Wander Behavior
Each animal pack has a wanderAngle initialized to a random direction. The pack's getWanderDirection() returns the normalized vector for this angle. Animals within the pack tend to move in this shared direction, creating the flocking behavior.
Startle, Panic, and Attack
| State | Trigger | Effect |
|---|---|---|
| Startle | Close approach (~10m) | Brief hesitation animation, then flee |
| Panic | Damage or sustained proximity | Immediate flee at max speed |
| Attack | Dangerous animal type (wolf, bear) + threshold | Move toward player and bite |
The startle animation has an animationIndex parameter allowing different startle reactions per animal asset.
State Replication Frequency
The updates counter tracks how many state packets have been sent, useful for debugging replication issues. Idle animals are excluded from batches because they have no state change to send.
Plugin Patterns
AnimalManager has no static hooks, but plugins commonly:
| Pattern | Implementation |
|---|---|
| Spawn animal at player | giveAnimal(player, animalId) |
| Respawn dead animals | Iterate animals, check isDead, call spawnAnimal |
| Modify pack behavior | Replace/wrap Animal component methods |
| Custom loot tables | Listen for dropLoot calls and override item drops |
| Animal count tracking | Subscribe to animals list mutations via custom wrapper |
Animal Spawning Internals
spawnAnimal In Detail
csharp
public static void spawnAnimal(ushort id, Vector3 point, Quaternion angle, bool isAdmin = false)
{
// Step 1: Try to revive a dead animal first
for (int i = 0; i < animals.Count; i++)
{
Animal animal = animals[i];
if (animal != null && animal.id == id && animal.isDead)
{
// Revive: reposition and restore
animal.sendRevive(point, angle.eulerAngles.y);
SendAnimalAlive.Invoke(ENetReliability.Reliable,
Provider.GatherRemoteClientConnections(),
writer => {
writer.WriteUInt16(animal.index);
writer.WriteClampedVector3(point);
writer.WriteFloat(angle.eulerAngles.y);
});
return;
}
}
// Step 2: No dead animal found — instantiate new one
if (animals.Count >= GetMaxInstances()) return;
// Check spawn region validity
byte x, y;
if (!Regions.tryGetCoordinate(point, out x, out y)) return;
// Instantiate prefab
Animal newAnimal = manager.addAnimal(id, point, angle.eulerAngles.y, true);
// Create transient pack
PackInfo pack = new PackInfo();
pack.animals.Add(newAnimal);
newAnimal.pack = pack;
newAnimal.index = (ushort)animals.Count;
animals.Add(newAnimal);
// Broadcast to clients
SendSingleAnimal.Invoke(ENetReliability.Reliable,
Provider.GatherRemoteClientConnections(),
writer => {
writer.WriteUInt16(id);
writer.WriteClampedVector3(point);
writer.WriteFloat(angle.eulerAngles.y);
});
}Max Instance Enforcement
csharp
private static int GetMaxInstances()
{
switch (Level.info.size)
{
case ELevelSize.TINY: return Provider.modeConfigData.Animals.Max_Instances_Tiny;
case ELevelSize.SMALL: return Provider.modeConfigData.Animals.Max_Instances_Small;
case ELevelSize.MEDIUM: return Provider.modeConfigData.Animals.Max_Instances_Medium;
case ELevelSize.LARGE: return Provider.modeConfigData.Animals.Max_Instances_Large;
case ELevelSize.INSANE: return Provider.modeConfigData.Animals.Max_Instances_Insane;
default: return 0;
}
}The instance limit prevents unlimited animal spawning via admin commands or plugin calls.
Pack Behavior Internals
Wander Steering
Each pack's wander behavior uses a steering algorithm:
csharp
// Called each frame per pack
private void UpdatePackWander(PackInfo pack)
{
Vector3 avgPos = pack.getAverageAnimalPoint();
Vector3 avgSpawn = pack.getAverageSpawnPoint();
// Distance from home check
Vector3 delta = avgPos - avgSpawn;
delta.y = 0; // Ignore height
if (delta.sqrMagnitude > 1600f) // 40m from home
{
// Steer back toward spawn area
Vector3 homeDir = (-delta).normalized;
pack.wanderAngle = Mathf.Atan2(homeDir.z, homeDir.x) * Mathf.Rad2Deg;
}
else
{
// Random drift within home range
pack.wanderAngle += Random.Range(-5f, 5f) * Time.deltaTime;
}
}The 40-meter radius check keeps packs from wandering too far from their spawn zone.
Flocking Cohesion
Animals within a pack share the same wanderNormal direction. This creates the cohesive flocking effect without implementing a full boids algorithm:
- All pack members get the same
wanderAngle. - Each animal moves in that direction at its own speed.
- The average position centroid is frame-cached (recalculated once per frame) to avoid redundant vector math.
State Replication Details
SendAnimalStates Serialization
csharp
internal static readonly ClientStaticMethod<byte[]> SendAnimalStates = ...;
// Server-side write
SendAnimalStates.Invoke(ENetReliability.Unreliable,
Provider.GatherRemoteClientConnections(),
writer => {
uint currentSeq = ++seq;
writer.WriteUInt32(currentSeq);
ushort count = 0;
// Count animals that need updates
for (int i = 0; i < animals.Count; i++)
{
Animal animal = animals[i];
if (animal != null && !animal.isDead && animal.hasStateChanged)
count++;
}
writer.WriteUInt16(count);
// Write each animal's state
for (int i = 0; i < animals.Count; i++)
{
Animal animal = animals[i];
if (animal != null && !animal.isDead && animal.hasStateChanged)
{
writer.WriteUInt16(animal.index);
writer.WriteClampedVector3(animal.transform.position);
writer.WriteFloat(animal.yaw);
animal.hasStateChanged = false;
}
}
},
k_ESteamNetworkingSendType_NoNagle);Client-Side Receive
csharp
[SteamCall(ESteamCallValidation.ONLY_FROM_SERVER)]
public void ReceiveAnimalStates(in ClientInvocationContext context)
{
uint newSeq = context.reader.ReadUInt32();
if (newSeq <= seq) return; // Discard out-of-order
seq = newSeq;
ushort count = context.reader.ReadUInt16();
for (ushort i = 0; i < count; i++)
{
ushort index = context.reader.ReadUInt16();
Vector3 position = context.reader.ReadClampedVector3();
float yaw = context.reader.ReadFloat();
Animal animal = GetAnimalByIndex(index);
if (animal != null)
animal.tellState(position, yaw);
}
}The NoNagle send type is used because animal state updates are high-frequency (every tick for moving animals) and benefit from low latency over packet coalescing.
Loot Drop System Detail
Reward Table Resolution
csharp
public static void dropLoot(Animal animal)
{
// Path 1: Reward table (preferred)
if (animal.asset.rewardID != 0)
{
ushort resolvedId = SpawnTableTool.ResolveLegacyId(
animal.asset.rewardID, EAssetType.ITEM,
(error) => UnturnedLog.error($"Animal {animal.asset.name} reward table error: {error}"));
int dropCount = Random.Range(animal.asset.rewardMin,
Math.Min(animal.asset.rewardMax + 1, 101)); // Clamped to [0,100]
for (int i = 0; i < dropCount; i++)
{
ItemManager.dropItem(
new Item(resolvedId, EItemOrigin.NATURE),
animal.transform.position + Random.insideUnitSphere * 0.5f,
false, true, true);
}
return;
}
// Path 2: Meat/pelt system
int meatCount = Random.Range(2, 5); // 2-4 meat pieces
for (int i = 0; i < meatCount; i++)
{
if (animal.asset.meat != 0)
ItemManager.dropItem(
new Item(animal.asset.meat, EItemOrigin.NATURE),
animal.transform.position + Random.insideUnitSphere * 0.5f,
false, true, true);
}
// Pelt (if configured)
if (animal.asset.pelt != 0)
{
int peltCount = Random.Range(2, 5);
for (int i = 0; i < peltCount; i++)
{
ItemManager.dropItem(
new Item(animal.asset.pelt, EItemOrigin.NATURE),
animal.transform.position + Random.insideUnitSphere * 0.5f,
false, true, true);
}
}
}Drop Scatter
The Random.insideUnitSphere * 0.5f ensures dropped items scatter in a 0.5m radius from the death point, preventing item stacking and making the loot feel natural.
Animal Asset Configuration
AnimalAsset Fields
| Field | Type | Purpose |
|---|---|---|
id | ushort | Asset ID for lookup and spawning |
meat | ushort | Item ID for meat drops |
pelt | ushort | Item ID for pelt drops |
rewardID | ushort | Spawn table ID for reward drops |
rewardMin | ushort | Minimum reward drop count |
rewardMax | ushort | Maximum reward drop count |
speedRun | float | Movement speed when fleeing |
speedWalk | float | Movement speed when wandering |
health | ushort | Maximum health |
attackDamage | float | Damage per bite |
Common Issues
Pack desync: When animals in a pack die and respawn at different times, they may end up in different packs temporarily. The spawn system handles this by creating transient single-animal packs.
Instance cap during events: Server events that spawn many animals simultaneously can hit the per-level-size instance cap. Plugins should check
animals.Count < GetMaxInstances()before spawning.Loot table errors: If
rewardIDreferences an invalid spawn table,SpawnTableTool.ResolveLegacyIdreturns 0, and no loot drops. The error is logged to console.Pose calculation on death: Dead animals stop updating their position in state batches but remain in the
animalslist. This ensures they can be revived without losing their index.
