Table of Contents
- What is the Composite Pattern
- Use Cases in Unity
- Implementation Approaches
- Performance Optimization Considerations
- Best Practices
- Conclusion
What is the Composite Pattern
The Composite Pattern is a structural design pattern that allows you to compose objects into tree structures to represent “part-whole” hierarchies. It lets clients treat individual objects and compositions of objects uniformly.
Core Concepts
- Component: Defines the interface for objects in the tree structure
- Leaf: A leaf node in the tree; it has no children
- Composite: A branch node in the tree; it can contain child nodes
UML Class Diagram
Component
├── Operation()
├── Add(Component)
├── Remove(Component)
└── GetChild(int)
↑
┌────┴────┐
Leaf Composite
├── children: List<Component>
├── Operation()
├── Add(Component)
├── Remove(Component)
└── GetChild(int)
Use Cases in Unity
In Unity development, the Composite Pattern has several typical use cases:
- UI System: Panels containing multiple UI elements forming a tree
- GameObject Hierarchy: GameObject parent-child relationships naturally fit the pattern
- Skill Systems: Composite skills built from multiple sub-skills
- Equipment Systems: Sets composed of multiple individual items
- AI Behavior Trees: Composition and nesting of behavior nodes
- Scene Management: Areas containing multiple sub-areas or game objects
Implementation Approaches
Approach 1: Classic Composite Implementation
This is the most standard implementation and suits scenarios that require strict adherence to the pattern.
using System.Collections.Generic;
using UnityEngine;
// Abstract component
public abstract class GameComponent
{
protected string name;
protected Transform transform;
public GameComponent(string name)
{
this.name = name;
}
public abstract void Execute();
public abstract void Add(GameComponent component);
public abstract void Remove(GameComponent component);
public abstract GameComponent GetChild(int index);
public abstract int GetChildCount();
}
// Leaf node - single game object
public class GameLeaf : GameComponent
{
private GameObject gameObject;
public GameLeaf(string name, GameObject gameObject) : base(name)
{
this.gameObject = gameObject;
this.transform = gameObject.transform;
}
public override void Execute()
{
Debug.Log($"Executing leaf: {name}");
// Run specific game logic
if (gameObject != null)
{
// e.g., play animation, move, attack, etc.
var animator = gameObject.GetComponent<Animator>();
if (animator != null)
{
animator.SetTrigger("Execute");
}
}
}
// Leaf nodes do not support children
public override void Add(GameComponent component)
{
throw new System.NotSupportedException("Cannot add component to a leaf");
}
public override void Remove(GameComponent component)
{
throw new System.NotSupportedException("Cannot remove component from a leaf");
}
public override GameComponent GetChild(int index)
{
throw new System.NotSupportedException("Leaf has no children");
}
public override int GetChildCount()
{
return 0;
}
}
// Composite node - group of game objects
public class GameComposite : GameComponent
{
private List<GameComponent> children = new List<GameComponent>();
private GameObject rootObject;
public GameComposite(string name, GameObject rootObject = null) : base(name)
{
this.rootObject = rootObject;
if (rootObject != null)
{
this.transform = rootObject.transform;
}
}
public override void Execute()
{
Debug.Log($"Executing composite: {name}");
// Execute its own logic first
if (rootObject != null)
{
var animator = rootObject.GetComponent<Animator>();
if (animator != null)
{
animator.SetTrigger("Execute");
}
}
// Then execute all children
foreach (var child in children)
{
child.Execute();
}
}
public override void Add(GameComponent component)
{
children.Add(component);
// If there is a GameObject, establish parent-child relation
if (transform != null && component.transform != null)
{
component.transform.SetParent(transform);
}
}
public override void Remove(GameComponent component)
{
children.Remove(component);
// Detach parent-child relation
if (component.transform != null)
{
component.transform.SetParent(null);
}
}
public override GameComponent GetChild(int index)
{
if (index >= 0 && index < children.Count)
{
return children[index];
}
return null;
}
public override int GetChildCount()
{
return children.Count;
}
}
// Usage example
public class CompositeExample : MonoBehaviour
{
void Start()
{
// Create root composite
var army = new GameComposite("Army");
// Create infantry squad
var infantrySquad = new GameComposite("Infantry Squad");
infantrySquad.Add(new GameLeaf("Soldier1", CreateSoldier("Soldier1")));
infantrySquad.Add(new GameLeaf("Soldier2", CreateSoldier("Soldier2")));
infantrySquad.Add(new GameLeaf("Soldier3", CreateSoldier("Soldier3")));
// Create tank squad
var tankSquad = new GameComposite("Tank Squad");
tankSquad.Add(new GameLeaf("Tank1", CreateTank("Tank1")));
tankSquad.Add(new GameLeaf("Tank2", CreateTank("Tank2")));
// Assemble army
army.Add(infantrySquad);
army.Add(tankSquad);
// Execute the entire army's action
army.Execute();
}
private GameObject CreateSoldier(string name)
{
var soldier = GameObject.CreatePrimitive(PrimitiveType.Capsule);
soldier.name = name;
return soldier;
}
private GameObject CreateTank(string name)
{
var tank = GameObject.CreatePrimitive(PrimitiveType.Cube);
tank.name = name;
tank.transform.localScale = Vector3.one * 2f;
return tank;
}
}
Pros:
- Strictly adheres to Composite design principles
- Type-safe with clear structure
- Supports arbitrary depth of nesting
- Easy to extend with new component types
Cons:
- More code and complexity
- Might be over-engineered for simple scenarios
- Extra memory overhead to maintain the tree structure
Approach 2: Unity GameObject-based Implementation
Leverages Unity’s native GameObject parent-child relationships—this is the most “Unity-ish” approach.
using System.Collections.Generic;
using UnityEngine;
// Game component interface
public interface IGameComponent
{
void Execute();
string GetName();
Transform GetTransform();
}
// Base component class
public abstract class BaseGameComponent : MonoBehaviour, IGameComponent
{
[SerializeField] protected string componentName;
protected virtual void Awake()
{
if (string.IsNullOrEmpty(componentName))
{
componentName = gameObject.name;
}
}
public abstract void Execute();
public string GetName()
{
return componentName;
}
public Transform GetTransform()
{
return transform;
}
}
// Leaf component - single unit
public class UnitComponent : BaseGameComponent
{
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private Vector3 targetPosition;
[SerializeField] private bool hasTarget = false;
public override void Execute()
{
Debug.Log($"Unit {componentName} executing action");
if (hasTarget)
{
MoveToTarget();
}
else
{
PerformDefaultAction();
}
}
private void MoveToTarget()
{
transform.position = Vector3.MoveTowards(
transform.position,
targetPosition,
moveSpeed * Time.deltaTime
);
if (Vector3.Distance(transform.position, targetPosition) < 0.1f)
{
hasTarget = false;
Debug.Log($"{componentName} reached target!");
}
}
private void PerformDefaultAction()
{
// Default behavior, e.g., patrol
transform.Rotate(0, 45 * Time.deltaTime, 0);
}
public void SetTarget(Vector3 target)
{
targetPosition = target;
hasTarget = true;
}
}
// Composite component - group of units
public class GroupComponent : BaseGameComponent
{
[SerializeField] private bool executeInSequence = false;
[SerializeField] private float sequenceDelay = 0.5f;
private List<IGameComponent> childComponents = new List<IGameComponent>();
private bool isInitialized = false;
protected override void Awake()
{
base.Awake();
InitializeChildren();
}
private void InitializeChildren()
{
if (isInitialized) return;
childComponents.Clear();
// Get components from all direct children
for (int i = 0; i < transform.childCount; i++)
{
var child = transform.GetChild(i);
var component = child.GetComponent<IGameComponent>();
if (component != null)
{
childComponents.Add(component);
}
}
isInitialized = true;
}
public override void Execute()
{
Debug.Log($"Group {componentName} executing with {childComponents.Count} children");
if (executeInSequence)
{
StartCoroutine(ExecuteSequentially());
}
else
{
ExecuteSimultaneously();
}
}
private void ExecuteSimultaneously()
{
foreach (var child in childComponents)
{
child.Execute();
}
}
private System.Collections.IEnumerator ExecuteSequentially()
{
foreach (var child in childComponents)
{
child.Execute();
yield return new WaitForSeconds(sequenceDelay);
}
}
// Dynamically add a child component
public void AddChild(GameObject childObject)
{
childObject.transform.SetParent(transform);
var component = childObject.GetComponent<IGameComponent>();
if (component != null && !childComponents.Contains(component))
{
childComponents.Add(component);
}
}
// Remove a child component
public void RemoveChild(GameObject childObject)
{
var component = childObject.GetComponent<IGameComponent>();
if (component != null)
{
childComponents.Remove(component);
}
childObject.transform.SetParent(null);
}
// Reinitialize the child component list
public void RefreshChildren()
{
isInitialized = false;
InitializeChildren();
}
}
// Advanced group component - supports more complex operations
public class AdvancedGroupComponent : GroupComponent
{
[SerializeField] private GroupFormation formation = GroupFormation.Line;
[SerializeField] private float spacing = 2f;
[SerializeField] private bool autoArrange = true;
public enum GroupFormation
{
Line,
Circle,
Grid,
Wedge
}
void Start()
{
if (autoArrange)
{
ArrangeFormation();
}
}
public void ArrangeFormation()
{
var children = new List<Transform>();
for (int i = 0; i < transform.childCount; i++)
{
children.Add(transform.GetChild(i));
}
switch (formation)
{
case GroupFormation.Line:
ArrangeInLine(children);
break;
case GroupFormation.Circle:
ArrangeInCircle(children);
break;
case GroupFormation.Grid:
ArrangeInGrid(children);
break;
case GroupFormation.Wedge:
ArrangeInWedge(children);
break;
}
}
private void ArrangeInLine(List<Transform> children)
{
for (int i = 0; i < children.Count; i++)
{
children[i].localPosition = new Vector3(i * spacing, 0, 0);
}
}
private void ArrangeInCircle(List<Transform> children)
{
float angleStep = 360f / children.Count;
for (int i = 0; i < children.Count; i++)
{
float angle = i * angleStep * Mathf.Deg2Rad;
Vector3 position = new Vector3(
Mathf.Cos(angle) * spacing,
0,
Mathf.Sin(angle) * spacing
);
children[i].localPosition = position;
}
}
private void ArrangeInGrid(List<Transform> children)
{
int gridSize = Mathf.CeilToInt(Mathf.Sqrt(children.Count));
for (int i = 0; i < children.Count; i++)
{
int x = i % gridSize;
int z = i / gridSize;
children[i].localPosition = new Vector3(x * spacing, 0, z * spacing);
}
}
private void ArrangeInWedge(List<Transform> children)
{
for (int i = 0; i < children.Count; i++)
{
float offset = (i - children.Count / 2f) * spacing;
children[i].localPosition = new Vector3(offset, 0, -i * spacing * 0.5f);
}
}
}
Pros:
- Fully leverages Unity’s GameObject system
- Visual editing and easy debugging
- Parent-child relationships handled automatically
- Supports Inspector configuration
- Memory efficient
Cons:
- Dependent on Unity’s MonoBehaviour system
- Not ideal for purely logical composites
- Constrained by GameObject lifecycle
Approach 3: ScriptableObject-based Implementation
Ideal for data-driven composites, especially for skills, equipment, and configuration systems.
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "New Skill", menuName = "Game/Skill")]
public abstract class SkillData : ScriptableObject
{
[SerializeField] protected string skillName;
[SerializeField] protected Sprite icon;
[SerializeField] protected float cooldown;
[SerializeField] protected int manaCost;
public abstract void Execute(GameObject caster, GameObject target = null);
public abstract float GetDuration();
public virtual string GetDescription() => skillName;
}
[CreateAssetMenu(fileName = "New Basic Skill", menuName = "Game/Basic Skill")]
public class BasicSkill : SkillData
{
[SerializeField] private int damage;
[SerializeField] private float range;
[SerializeField] private GameObject effectPrefab;
public override void Execute(GameObject caster, GameObject target = null)
{
Debug.Log($"Executing basic skill: {skillName}");
if (target != null && Vector3.Distance(caster.transform.position, target.transform.position) <= range)
{
// Apply damage
var health = target.GetComponent<Health>();
if (health != null)
{
health.TakeDamage(damage);
}
// Play VFX
if (effectPrefab != null)
{
Instantiate(effectPrefab, target.transform.position, Quaternion.identity);
}
}
}
public override float GetDuration()
{
return 0.5f; // Instant cast
}
}
[CreateAssetMenu(fileName = "New Combo Skill", menuName = "Game/Combo Skill")]
public class ComboSkill : SkillData
{
[SerializeField] private List<SkillData> subSkills = new List<SkillData>();
[SerializeField] private float delayBetweenSkills = 0.3f;
[SerializeField] private bool executeSimultaneously = false;
public override void Execute(GameObject caster, GameObject target = null)
{
Debug.Log($"Executing combo skill: {skillName}");
if (executeSimultaneously)
{
ExecuteSimultaneously(caster, target);
}
else
{
var skillExecutor = caster.GetComponent<SkillExecutor>();
if (skillExecutor != null)
{
skillExecutor.StartCoroutine(ExecuteSequentially(caster, target));
}
}
}
private void ExecuteSimultaneously(GameObject caster, GameObject target)
{
foreach (var skill in subSkills)
{
if (skill != null)
{
skill.Execute(caster, target);
}
}
}
private System.Collections.IEnumerator ExecuteSequentially(GameObject caster, GameObject target)
{
foreach (var skill in subSkills)
{
if (skill != null)
{
skill.Execute(caster, target);
yield return new WaitForSeconds(delayBetweenSkills);
}
}
}
public override float GetDuration()
{
if (executeSimultaneously)
{
float maxDuration = 0f;
foreach (var skill in subSkills)
{
if (skill != null)
{
maxDuration = Mathf.Max(maxDuration, skill.GetDuration());
}
}
return maxDuration;
}
else
{
float totalDuration = 0f;
foreach (var skill in subSkills)
{
if (skill != null)
{
totalDuration += skill.GetDuration() + delayBetweenSkills;
}
}
return totalDuration;
}
}
public void AddSkill(SkillData skill)
{
if (skill != null && !subSkills.Contains(skill))
{
subSkills.Add(skill);
}
}
public void RemoveSkill(SkillData skill)
{
subSkills.Remove(skill);
}
}
// Skill executor
public class SkillExecutor : MonoBehaviour
{
[SerializeField] private List<SkillData> availableSkills = new List<SkillData>();
private Dictionary<SkillData, float> skillCooldowns = new Dictionary<SkillData, float>();
void Update()
{
// Update cooldowns
var keys = new List<SkillData>(skillCooldowns.Keys);
foreach (var skill in keys)
{
skillCooldowns[skill] -= Time.deltaTime;
if (skillCooldowns[skill] <= 0)
{
skillCooldowns.Remove(skill);
}
}
}
public bool CanUseSkill(SkillData skill)
{
return !skillCooldowns.ContainsKey(skill);
}
public void UseSkill(SkillData skill, GameObject target = null)
{
if (CanUseSkill(skill))
{
skill.Execute(gameObject, target);
skillCooldowns[skill] = skill.cooldown;
}
}
}
Pros:
- Separation of data and logic
- Supports dynamic composition at runtime
- Easy serialization and persistence
- Asset references and reuse
- Convenient for designer-driven configuration
Cons:
- Requires an extra executor component
- Less suitable for complex runtime logic
- Harder to debug
Approach 4: Lightweight Interface-based Implementation
Suited for performance-sensitive scenarios with minimal memory footprint.
using System.Collections.Generic;
using UnityEngine;
public interface IExecutable
{
void Execute();
}
public interface IComposite : IExecutable
{
void Add(IExecutable executable);
void Remove(IExecutable executable);
IExecutable GetChild(int index);
int GetChildCount();
}
// Lightweight leaf implementation
public struct ActionLeaf : IExecutable
{
private System.Action action;
public ActionLeaf(System.Action action)
{
this.action = action;
}
public void Execute()
{
action?.Invoke();
}
}
// Lightweight composite implementation
public class ActionComposite : IComposite
{
private List<IExecutable> children;
private System.Action preAction;
private System.Action postAction;
public ActionComposite(System.Action preAction = null, System.Action postAction = null)
{
this.children = new List<IExecutable>();
this.preAction = preAction;
this.postAction = postAction;
}
public void Execute()
{
preAction?.Invoke();
for (int i = 0; i < children.Count; i++)
{
children[i].Execute();
}
postAction?.Invoke();
}
public void Add(IExecutable executable)
{
children.Add(executable);
}
public void Remove(IExecutable executable)
{
children.Remove(executable);
}
public IExecutable GetChild(int index)
{
return index >= 0 && index < children.Count ? children[index] : null;
}
public int GetChildCount()
{
return children.Count;
}
}
// Usage example
public class LightweightCompositeExample : MonoBehaviour
{
void Start()
{
// Create a composite action
var moveAndAttack = new ActionComposite(
preAction: () => Debug.Log("Starting move and attack sequence"),
postAction: () => Debug.Log("Completed move and attack sequence")
);
// Add move action
moveAndAttack.Add(new ActionLeaf(() => {
Debug.Log("Moving forward");
transform.Translate(Vector3.forward);
}));
// Add attack action
moveAndAttack.Add(new ActionLeaf(() => {
Debug.Log("Attacking!");
// Attack logic
}));
// Add a sub-composite
var multiAttack = new ActionComposite();
multiAttack.Add(new ActionLeaf(() => Debug.Log("Attack 1")));
multiAttack.Add(new ActionLeaf(() => Debug.Log("Attack 2")));
multiAttack.Add(new ActionLeaf(() => Debug.Log("Attack 3")));
moveAndAttack.Add(multiAttack);
// Execute the entire sequence
moveAndAttack.Execute();
}
}
Pros:
- Very low memory footprint
- High execution efficiency
- Concise code
- Supports lambda expressions
Cons:
- Limited functionality
- No complex state management
- Harder to debug
- Lower type safety
Performance Optimization Considerations
1. Object Pooling
public class CompositeObjectPool : MonoBehaviour
{
private Queue<GameComposite> compositePool = new Queue<GameComposite>();
private Queue<GameLeaf> leafPool = new Queue<GameLeaf>();
public GameComposite GetComposite(string name)
{
if (compositePool.Count > 0)
{
var composite = compositePool.Dequeue();
composite.Reset(name);
return composite;
}
return new GameComposite(name);
}
public void ReturnComposite(GameComposite composite)
{
composite.Clear();
compositePool.Enqueue(composite);
}
}
2. Lazy Loading
public class LazyComposite : IComposite
{
private List<IExecutable> children;
private System.Func<List<IExecutable>> childrenProvider;
private bool isLoaded = false;
public LazyComposite(System.Func<List<IExecutable>> childrenProvider)
{
this.childrenProvider = childrenProvider;
}
private void EnsureLoaded()
{
if (!isLoaded)
{
children = childrenProvider?.Invoke() ?? new List<IExecutable>();
isLoaded = true;
}
}
public void Execute()
{
EnsureLoaded();
foreach (var child in children)
{
child.Execute();
}
}
}
3. Caching Optimization
public class CachedComposite : IComposite
{
private List<IExecutable> children = new List<IExecutable>();
private bool isDirty = true;
private List<IExecutable> cachedExecutionList;
public void Execute()
{
if (isDirty)
{
RefreshExecutionCache();
isDirty = false;
}
for (int i = 0; i < cachedExecutionList.Count; i++)
{
cachedExecutionList[i].Execute();
}
}
private void RefreshExecutionCache()
{
cachedExecutionList = new List<IExecutable>(children);
}
public void Add(IExecutable executable)
{
children.Add(executable);
isDirty = true;
}
}
Best Practices
1. Choose the Right Approach
- UI System: GameObject-based approach
- Skills/Equipment Systems: ScriptableObject-based approach
- AI Behavior: Interface-based approach
- Complex Game Logic: Classic approach
2. Avoid Excessive Nesting
// Not recommended: excessive nesting
var root = new GameComposite("Root");
var level1 = new GameComposite("Level1");
var level2 = new GameComposite("Level2");
var level3 = new GameComposite("Level3");
// ... continue nesting
// Recommended: flatter structure
var root = new GameComposite("Root");
root.Add(new GameLeaf("Action1", gameObject1));
root.Add(new GameLeaf("Action2", gameObject2));
root.Add(new GameLeaf("Action3", gameObject3));
3. Combine Design Patterns Appropriately
// Composite + Command pattern
public class CommandComposite : IComposite
{
private List<ICommand> commands = new List<ICommand>();
public void Execute()
{
foreach (var command in commands)
{
command.Execute();
}
}
public void Add(IExecutable executable)
{
if (executable is ICommand command)
{
commands.Add(command);
}
}
}
// Composite + Observer pattern
public class ObservableComposite : IComposite
{
public System.Action<IExecutable> OnChildExecuted;
private List<IExecutable> children = new List<IExecutable>();
public void Execute()
{
foreach (var child in children)
{
child.Execute();
OnChildExecuted?.Invoke(child);
}
}
}
4. Error Handling and Safety Checks
public class SafeComposite : IComposite
{
private List<IExecutable> children = new List<IExecutable>();
public void Execute()
{
for (int i = children.Count - 1; i >= 0; i--)
{
try
{
if (children[i] != null)
{
children[i].Execute();
}
else
{
children.RemoveAt(i); // Clean up null references
}
}
catch (System.Exception e)
{
Debug.LogError($"Error executing child {i}: {e.Message}");
}
}
}
}
Conclusion
The Composite Pattern has multiple implementation strategies in Unity, each suited to different scenarios:
- Classic implementation: Best for complex business logic, offers maximum flexibility
- GameObject-based implementation: The most Unity-native approach for scene object management
- ScriptableObject-based implementation: Data-driven, ideal for configuration systems
- Lightweight implementation: Performance-first, suitable for simple composite operations
When choosing an approach, consider:
- Performance needs: Lightweight > GameObject > ScriptableObject > Classic
- Maintainability: Classic > ScriptableObject > GameObject > Lightweight
- Unity integration: GameObject > ScriptableObject > Classic > Lightweight
Proper use of the Composite Pattern can make a Unity project’s architecture clearer and easier to maintain and extend. In practice, choose the approach that fits your specific needs, and pay attention to performance optimization and error handling.

