Unity

[Unity] 디자인 패턴 : Strategy 패턴

sunlight-dby 2025. 6. 17. 22:44

 

 

이 글은 해당 유튜브를 보고 진행한 것에 대해 공부한 것을 정리한 글입니다.

 


 

전략 패턴 (Strategy Pattern)

전략 패턴은 다양한 알고리즘 또는 동작을 하나의 추상적인 접근점으로 만들어, 접근점에서 알고리즘이 서로 교환 가능하도록 하는 패턴입니다.

전략 패턴의 도식화

 


특징

  • 알고리즘의 캡슐화와 상호 교환
    • 전략 패턴은 동일 계열의 알고리즘군을 정의하고, 각각의 알고리즘을 캡슐화하여 상호 교환이 가능하도록 만듭니다.
    • 객체 내에서 알고리즘이나 동작을 래핑하고, 상황에 따라 동적으로 교체할 수 있게 합니다.
    • 전략과 다른 컴포넌트의 결합도를 낮게 유지해야 합니다.
  • 동작의 유연한 변경 가능
    • 알고리즘을 사용하는 객체는 구체적인 전략 구현을 몰라도 되며, 런타임 시 알고리즘을 교체할 수 있습니다.
    • 각 전략은 자체적으로 캡슐화되어 있기 때문에, 다른 게임플레이 시스템(이벤트 시스템 등)과 어떻게 정보를 공유하고 통신할지 신중하게 디자인해야 합니다.

사용 용도

  • 많은 변형을 가진 알고리즘이 존재할 때
    • 행동들이 조금씩 다를 뿐 개념적으로 관련된 많은 클래스들이 존재할 때 유용합니다.
    • 서로 비슷하지만 세부 동작이 다른 클래스들이 많은 경우에 유용합니다.
    • 알고리즘의 변형이 필요할 때, 이러한 변형물들이 알고리즘의 상속 관계로 구현될 때 사용 가능합니다.
  • 복잡한 조건문이 있을 때
    • 하나의 클래스 내에서 if - else 또는 switch로 복잡하게 분기되는 로직은 유지보수가 어려워집니다. 전략 패턴으로 분리하면 개방/폐쇄 원칙을 지킬 수 있습니다.
  • 클라이언트가 알 필요 없는 알고리즘이 있을 때
    • 구현 세부사항을 감추고, 외부에서는 어떤 전략이 쓰였는지 노출하지 말아야 할때 전략 패턴을 적용합니다.

장점

  • 런타임에 동작을 유용하게 변경할 수 있습니다.
  • 새로운 전략을 추가할 때, 기존 코드를 수정하지 않아도 되므로 OCP(개방 - 폐쇄 원칙)를 충실히 준수하면서 더 유연한 시스템을 만드는 것이 가능합니다.
  • 각 동작(전략)이 각각의 독립된 클래스로 깔끔하게 구분되므로 테스트가 쉽고 유지보수가 용이합니다.

단점

  • 전략 클래스가 많아지면서 관리해야 할 객체 수가 증가하고 복잡도가 높아질 수 있습니다.
  • 객체 간 메시지 전달 및 전략 오브젝트로 인해 메모리 오버헤드가 발생할 수 있습니다.
    • 성능이 중요한 곳에서는 다른 패턴 또는 최적화 방법을 고려하는 등 주의가 필요합니다.

전략 패턴 사용 예시

캐릭터 이동 전략

  • 주변 환경이나 파워업에 따라 플레이어 캐릭터의 이동 능력이 업그레이드되는 플랫포머 게임에 사용할 수 있습니다.
  • 처음에 플레이어는 걷기와 점프만 가능하지만 나중에는 이중 점프, 대시, 날기 능력을 얻을 수 있습니다.

AI 동작

  • 게임 상태나 플레이어의 행동에 따라 다른 AI 동작으로 전환할 수 있습니다.
  • 플레이어의 행동에 따라 적의 상태를 공격, 방어, 순찰 전략으로 변경할 수 있습니다.

