본문 바로가기

유니티/디자인 패턴

[Unity] 전략 패턴 (Strategy Pattern)

반응형

전략 패턴이란?

이름에서 알 수 있듯이 어떠한 결정에 있어서 전략적으로(유연하고 치밀하게) 대처하는 디자인패턴이다.

상황이 바뀌어도 그에 맞게 대처가 가능해야 한다는 것이다. 그래서 이 디자인 패턴은 런타임 시점에 객체에게 다양한 동작을 할당해 준다.

 

예를 들어, 유니티로 RPG 게임을 제작할 때 플레이어가 무기를 교체하는 순간을 상상해 보자.

유저가 사용할 수 있는 무기로 Sword, Gun, Hammer, Bow가 있다고 했을 때 공격과 관련된 로직을 어떻게 작성해야 할까?

전략 패턴은 이런 문제와 관련하여 런타임 때 유연하게 동작하도록 해준다.

 

전략 패턴은 보통 상속과 다형성 등을 배우고 나면 자주 쓰이게 되는데, 난이도가 어렵지 않으면서 객체 지향 패러다임을 효과적으로 구현할 수 있기 때문이다.

 

구조는 크게 세 가지로 이루어져 있다.

콘텍스트, 전략 인터페이스, 구체 전략 클래스로 이루어져 있으며 패턴의 구조는 아래와 같다.

 

전략 패턴 구조

 

콘텍스트 : 런타임 시점에 다양한 구체 전략 클래스를 유동적으로 변경하며 사용한다.

전략 인터페이스 : 구체 전략 클래스 객체를 생성하지 위한 인터페이스이다. 콘텍스트에서는 구체 전략 클래스를 업캐스팅하여 사용한다.

구체 전략 클래스 : 전략 인터페이스를 상속받아 사용한다. 다양하고 구체적인 로직을 작성한다.


왜 사용해야 할까?

가장 큰 장점은 캡슐화라고 할 수 있을 것 같다.

다시 RPG 게임의 플레이어가 무기를 교체하는 순간을 예로 들면, Sword 클래스는 검을 휘두르는 로직만 작성하면 되고 Bow 클래스는 활을 쏘고 화살이 날아가는 로직만 작성하면 된다. 나머지 Gun 클래스와 Hammer 클래스도 마찬가지이다. (단일 책임 원칙을 고수하며 자기 자신의 클래스 내용만 쓰면 된다.)

 

장점

개방 폐쇄 원칙 : 확장에는 열려 있는 구조로 쉽게 변형 혹은 확장 가능

동적 구현 : 런타임 시점에 동작하므로 유연하게 대처

캡슐화 : 각각의 로직을 캡슐화하여 구조화

 

단점

콘텍스트와의 결합 : 콘텍스트(혹은 이를 사용하는 클라이언트)의 로직에 의존


어떻게 사용할까?

구현은 간단하다.

인터페이스와 인터페이스를 상속받은 구체 클래스, 그리고 이를 사용할 콘텍스트 클래스가 있으면 된다.

 

이번 전략 패턴의 예시는 포스팅의 처음에서 언급된 무기를 교체하는 시스템이다.

 

여기서 무기(활, 검, 총, 해머)는 전략 인터페이스를 상속받은 구체 전략 클래스에 해당한다.

무기 클래스의 부모가 될 전략 인터페이스는 IWeapon이라는 인터페이스를 사용한다.

 

public interface IWeapon
{
    KeyCode Key { get; }
    float DamageCoef { get; }
    void Use(int power);
}

 

여기서 Key는 키보드로 입력을 받았을 때 어떤 무기가 실행될지를 알려주는 값이다.

핵심 함수는 Use라고 할 수 있는데, 플레이어는 Use함수만 실행하고 나머지 구체적인 로직은 전략 클래스가 직접 각각 구현을 한다.

예를 들면 다음 Sword 클래스와 같다.

 

public class Sword :  IWeapon
{
    public float DamageCoef => 1f;
    public KeyCode Key => KeyCode.Alpha4;

