What Is It?
The Builder pattern separates how an object is assembled from the object itself. Instead of a constructor with nine optional parameters, you call small, named steps — WithHead, WithArmor, WithWeapon — and finish with Build().
In C# builders are usually fluent: every step returns the builder, so the whole recipe reads as a single chained expression. A separate Director can store reusable recipes ("Knight", "Archer") that drive the same builder in a fixed order.
In Unity this is a natural fit for procedurally assembled characters, loot with random affixes, or dungeon rooms: the builder collects choices, validates them, and only at Build() instantiates prefabs and wires components together.
When Is It Used?
Reach for it when an object has many optional parts or settings and most combinations are valid — character customisers, weapon modding, enemy variants for a wave spawner.
It also helps when construction must happen in a particular order or must be validated before the object goes live, such as ensuring a ranged unit always has a weapon.
Skip it for simple objects with two or three fields. A plain constructor, object initialiser or a ScriptableObject preset is less ceremony.
Interactive Demo
Click the controls and watch the objects collaborate. The console mirrors what the C# code below would log.
Code
using UnityEngine;
namespace Patterns.Creational.Builder
{
/// <summary>
/// Client: builds a custom character by hand and two presets
/// through the director, all from the same builder instance.
/// </summary>
public class BuilderTester : MonoBehaviour
{
[SerializeField] private Character characterPrefab;
[SerializeField] private float spacing = 2f;
private void Start()
{
var builder = new CharacterBuilder(characterPrefab);
var director = new CharacterDirector(builder);
Character mage = builder
.Named("Mage")
.WithHead(HeadType.Hood)
.WithWeapon(WeaponType.Staff)
.WithColor(new Color(0.55f, 0.35f, 0.9f))
.Build(Vector3.zero);
Character knight = director.BuildKnight(Vector3.right * spacing);
Character archer = director.BuildArcher(Vector3.right * spacing * 2f);
Debug.Log($"{mage.DisplayName} DEF {mage.Defense}");
Debug.Log($"{knight.DisplayName} DEF {knight.Defense}");
Debug.Log($"{archer.DisplayName} DEF {archer.Defense}");
}
}
}using UnityEngine;
namespace Patterns.Creational.Builder
{
public enum HeadType { Bare, Helmet, Hood }
public enum ArmorType { None, Leather, Plate }
public enum WeaponType { None, Sword, Bow, Staff }
/// <summary>
/// The product. Its data is only set by CharacterBuilder, so once
/// built it cannot drift into an invalid combination.
/// </summary>
public class Character : MonoBehaviour
{
public string DisplayName { get; private set; }
public HeadType Head { get; private set; }
public ArmorType Armor { get; private set; }
public WeaponType Weapon { get; private set; }
public Color Tint { get; private set; }
internal void Init(string displayName, HeadType head, ArmorType armor, WeaponType weapon, Color tint)
{
DisplayName = displayName;
Head = head;
Armor = armor;
Weapon = weapon;
Tint = tint;
foreach (var r in GetComponentsInChildren<Renderer>())
r.material.color = tint;
gameObject.name = displayName;
}
public int Defense
{
get
{
int armorValue = Armor switch
{
ArmorType.Plate => 12,
ArmorType.Leather => 5,
_ => 0,
};
return armorValue + (Head == HeadType.Helmet ? 3 : 0);
}
}
}
}using UnityEngine;
namespace Patterns.Creational.Builder
{
/// <summary>
/// Fluent builder. Steps only record choices; Build() validates them,
/// instantiates the prefab and resets for the next character.
/// </summary>
public class CharacterBuilder
{
private readonly Character prefab;
private string displayName;
private HeadType head;
private ArmorType armor;
private WeaponType weapon;
private Color tint;
public CharacterBuilder(Character prefab)
{
this.prefab = prefab;
Reset();
}
public CharacterBuilder Named(string value) { displayName = value; return this; }
public CharacterBuilder WithHead(HeadType value) { head = value; return this; }
public CharacterBuilder WithArmor(ArmorType value) { armor = value; return this; }
public CharacterBuilder WithWeapon(WeaponType value) { weapon = value; return this; }
public CharacterBuilder WithColor(Color value) { tint = value; return this; }
public Character Build(Vector3 position)
{
if (weapon == WeaponType.None)
Debug.LogWarning($"{displayName} was built unarmed.");
Character result = Object.Instantiate(prefab, position, Quaternion.identity);
result.Init(displayName, head, armor, weapon, tint);
Reset();
return result;
}
public void Reset()
{
displayName = "Character";
head = HeadType.Bare;
armor = ArmorType.None;
weapon = WeaponType.None;
tint = Color.white;
}
}
}using UnityEngine;
namespace Patterns.Creational.Builder
{
/// <summary>
/// Director: knows the recipes for common archetypes and drives
/// a builder through the steps in the right order.
/// </summary>
public class CharacterDirector
{
private readonly CharacterBuilder builder;
public CharacterDirector(CharacterBuilder builder) => this.builder = builder;
public Character BuildKnight(Vector3 position) => builder
.Named("Knight")
.WithHead(HeadType.Helmet)
.WithArmor(ArmorType.Plate)
.WithWeapon(WeaponType.Sword)
.WithColor(new Color(0.75f, 0.8f, 0.9f))
.Build(position);
public Character BuildArcher(Vector3 position) => builder
.Named("Archer")
.WithHead(HeadType.Hood)
.WithArmor(ArmorType.Leather)
.WithWeapon(WeaponType.Bow)
.WithColor(new Color(0.3f, 0.7f, 0.35f))
.Build(position);
}
}Advantages & Disadvantages
+ Advantages
- Construction code is self-documenting:
.WithArmor(Plate).WithWeapon(Sword)reads like a sentence. - Directors turn common configurations into one-line presets, reused by spawners, editor tools and tests.
- The product can be immutable after
Build(), because all mutation happens inside the builder.
− Disadvantages
- Adds an extra class (or two) per product type, which is noise for small objects.
- It is easy to forget to reset a builder between uses and leak parts into the next product.
- Required parts are not enforced by the compiler; you must validate in
Build().
Tips
- 01Return
thisfrom every step to make the API fluent, and keep step names verb-like (WithX,AddX). - 02Reset internal state at the end of
Build()so the same builder instance can be reused safely by a spawner. - 03Keep the builder a plain C# class; only touch
Instantiateand components insideBuild()so the steps are cheap and testable. - 04Store Director recipes as ScriptableObjects if designers should be able to author new presets without code.
- 05Log an error in
Build()when a required part is missing — fail at construction time, not in the middle of combat.