탐색 전략

  • 경로 탐색 시스템을 만들었다면, 전략 패턴을 사용하면 컨텍스트에 따라 게임플레이 중에 서로 교체할 수 있는 여러 알고리즘(A*, Dijkstra의 최단 경로 등)을 정의할 수 있습니다.

공격 전략

  • 플레이어나 AI가 MeleeAttack, RangedAttack, AreaEffectAttack 같은 전략으로 무기 유형을 동적으로 전환할 수 있도록 할 수 있습니다.
  • 남은 체력에 따라 모드나 고유 전투 능력이 변하는 적 보스를 만들 수 있습니다.

난이도 조정

  • 플레이어의 성과에 따라 게임의 난이도를 자동으로 조정할 수 있습니다. 실시간으로 변하는 '적응적 난이도' 전략을 구현할 수 있습니다.

구현 예시 : 리팩터링 이전 코드

이 스크립트는 게임이 발전함에 따라 점점 더 복잡해지고 관리하기 어려워집니다.

새로운 능력을 추가할 때마다 기존 코드를 수정해야 하며, 이는 개방 - 폐쇄 원칙에 위반됩니다.

public class AbilityRunner : MonoBehaviour
{
   public enum Ability
   {
       RadarPulse,
       AirSupport,
       FirstAid
   }
   
   public Ability currentAbility;
   
   void Update()
   {
       if (Input.GetKeyDown(KeyCode.Space))
       {
           ActivateAbility(currentAbility);
       }
   }
   
   void ActivateAbility(Ability ability)
   {
       switch (ability)
       {
           case Ability.RadarPulse:
               // Radar Pulse
               Debug.Log(“Activating Radar Pulse”);
               break;
           case Ability.AirSupport:
               // Air Support
               Debug.Log(“Calling in Air Support”);
               break;
           case Ability.FirstAid:
               // First Aid/Healing
               Debug.Log(“Using First Aid”);
               break;
       }
   }
}

구현 예시 : 전략 패턴 적용

Ability 추상 클래스를 생성하고, 각 특수 능력의 Ability 클래스를 구체적으로 구현합니다.

해당 스크립트에서는 스크립터블 오브젝트를 확장하지만, 다른 오브젝트도 사용 가능합니다.


Ability 추상 클래스 정의

public abstract class Ability : ScriptableObject
{
   [SerializeField] protected string abilityName;
   
   public virtual void Use(GameObject gameObject)
   {
       Debug.Log($"Using ability : {abilityName}");
       PlaySound();
       PlayParticleFX();
   }
}

구체 전략 클래스 예시

[CreateAssetMenu(fileName = “RadarPulseAbility”, menuName = “Abilities/RadarPulse”)]
public class RadarPulse : Ability
{
   public override void Use(GameObject gameObject)
   {
       Debug.Log(“Activating Radar Pulse”);
       // 여기에 Radar Pulse 로직 구현
   }
}
 
[CreateAssetMenu(fileName = “AirSupportAbility”, menuName = “Abilities/AirSupport”)]
public class AirSupport : Ability
{
   public override void Use(GameObject gameObject)
   {
       Debug.Log(“Calling in Air Support”);
       // 여기에 Air Support 로직 구현
   }
}
 
[CreateAssetMenu(fileName = “FirstAidAbility”, menuName = “Abilities/ FirstAid”)]
public class FirstAid : Ability
{
   public override void Use(GameObject gameObject)
   {
       Debug.Log(“Using First Aid”);
   }
}

전략 클래스 적용

 public class AbilityRunner : MonoBehaviour
 {
 // Unity 에디터를 통해 할당
    public Ability currentAbility;
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            currentAbility.Use(gameObject);
        }
    }
 }

Unity 내 전략 패턴 활용 사례

Renderer Feature의 Add Render Feature 기능

Renderer Feature의 전략 패턴 도식

 

 

 

Unity의 SRP 또는 URP에서 사용자 정의 렌더링 기능을 추가하는 구조도 전략 패턴의 적용이 되어있습니다.

각 렌더링 기능이 별도 클래스로 정의되고, 필요에 따라 런타임에 주입되어 처리됩니다.