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.
Top comments (0)