전략 패턴(Strategy Pattern)은 행동을 캡슐화하고, 여러 알고리즘을 상호교환할 수 있도록 만드는 디자인 패턴입니다.
런타임에 알고리즘을 선택할 수 있게 하여 유연성을 높이고 코드 재사용성을 증가시킵니다.
예시코드
#include <iostream>
#include <memory>
#include <string>
using namespace std;
class AttackStrategy
{
public:
virtual ~AttackStrategy() = default;
virtual void attack() = 0;
};
// 구체적인 공격 전략들
class MeleeAttack : public AttackStrategy
{
public:
void attack() override
{
cout << "근접 공격" << endl;
}
};
class RangedAttack : public AttackStrategy
{
public:
void attack() override
{
cout << "원거리 공격" << endl;
}
};
class MagicAttack : public AttackStrategy
{
public:
void attack() override
{
cout << "마법 공격" << endl;
}
};
// 게임 캐릭터 클래스
class Character
{
private:
string name;
unique_ptr<AttackStrategy> attackStrategy;
public:
Character(const string& name) : name(name) {}
void setAttackStrategy(unique_ptr<AttackStrategy> strategy)
{
attackStrategy = move(strategy);
}
void performAttack()
{
cout << name << "의 공격: ";
if (attackStrategy)
{
attackStrategy->attack();
}
else
{
cout << "공격 전략이 설정되지 않았습니다!" << endl;
}
}
};
// 클라이언트 코드
int main()
{
Character warrior("전사");
Character archer("궁수");
Character mage("마법사");
warrior.setAttackStrategy(make_unique<MeleeAttack>());
archer.setAttackStrategy(make_unique<RangedAttack>());
mage.setAttackStrategy(make_unique<MagicAttack>());
warrior.performAttack();
archer.performAttack();
mage.performAttack();
// 전략 변경
cout << "\n전략 변경 후:\n" << endl;
warrior.setAttackStrategy(make_unique<RangedAttack>());
warrior.performAttack();
return 0;
}
위와 같이,
공격 전략을 정의하는 인터페이스나 클래스를 만들고, 해당 정의 클래스를 상속받아서 구체적으로 행동 전략이 구현된 클래스들 작성합니다.
이렇게 작성된 행동 전략들을 설정하는 것으로 유연하게 행동 패턴을 다르게 구현하는 것이 가능합니다.
몬스터의 AI는 많은 변수를 고려해야 하며, 또한. 몬스터 객체마다 다른 행동 패턴을 가지는 경우도 있습니다.
예를 들어, 근접 공격 몬스터와 원거리 공격 몬스터는 그 공격의 방식에서 큰 차이가 있습니다.
근접 몬스터는 타깃이 되는 객체까지 가까이 이동한 후에, 공격을 시작하고.
원거리 몬스터는 타깃과 객체 사이의 거리가 사정거리 이내에 도착할 때까지 접근하여, 투사체를 발사한다든지 즉발기를 쏘는 등의 공격을 할 것입니다.
심지어, 보스 몬스터의 경우에는 여러 가지의 공격 패턴을 랜덤하게 섞어서 쓰기도 합니다.
기획팀에서 작성한 몬스터의 공격 양식을 구현하기 위해서는, 이러한 유연성과 확장성을 고려해야 했습니다.
이러한 다양한 공격 패턴을
구현하기 위한 알고리즘으로 전략 패턴을 도입했습니다.

공격 패턴을 정의하는 인터페이스입니다.
몬스터 엔티티를 제어하는 매니저 클래스에서,

YGB_IAttack 인터페이스를 구현한, 공격 전략 클래스 객체를 설정하고 유니테스크 함수인 Attack을 호출하는 것으로,
공격 패턴을 구현할 수 있습니다.

몬스터 매니저는 추상클래스로 정의되어, 기본적이고 공통적인 AI 함수를 보유한 채로, 상속받은 클래스에서 추가적으로 메서드를 재정의하여 사용할 수 있도록 합니다.
근접 타입, 원거리 타입, 보스 타입 등에 따라서 AI가 달라질 수 있기 때문입니다.

예를 들어, 근접 타입의 몬스터의 경우.
위와 같이 YGB_NearBodyAttack 클래스를 AddComponet하고, 전략 패턴의 객체로 할당하여 공격 AI를 사용합니다.

반대로, 원거리 타입의 몬스터의 경우.
YGB_RangeAttack 클래스를 AddComponent하고, 전략 패턴의 객체로 할당하여 공격 AI로 사용합니다.

보스 객체 또한 마찬가지입니다.
DragonAttack 클래스를 전략 패턴의 객체로 할당하여 공격 AI로 사용합니다.

전략 패턴으로 설정된 공격 AI는
유한 상태 기계를 도입한 프로세스에서, 현재 상태가 Attack일 경우에. 다음 함수를 호출하는 것으로 시작됩니다.
이때. 몬스터가 사망하거나 상태이상에 걸리는 등의 상황을 고려하여, 언제든지 공격 패턴이 취소될 수 있도록.
매니저 클래스에서 Attack AI의 유니테스크 토큰을 가지고 있어, 캔슬할 수 있게 하였습니다.
처음 공격 AI를 구현할 때는
이 부분을 고려하지 않고 바로 공격 AI를 적용한 탓에, 예외 상황이 계속 발생하는 것을 테스트 중에 알게 되었고.
예방을 위한 안전 코드를 추가한 것입니다.


Try-catch-finally 문을 사용하여서
중간에 캔슬 토큰을 받아서, 함수가 취소되더라도 예외 상황이 발생하는 것이 아닌, 의도된 상황으로 체크하고 넘어가도록 하였으며.
Attack AI 구현과 관련하여 수정된 변수. 그리고 콜라이더 등을 재세팅하는 코드를 Finally에 추가하는 것으로 마쳤습니다.



근접 공격 AI와는 다르게
투사체를 발사하는 형식이기 때문에, 투사체를 관리하는 배열에, 생성한 투사체 인스턴스를 추가하여 관리하는 코드가 따로 존재합니다.
투사체는 몬스터의 죽음. 즉, Disable과 동시에 모두 파괴됩니다.
현재 프로젝트에서는 총 네 마리의 보스의 구현이 완성된 상태며.
직접 2D 리깅하여 애니메이션을 만들었으며,
공격 패턴을 열거형으로 분류하고.


열거형의 상태에 따라서, 애니메이션을 동작 시키고 해당 애니메이션의 프레임 단계 별로 필요한 콜라이더를 활성/비활성화 시키거나 몬스터를 이동시키거나, 파티클을 생성하고, 사운드를 생성하는 등의 방식으로 패턴을 구현하였습니다.



위 사진은 코드의 일부입니다.
스핑크스 보스의 레이저 패턴을 구현한 코드입니다.