Equipment in most RPGs comes in one size. A breastplate dropped by a dungeon boss fits a 150 cm halfling the same way it fits a 200 cm warrior — the mesh scales uniformly to fill the character, or clips through it, or floats off it. Players notice this. Not consciously, most of the time. But the visual coherence of the world suffers when armor looks like it was cast from the same mould and pinned to every body regardless of shape.
Procedural equipment fitting solves this. Each piece of equipment gets scaled based on the wearing character’s body dimensions. A chest plate stretches at the shoulders for a broad character, wraps closer on a narrow one. Boots extend for a long-footed character. The system can go further: loot drops can be weighted toward items that fit the current character, making rewards feel personal.
This guide implements the system in three parts: the fit category model, the mesh scaling approach, and body-aware loot generation.
How the fit system works
The core abstraction is a fit category per equipment slot. Each slot is governed by one primary body dimension (chest armor → chest circumference; gloves → hand breadth; boots → foot length). Equipment has a target dimension — the dimension it was designed to fit. The gap between the character’s actual dimension and the equipment’s target determines fit quality.
Character dimension: 1020 mm chest circumference
Equipment target: 950 mm
Gap: +70 mm → Tight fit
Three fit categories:
- Regular: dimension is within the equipment’s intended range
- Tight: character dimension significantly exceeds the target
- Loose: character dimension is significantly below the target
Each category drives a different visual outcome: regular looks correct, tight shows visible tension, loose has sag or excess fabric. You can implement this with bone scaling alone (this guide), blend shapes (requires more asset work), or a combination.
Step 1: Define the fit model
// EquipmentFitModel.cs
using System.Collections.Generic;
using UnityEngine;
public enum FitCategory { Loose, Regular, Tight }
[System.Serializable]
public class SlotFitConfig
{
public string slotId; // "chest", "legs", "gloves", "boots", "helmet"
public string governingDimension; // dimension_id from the API
public float targetDimMm; // the dimension the equipment was designed for
public float tightThreshold; // character dim above this → Tight
public float looseThreshold; // character dim below this → Loose
}
public static class DefaultSlotConfigs
{
// Adjust target/threshold values to match your game's art direction
public static readonly List<SlotFitConfig> All = new List<SlotFitConfig>
{
new SlotFitConfig {
slotId = "chest", governingDimension = "chest_circumference",
targetDimMm = 940f, tightThreshold = 1010f, looseThreshold = 860f
},
new SlotFitConfig {
slotId = "legs", governingDimension = "hip_circumference",
targetDimMm = 960f, tightThreshold = 1050f, looseThreshold = 870f
},
new SlotFitConfig {
slotId = "gloves", governingDimension = "hand_breadth",
targetDimMm = 88f, tightThreshold = 100f, looseThreshold = 74f
},
new SlotFitConfig {
slotId = "boots", governingDimension = "foot_length",
targetDimMm = 265f, tightThreshold = 295f, looseThreshold = 235f
},
new SlotFitConfig {
slotId = "helmet", governingDimension = "head_circumference",
targetDimMm = 570f, tightThreshold = 600f, looseThreshold = 540f
},
};
}
Step 2: The fit calculation
// EquipmentFitSystem.cs
using System;
using System.Collections.Generic;
using UnityEngine;
public class EquipmentFitSystem : MonoBehaviour
{
public FitCategory GetFit(float characterDimMm, SlotFitConfig config)
{
if (characterDimMm > config.tightThreshold) return FitCategory.Tight;
if (characterDimMm < config.looseThreshold) return FitCategory.Loose;
return FitCategory.Regular;
}
// Returns a scale multiplier clamped to avoid extreme deformation
// 1.0 = perfect fit, >1.0 = tight (stretched), <1.0 = loose (excess material)
public float GetFitScale(float characterDimMm, SlotFitConfig config)
{
float raw = characterDimMm / config.targetDimMm;
return Mathf.Clamp(raw, 0.82f, 1.22f);
}
public void ApplyFit(GameObject equipmentObject, SlotFitConfig config, AvatarDimensions charDims)
{
float charDim = GetDimensionValue(config.governingDimension, charDims);
float scale = GetFitScale(charDim, config);
FitCategory fit = GetFit(charDim, config);
// Scale the equipment mesh to the character's body
// X and Z govern circumference/breadth; Y governs length
var t = equipmentObject.transform;
t.localScale = config.slotId switch
{
"chest" => new Vector3(scale, 1f, scale),
"legs" => new Vector3(scale, 1f, scale),
"gloves" => new Vector3(scale, 1f, scale),
"boots" => new Vector3(scale, scale, scale),
"helmet" => new Vector3(scale, scale, scale),
_ => new Vector3(scale, 1f, scale),
};
// Notify visual components of fit state for animation/shader effects
var fitRenderer = equipmentObject.GetComponent<FitVisualController>();
if (fitRenderer != null)
fitRenderer.SetFit(fit, scale);
Debug.Log($"[FitSystem] {config.slotId}: charDim={charDim:F0}mm " +
$"target={config.targetDimMm:F0}mm scale={scale:F3} fit={fit}");
}
private float GetDimensionValue(string dimensionId, AvatarDimensions dims)
{
return dimensionId switch
{
"chest_circumference" => dims.chest_circumference_mm,
"hip_circumference" => dims.hip_circumference_mm,
"hand_breadth" => dims.hand_breadth_mm,
"foot_length" => dims.foot_length_mm,
"head_circumference" => dims.head_circumference_mm,
"waist_circumference_natural" => dims.waist_circumference_mm,
"biacromial_breadth" => dims.shoulder_width_mm,
_ => dims.chest_circumference_mm,
};
}
}
Step 3: Visual fit feedback
When a character equips an item, tell the player how it fits. This isn’t just cosmetic — it’s a game mechanic. Poorly fitting armor could offer reduced protection; an oversized helmet could impair vision range; loose gloves could reduce attack speed.
// FitVisualController.cs — attach to equipment prefabs
using UnityEngine;
public class FitVisualController : MonoBehaviour
{
[Header("Animator for fit states (optional)")]
public Animator fitAnimator;
[Header("Renderer for fit-based material changes (optional)")]
public Renderer equipmentRenderer;
public Material tightMaterial; // tension/stretch effect
public Material regularMaterial;
public Material looseMaterial; // sag/excess fabric effect
public void SetFit(FitCategory fit, float scale)
{
if (fitAnimator != null)
fitAnimator.SetInteger("FitState", (int)fit);
if (equipmentRenderer != null)
{
equipmentRenderer.material = fit switch
{
FitCategory.Tight => tightMaterial ?? regularMaterial,
FitCategory.Loose => looseMaterial ?? regularMaterial,
FitCategory.Regular => regularMaterial,
_ => regularMaterial,
};
}
}
}
Step 4: Body-aware loot generation
Standard loot tables drop items with equal probability regardless of who picks them up. With character body data, you can bias drops toward items the current character can actually wear:
// LootGenerator.cs
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
[System.Serializable]
public class LootItem
{
public string itemId;
public string slotId;
public float targetDimMm; // the dimension this item was designed for
public float baseWeight; // base drop probability weight
}
public class LootGenerator : MonoBehaviour
{
private EquipmentFitSystem _fitSystem;
void Awake() => _fitSystem = GetComponent<EquipmentFitSystem>();
public LootItem RollLoot(List<LootItem> lootTable, AvatarDimensions charDims)
{
// Compute adjusted weights: items that fit well get higher weight
var weights = lootTable.Select(item => {
var config = DefaultSlotConfigs.All.Find(c => c.slotId == item.slotId);
if (config == null) return item.baseWeight;
float charDim = _fitSystem.GetDimensionValue_Public(config.governingDimension, charDims);
float fitScale = _fitSystem.GetFitScale(charDim, config);
float deviation = Mathf.Abs(1f - fitScale); // 0 = perfect fit
// Gaussian-shaped weight function: peak at perfect fit, falls off with deviation
float fitWeight = Mathf.Exp(-4f * deviation * deviation);
return item.baseWeight * (0.3f + 0.7f * fitWeight); // floor at 30% base weight
}).ToList();
// Weighted random selection
float total = weights.Sum();
float roll = Random.value * total;
float cumulative = 0f;
for (int i = 0; i < lootTable.Count; i++)
{
cumulative += weights[i];
if (roll <= cumulative) return lootTable[i];
}
return lootTable[lootTable.Count - 1];
}
}
This doesn’t guarantee the player gets perfectly fitting items — it tilts the distribution toward items they can use. A player who always gets armor that fits starts to feel the game is responding to them. That’s good game feel.
Performance considerations
AvatarDimensions is a small struct (a few floats). Cache it per character — don’t recalculate from height/weight on every equipment change. Bone scale changes are cheap. Recalculating scale each frame would be pointless; recalculate only when equipment changes or when the character’s body changes (if your game has growth, aging, or body-altering mechanics).
Limit bone scaling to the equipment’s primary affected region. Scaling the chest armor only affects the spine/chest bone, not the legs. Fine-grained scaling produces better visual results and avoids cascading transforms through the rig.