Interactable Mannequin — Clothing Display
Building clothing display systems, armor showcases, and player outfit previews in Unturned depends on understanding how InteractableMannequin stores a packed state of seven clothing slots, cosmetic skins, and pose animation data in barricade state bytes. InteractableMannequin extends Interactable and implements IManualOnDestroy. It stores a full set of player clothing items and renders them on a HumanClothes component, supporting pose animation, cosmetic skin display, and four interaction modes for adding, removing, swapping, and copying clothing.
Source code location: Unturned/Interactable/InteractableMannequin.cs
State Data Layout
The mannequin state is a packed Block containing:
| Data | Size | Description |
|---|---|---|
owner | 8 bytes | CSteamID owner |
group | 8 bytes | CSteamID group |
visualShirt–visualGlasses | 7 × 4 bytes | Economy skin IDs |
shirt–glasses | 7 × 2 bytes | Item IDs |
shirtQuality–glassesQuality | 7 × 1 byte | Item qualities |
shirtState–glassesState | 7 × variable | Item state arrays (length-prefixed) |
pose_comp | 1 byte | Packed pose and mirror flag |
State Size
- Empty mannequin: ~66 bytes (owner + group + zeros + pose)
- Fully clothed with standard items: ~200 bytes
- Items with large states (dye data, proof data): up to 230+ bytes
The 255-byte barricade state limit must accommodate the full mannequin data.
updateState — Deserialization
csharp
public override void updateState(Asset asset, byte[] state)
{
isLocked = ((ItemBarricadeAsset)asset).isLocked;
Transform root = transform.Find("Root");
anim = root.GetComponent<Animation>();
clothes = root.GetOrAddComponent<HumanClothes>();
clothes.ShouldHairOverridesUseFallbackColor = true;
updateState(state); // overload that parses Block data
}Block Parsing
csharp
public void updateState(byte[] state)
{
Block block = new Block(state);
_owner = new CSteamID((ulong)block.read(Types.UINT64_TYPE));
_group = new CSteamID((ulong)block.read(Types.UINT64_TYPE));
clothes.skin = new Color32(210, 210, 210, 255); // Default skin tone
clothes.color = clothes.skin; // Hair color
clothes.BeardColor = clothes.skin;
clothes.visualShirt = block.readInt32();
// ... read all 7 visuals, all 7 item IDs, all 7 qualities, all 7 state arrays
clothes.apply();
setPose(block.readByte());
}The default skin tone (210, 210, 210) prevents mannequins from appearing with black "void skin" before clothing is applied. ShouldHairOverridesUseFallbackColor = true ensures hats with hair overrides use neutral hair color.
Pose System
Encoding
Pose is encoded as a single byte with the high bit as a mirror flag:
csharp
public byte getComp(bool mirror, byte pose)
{
byte mirrorComp = (byte)(mirror ? 1 : 0);
byte poseComp = (byte)((mirrorComp << 7) | pose);
return poseComp;
}Three Built-in Poses
csharp
public void updatePose()
{
string clip;
switch (pose)
{
case 0: clip = "T"; break;
case 1: clip = "Classic"; break;
case 2: clip = "Lie"; break;
}
if (anim != null)
{
anim.transform.localScale = new Vector3(mirror ? -1 : 1, 1, 1);
anim.Play(clip);
}
}Mirror is implemented as X-scale inversion (-1 vs 1).
Clothing Interaction Modes
| Mode | Behavior |
|---|---|
COSMETICS | Copies player's visual skins to mannequin, returns clothing items to player |
ADD | Takes the held clothing item from hotbar and places it on the mannequin |
REMOVE | Returns all mannequin clothing to the player's inventory |
SWAP | Exchanges all clothing between the player and mannequin |
ADD Mode
csharp
case EMannequinUpdateMode.ADD:
ItemJar item = player.inventory.getItem(player.equipment.equippedPage, ...);
switch (player.equipment.asset.type)
{
case EItemType.SHIRT:
if (shirt != 0)
player.inventory.forceAddItem(new Item(shirt, 1, shirtQuality, shirtState), false);
clothes.shirt = item.item.id;
shirtQuality = item.item.quality;
shirtState = item.item.state;
break;
// ... PANTS, HAT, BACKPACK, VEST, MASK, GLASSES
}
player.equipment.use();Each clothing type handled individually. Existing mannequin items returned to player before new ones assigned.
SWAP Mode
csharp
case EMannequinUpdateMode.SWAP:
// Read all 7 clothing slots from player
// Write mannequin clothes to player via player.clothing.updateClothes(...)
// Write player clothes to mannequin via updateClothes(...)Obstruction Safety Check
csharp
public bool isObstructedByPlayers()
{
const float halfHeight = 1.0f;
const float radius = 0.4f;
Vector3 center = transform.position;
Vector3 p0 = center + new Vector3(0.0f, -halfHeight + radius, 0.0f);
Vector3 p1 = center + new Vector3(0.0f, halfHeight - radius, 0.0f);
int mask = IsChildOfVehicle
? RayMasks.BLOCK_CHAR_HINGE_OVERLAP_ON_VEHICLE
: RayMasks.BLOCK_CHAR_HINGE_OVERLAP;
int numCollisions = Physics.OverlapCapsuleNonAlloc(p0, p1, radius,
InteractableDoor.checkColliders, mask, QueryTriggerInteraction.Ignore);
return numCollisions > 0;
}A capsule overlap check matching the player hitbox prevents pose-switch exploits that push players through geometry.
Destroy Handling
csharp
public void ManualOnDestroy()
{
if (!Provider.isServer) return;
dropClothes();
}
public void dropClothes()
{
if (shirt != 0)
ItemManager.dropItem(new Item(shirt, 1, shirtQuality, shirtState), ...);
// ... all 7 clothing types
clearClothes();
}Network Protocol
| Direction | Message | Rate Limit | Reliability |
|---|---|---|---|
| Client → Server | SendUpdateRequest(EMannequinUpdateMode) | 2 Hz | Unreliable |
| Server → All | SendUpdate(byte[] state) | — | Reliable |
| Client → Server | SendPoseRequest(byte poseComp) | 2 Hz | Unreliable |
| Server → All | SendPose(byte poseComp) | — | Reliable |
State updates use InvokeAndLoopback for atomic broadcast.
isUpdatable Rate Limit
csharp
public bool isUpdatable => Time.realtimeSinceStartup - updated > 0.5f;500ms rate limit prevents rapid ADD/REMOVE/SWAP. Network bandwidth saver and exploit prevention.
Use Interaction with Quick-Add
csharp
public override void use()
{
if (InputEx.GetKey(ControlsSettings.other))
{
if (Player.LocalPlayer.equipment.useable is UseableClothing)
ClientRequestUpdate(EMannequinUpdateMode.ADD);
else
ClientRequestUpdate(EMannequinUpdateMode.REMOVE);
}
else
{
PlayerUI.instance.mannequinUI.open(this);
PlayerLifeUI.close();
}
}- Holding clothing → ADD mode.
- Holding anything else → REMOVE mode (avoid accidental wipe).
- No "other" key → Open UI.
Clothing Rendering Order
HumanClothes.apply() renders in this order:
- Skin (base body mesh)
- Shirt (over skin)
- Vest (over shirt)
- Backpack (over vest)
- Pants (over skin on legs)
- Mask (over face)
- Glasses (over eyes)
- Hat (on head)
rebuildState — Full State Writing
csharp
public void rebuildState()
{
Block block = new Block();
block.write(owner, group);
block.writeInt32(visualShirt);
// ... all 7 visual skin IDs
block.writeUInt16(clothes.shirt);
block.writeByte(shirtQuality);
// ... all 7 item IDs + qualities
block.writeByteArray(shirtState);
// ... all 7 state arrays
block.writeByte(pose_comp);
byte[] state = block.getBytes(out size);
BarricadeManager.updateState(transform, state, size);
}Worked Code Example: Mannequin Management Plugin
Bulk Mannequin State Export
csharp
using SDG.Unturned;
using UnityEngine;
public class MannequinExporter
{
/// <summary>
/// Exports all mannequins within a radius as a JSON-serializable
/// snapshot of their clothing, poses, and visual skin states.
/// Useful for backup restoration and cross-server migration.
/// </summary>
public string ExportMannequinsInRadius(Vector3 center, float radius)
{
System.Text.StringBuilder json = new System.Text.StringBuilder();
json.AppendLine("[");
bool first = true;
foreach (BarricadeRegion region in BarricadeManager.regions.Values)
{
foreach (BarricadeDrop drop in region.drops)
{
if (Vector3.Distance(drop.model.position, center) > radius)
continue;
InteractableMannequin mannequin = drop.model.GetComponent<InteractableMannequin>();
if (mannequin == null)
continue;
if (!first) json.AppendLine(",");
first = false;
json.AppendLine(" {");
json.AppendLine($" \"position\": \"{drop.model.position}\",");
json.AppendLine($" \"pose\": {mannequin.pose},");
json.AppendLine($" \"mirror\": {mannequin.mirror.ToString().ToLowerInvariant()},");
json.AppendLine($" \"shirt\": {mannequin.clothes.shirt},");
json.AppendLine($" \"pants\": {mannequin.clothes.pants},");
json.AppendLine($" \"hat\": {mannequin.clothes.hat},");
json.AppendLine($" \"backpack\": {mannequin.clothes.backpack},");
json.AppendLine($" \"vest\": {mannequin.clothes.vest},");
json.AppendLine($" \"mask\": {mannequin.clothes.mask},");
json.AppendLine($" \"glasses\": {mannequin.clothes.glasses}");
json.Append(" }");
}
}
json.AppendLine();
json.AppendLine("]");
return json.ToString();
}
}Pose Randomizer
csharp
using SDG.Unturned;
using UnityEngine;
public class MannequinPosePlugin
{
private static readonly System.Random _random = new System.Random();
/// <summary>
/// Randomizes all mannequin poses within range, cycling through
/// the three built-in poses (T-pose, Classic, Lie) with optional mirror.
/// </summary>
public static int RandomizePosesInZone(Vector3 center, float radius)
{
int count = 0;
foreach (BarricadeRegion region in BarricadeManager.regions.Values)
{
foreach (BarricadeDrop drop in region.drops)
{
if (Vector3.Distance(drop.model.position, center) > radius)
continue;
InteractableMannequin mannequin = drop.model.GetComponent<InteractableMannequin>();
if (mannequin == null)
continue;
byte newPose = (byte)_random.Next(0, 3);
bool newMirror = _random.NextDouble() > 0.5;
byte poseComp = InteractableMannequin.getComp(newMirror, newPose);
BarricadeManager.ServerSetMannequinPoseRequest(
drop.model,
poseComp
);
count++;
}
}
return count;
}
}Mermaid Diagram: Mannequin State Lifecycle
Comparison: Mannequin vs. Other Item Display Systems
| Feature | InteractableMannequin | Item Frame (Mod) | Storage Crate Display | Player Corpse |
|---|---|---|---|---|
| Clothing rendering | 7 slots via HumanClothes | Single item icon | Items as list | Full player model |
| Pose/animation | 3 poses + mirror | None | None | Ragdoll physics |
| State size | 200–230 bytes | ~20 bytes per item | Item count × 6 bytes | Full player state |
| Visual skins | 7 × 4 bytes economy skin IDs | No | No | Player's current skins |
| Interaction modes | ADD/REMOVE/SWAP/COSMETICS | Place/take | Open/close container | Loot (manual) |
| Obstruction safety | Capsule overlap check | No | No | Physics collider |
| Drop on destroy | Yes (dropClothes) | Yes (drop item) | Yes (drop all) | N/A (despawns) |
| Rate limit | 500ms (isUpdatable) | None | None | N/A |
| Network protocol | ClientSetPose + SendUpdate | Single item sync | Container sync | Death packet |
Failure Modes and Common Mistakes
255-byte state overflow — A fully clothed mannequin with large item states (dye data, proof items, multiple quality bytes) can push the total state beyond the 255-byte
Blocklimit. When this happens,rebuildState()silently truncates the state, and the last few items' state data is lost. This most commonly affects mannequins with hair-override hats carrying complex state arrays.Cloak of invisibility from SWAP exploit — If a player uses SWAP mode while wearing no clothing, the player's clothing is set to a mix of their own items and
nullwhere the mannequin had none. The clothing system does not handle partial null slots gracefully, resulting in invisible body parts (floating head, missing torso).Visual skin desync — The
visualShirtthroughvisualGlassesfields store economy skin IDs that are loaded from Steam inventory. If a skin is removed from Steam (delisted, refunded), the mannequin's visual skin reference becomes invalid. TheHumanClothescomponent renders the base item without the skin overlay, but the skin ID remains in the state bytes — potentially causing visual confusion for viewers expecting a specific skin.Pose mirror teleportation —
updatePose()setsanim.transform.localScale = new Vector3(mirror ? -1 : 1, 1, 1). If the mannequin'sRoottransform has non-uniform scaling (e.g., from a parent hierarchy), the X-scale mirror may interact with non-strict child transforms and cause the mannequin to appear at a different world position — a "teleporting" visual artifact.Timing exploit: ADD while holding consumable —
use()checks forUseableClothingto determine ADD mode. If a player equips a consumable item (food, medical) that has a clothing-type parent class or a misconfigured type enum, the ADD check may misidentify the held item as clothing. The consumable is consumed (deleted) but not placed on the mannequin — it vanishes.
How This Field Behaves Differently from the SDG Docs
SDG docs list mannequins as "cosmetic only." The community wiki describes mannequins as purely decorative. In the SDK, mannequins are a storage system — they hold items that are returned when removed, dropped on destruction, and can be swapped with players. They are as functional as a storage container for clothing items, not merely cosmetic displays.
SDG docs claim 4-5 clothing slots. Some documentation references only shirt, pants, hat, and backpack. The SDK mannequin stores seven slots: shirt, pants, hat, backpack, vest, mask, and glasses. The full set mirrors the player's clothing equipment slots.
SDG docs describe poses as "animation clips." The docs suggest arbitrary animation clips can be assigned. In the SDK, poses are strictly limited to three hardcoded animation names — "T", "Classic", and "Lie" — mapped to
posevalues 0, 1, and 2. There is no field for custom animation clip names or GUIDs.SDG docs say mannequins retain player skin tone. The documentation implies the interacting player's skin color transfers to the mannequin. In the SDK,
clothes.skinis hardcoded tonew Color32(210, 210, 210, 255)— a neutral gray default. The mannequin's skin tone is always this default regardless of who placed it or interacted with it last.
Performance Considerations
State Serialization
rebuildState() constructs a Block and writes all seven clothing slots, qualities, and state arrays. This is called on every ADD, REMOVE, and SWAP operation — up to a maximum of 2 times per second (500ms rate limit). Each call handles ~200 bytes of data. The cost is negligible (< 0.01ms per call) but important to note for tools that batch-process hundreds of mannequins.
Obstruction Check
isObstructedByPlayers() performs a Physics.OverlapCapsuleNonAlloc each time a pose change is requested. On a server with 50 mannequins being pose-cycled by automated plugins, this results in 50 capsule overlap checks per tick. Each check is ~0.02ms on modern hardware.
Client Rendering
HumanClothes.apply() iterates over all seven clothing slots and renders the associated prefab meshes. Each clothing item typically adds 1–3 mesh renderers. A fully clothed mannequin (7 items × 2 average meshes) contributes 14 additional draw calls to the scene. In a room of 20 mannequins, this is 280 draw calls — reasonable but worth considering for performance-critical maps.
Deeper FAQ
Q: Can I put weapons or non-clothing items on a mannequin?
No. The ADD mode switch statement checks player.equipment.asset.type against EItemType.SHIRT, PANTS, HAT, BACKPACK, VEST, MASK, and GLASSES. Any other item type is silently ignored. Plugins can patch this to support weapon display, but the HumanClothes rendering system uses clothing-specific attachment points — weapons would not render correctly without additional prefab attachment code.
Q: What happens if a player dies while wearing swapped mannequin clothes?
The clothes follow the player's death state: they are dropped with the corpse or retrieved by the player on respawn (sentinel items excluded). The mannequin retains whatever clothing was swapped onto it. After a swap, both the player and mannequin have new clothing sets — neither retains a "backup" of the original outfit.
Q: Can mannequins be locked like storage containers?
Yes. updateState() sets isLocked = ((ItemBarricadeAsset)asset).isLocked. The standard lock pattern (checkStore) applies, with owner/group validation. A locked mannequin requires ownership or group membership to modify. The checkUpdate method (used for mannequins) also has the singleplayer bypass that allows any interaction in singleplayer servers.
Q: Do mannequins preserve item quality and state data?
Yes. Each clothing slot stores the item ID (ushort), quality (byte), and state byte array (variable length). This means a mannequin can hold clothing with dye colors, proof-of-purchase data, custom names, or any other item state. The quality affects the visual appearance (damaged textures for low quality).
Q: How do I programmatically detect if a player is wearing mannequin clothes?
There is no built-in property tracking. You must track it yourself by hooking into SWAP and REMOVE interactions via Harmony. Store a reference to the mannequin transform that supplied each clothing item to the player, and check that reference when the player undresses or dies.
Cross-References
- Player Clothing Visual Equipment — How player clothing equipment slots map to the same 7-slot system used by mannequins.
- Interactable Safe — Lock System — Lock behavior for mannequin access control.
- Useable Clothing — Dressing/Undressing —
UseableClothingdetection during ADD mode. - Barricade Manager — Barricade state management and
ServerSetBarricadeLockedInternal.