    public void Use(int power)
    {
        SwiftHit();
        Debug.Log($"Damage is {power * DamageCoef}");
    }

    public void SwiftHit()
    {
        Debug.Log("Whoosh!");
    }
}

 

Sword 클래스는 IWeapon의 자식이기 때문에 Use 함수를 새로 정의한다.

이 함수에는 Sword가 사용할법한 로직을 정의한다.

위 예시에서는 Whoosh(빠르게 움직이는 칼 소리)를 출력하도록 했다.

 

다음은 해머 클래스이다.

 

public class Hammer : IWeapon
{
    public float DamageCoef => 6f;
    public KeyCode Key => KeyCode.Alpha3;

    public void Use(int power)
    {
        Swing();
        Debug.Log($"Damage is {power * DamageCoef}");
    }

    public void Swing()
    {
        Debug.Log("Thud!");
    }
}

 

이번에는 Thud(둔탁한 소리)를 출력하는 함수를 실행한다.

 

이처럼 각 전략 클래스에 해당하는 로직을 작성하기만 하면 된다.

이어서 Bow 클래스이다.

당연하겠지만 활을 쐈을 때 사용될만한 로직을 작성한다.

 

public class Bow : IWeapon
{
    public Arrow arrow = new();

    public float DamageCoef => 2f;

    public KeyCode Key => KeyCode.Alpha1;

    public void Use(int power)
    {
        arrow.SingleShot(power * DamageCoef);
    }
}

public class Arrow
{
    public int power;
    public void SingleShot(float damage)
    {
        Debug.Log($"Damage is {damage}");
    }
}

 

마지막으로는 Gun클래스이다.

 

public class Gun : IWeapon
{
    public Bullet bullet = new();
    public float DamageCoef => 3.2f;
    public KeyCode Key => KeyCode.Alpha2;

    public void Use(int power)
    {
        bullet.ShotBullet(power * DamageCoef);

    }
}

public class Bullet
{
    public void ShotBullet(float damage)
    {
        Debug.Log("Fire!");
        Debug.Log($"Damage is {damage}");
    }
}

 

이제 이 무기들을 관리할 WeaponManager클래스이다. 이 클래스가 콘텍스트(Context)에 해당한다.

 

public class WeaponManager
{
    public IWeapon[] weapons;
    public readonly KeyCode useKey = KeyCode.A;

    private IWeapon currentWeapon;

    public WeaponManager()
    {
        weapons = new IWeapon[]
        {
            new Bow(),
            new Gun(),
            new Hammer(),
            new Sword(),
        };
        currentWeapon = weapons[0];
    }

    public void UseWeapon(int playerDefaultDamage)
    {
        currentWeapon.Use(playerDefaultDamage);
    }

    public void ChangeWeapon()
    {
        foreach (var weapon in weapons)
        {
            if (Input.GetKeyDown(weapon.Key))
            {
                currentWeapon = weapon;
                Debug.Log("Weapon Changed!");
                return;
            }
        }
    }
}

 

여기서 ChangeWeapon을 하면 입력받은 키 값에 따라 무기를 교체한다. 교체된 무기는 currentWeapon에 저장된다.

그리고 UseWeapon을 하면 currentWeapon을 Use 하기만 하면 될 뿐이다.

이것이 전략 패턴을 유니티엔진에서 사용할 때의 핵심이라 할 수 있다.

 

마지막으로 아래는 콘텍스트 클래스를 사용하는 Client 클래스이다.

 

public class Player : MonoBehaviour
{
    public int defaultDamage = 10;
    public WeaponManager weaponManager;

    private void Start()
    {
        weaponManager = new WeaponManager();
    }

    void Update()
    {
        if (Input.anyKeyDown)
        {
            if (Input.GetKeyDown(weaponManager.useKey))
                weaponManager.UseWeapon(defaultDamage);
            else
                weaponManager.ChangeWeapon();
        }
    }
}

 

Player 클래스는 WeaponManager를 사용하여 키보드 입력에 따라 무기를 교체하거나 사용한다.

 

UML 다이어그램

반응형