What Is It?
The Strategy pattern pulls a family of interchangeable algorithms out of the class that uses them. The user — the context — holds a reference to an interface like IAttackStrategy and calls Attack(target) without knowing whether a sword, a bow or a fireball is behind it.
Each concrete strategy (MeleeAttack, RangedAttack, MagicAttack) encapsulates its own range, damage and effects. Changing how the character fights means assigning a different object to one field, not editing an ever-growing if/else chain.
The strategies here are plain C# classes, but in production they are often ScriptableObjects: a designer creates a “Fire Staff” asset, tunes its numbers in the Inspector and drags it into the weapon slot. Picking up a new weapon at runtime simply swaps the reference.
When Is It Used?
Use it when one behaviour has several variants that you want to choose or change at runtime: weapons, AI targeting rules, movement modes, difficulty curves, pathfinding heuristics.
It is a good refactor whenever a method starts with switch (weaponType) and each branch is substantial. Each branch becomes a class with a single responsibility.
If there are only two tiny variants that never change at runtime, a simple parameter or a delegate (Func<float, float>) is lighter than a full interface and set of classes.
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.Behavioral.Strategy
{
/// <summary>
/// Client: picks strategies at runtime and tells the attacker to
/// attack. Swapping weapons is just reassigning a reference.
/// </summary>
public class StrategyTester : MonoBehaviour
{
[SerializeField] private Attacker attacker;
[SerializeField] private Transform dummy; // must have an IDamageable component
[SerializeField] private Rigidbody arrowPrefab;
private IAttackStrategy melee;
private IAttackStrategy ranged;
private IAttackStrategy magic;
private void Awake()
{
melee = new MeleeAttack();
ranged = new RangedAttack(arrowPrefab);
magic = new MagicAttack();
attacker.SetStrategy(melee);
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Alpha1)) attacker.SetStrategy(melee);
if (Input.GetKeyDown(KeyCode.Alpha2)) attacker.SetStrategy(ranged);
if (Input.GetKeyDown(KeyCode.Alpha3)) attacker.SetStrategy(magic);
if (Input.GetKeyDown(KeyCode.Space) && dummy.TryGetComponent(out IDamageable target))
attacker.AttackTarget(target);
}
}
}using UnityEngine;
namespace Patterns.Behavioral.Strategy
{
/// <summary>
/// The strategy contract. The attacker calls this without knowing
/// which concrete attack it is holding.
/// </summary>
public interface IAttackStrategy
{
string Name { get; }
float Range { get; }
void Attack(Transform attacker, IDamageable target);
}
/// <summary>Anything that can be hit.</summary>
public interface IDamageable
{
Transform Transform { get; }
void TakeDamage(int amount);
}
}using UnityEngine;
namespace Patterns.Behavioral.Strategy
{
/// <summary>Close-range, high-damage swing.</summary>
public class MeleeAttack : IAttackStrategy
{
public string Name => "Melee";
public float Range => 1.5f;
public void Attack(Transform attacker, IDamageable target)
{
target.TakeDamage(25);
}
}
/// <summary>Launches an arrow toward the target from a distance.</summary>
public class RangedAttack : IAttackStrategy
{
private readonly Rigidbody arrowPrefab;
public RangedAttack(Rigidbody arrowPrefab) => this.arrowPrefab = arrowPrefab;
public string Name => "Ranged";
public float Range => 15f;
public void Attack(Transform attacker, IDamageable target)
{
Vector3 direction = (target.Transform.position - attacker.position).normalized;
Rigidbody arrow = Object.Instantiate(arrowPrefab, attacker.position, Quaternion.LookRotation(direction));
arrow.linearVelocity = direction * 30f;
Object.Destroy(arrow.gameObject, 2f);
target.TakeDamage(12); // hitscan damage; the arrow is visual
}
}
/// <summary>Damages the target and everything near it.</summary>
public class MagicAttack : IAttackStrategy
{
private const float BlastRadius = 3f;
public string Name => "Magic";
public float Range => 10f;
public void Attack(Transform attacker, IDamageable target)
{
Collider[] hits = Physics.OverlapSphere(target.Transform.position, BlastRadius);
foreach (Collider hit in hits)
{
if (hit.TryGetComponent(out IDamageable victim))
victim.TakeDamage(18);
}
}
}
}using UnityEngine;
namespace Patterns.Behavioral.Strategy
{
/// <summary>
/// Context. Holds one attack strategy and delegates to it.
/// It never checks which kind of attack it is.
/// </summary>
public class Attacker : MonoBehaviour
{
private IAttackStrategy strategy;
public IAttackStrategy Strategy => strategy;
public void SetStrategy(IAttackStrategy next)
{
if (next == null || next == strategy) return;
strategy = next;
Debug.Log($"{name} switched to {strategy.Name}");
}
public void AttackTarget(IDamageable target)
{
if (strategy == null) return;
float distance = Vector3.Distance(transform.position, target.Transform.position);
if (distance > strategy.Range)
{
Debug.Log($"{strategy.Name}: target out of range ({distance:0.0}m)");
return;
}
strategy.Attack(transform, target);
}
}
}Advantages & Disadvantages
+ Advantages
- Algorithms are interchangeable at runtime without touching the context.
- New variants are added as new classes, leaving existing, tested code alone.
- Each strategy is small and isolated, which makes it easy to tune and unit test.
− Disadvantages
- More types to manage; trivial variations may not justify their own class.
- Callers must know enough about the strategies to choose the right one.
- Strategies with very different data needs can force a bloated, lowest-common-denominator interface.
Tips
- 01Make strategies
ScriptableObjectassets so designers can create and balance variants without code changes. - 02Keep strategies stateless where possible, so a single asset can be shared by many attackers.
- 03Pass a small context (attacker transform, target, stats) into the method instead of giving strategies references to everything.
- 04Strategy and State look alike in code. The difference is intent: a strategy is chosen from outside, while states trigger their own transitions.
- 05For one-off behaviours, a C# delegate or lambda is a perfectly valid lightweight strategy.