일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
- LittleNightMare
- unity
- unreal ai lag
- 리디렉터 크래쉬
- Ai
- UE
- Unreal Engine 4
- unreal engine
- deltaTime
- ai 뚝뚝 끊김
- ai jitter
- ue4 Crash
- staticmesh mobility
- UE5
- Unreal Engine Error
- 리디렉션 크래쉬
- ai 주춤거림
- Random Map Generator
- redirector crash
- UnrealEngine
- 언리얼
- UE4
- splinemeshcomponent scale
- redirection crash
- 랜덤 맵 생성
- Unreal Engine 5
- unreal engine redirection crash
- register component
- 13iew
- ue4 error
- Today
- Total
Class GameDev* SheepAdult
커맨드 패턴(Command Pattern)을 통한 스킬 시스템 구현 본문
언리얼 엔진을 통한 멀티 플레이 RPG 게임을 만들던 도중 스킬 시스템이 생각보다 복잡했다. 스킬 각각 다른 이벤트를 통해 발동되는 건 물론 애니메이션 중간에 스킬이 생성될 수도 있고 키를 누른 직후 즉각 생성될 수도 있다. 차징, 홀딩, 캐스팅 등 타입을 가지고 있을 수 있으며 타겟팅을 통해 발동될 수도 있고 타겟팅 없이 주변에 대미지를 줄 수도 있었다. 이렇게 복잡한 시스템을 어떻게 구현할까 고민하다가 커맨드 패턴을 사용하기로 했다.
Why 커맨드 패턴
프로그래머로서 리그 오브 레전드(이하 롤)의 사일러스라는 캐릭터가 가장 눈길을 끈다. 왜냐하면 상대방의 스킬을 가져와 그대로 사용할 수 있기 때문이다. 이 말은 스킬은 각각 캐릭터와 독립된 객체로 존재해야하며 자신의 스킬 시스템에 set 하기만 하면 스킬을 사용할 수 있다는 말이다. 이번 프로젝트를 진행하면서 사일러스 같은 챔피언을 만들 수 있도록 스킬 시스템을 구현하는 것이 내 목표였다. 이를 위해서는 스킬을 캡슐화하여 그대로 받아올 수 있는 방법이 필요했다. 내가 누르는 키에 바인딩된 함수에 간단히 연결만 해주면 되기 때문이다.
어떻게 구현
사실 처음에 머릿속에 떠오르는 대로 스킬 시스템을 구현했다. 근데 그게 마침 커맨드 패턴이었다... 스킬을 생성할 객체가 있으면 좋겠다고 생각해서 스킬 생성 객체를 하나 만들고 해당 객체에서 UseSkill, SpawnSkill 등의 함수를 가상함수로 구현하여 유연하게 호출할 수 있도록 했다.
처음에는 Interface가 아닌 abstract 클래스를 부모로 둬서 구현했다. 추후에 이식성 등을 고려하여 Skill과 관련된 함수들만 Interface로 뺐다.
여기서 홀딩, 차징, 캐스팅 스킬은 배제하고 스킬 시스템 자체만 보자. 캐릭터의 스킬 컴포넌트에서 스킬을 관리하며 스킬 컴포넌트는 스킬에 대한 객체를 스킬 수만큼 배열 형태로 가지고 있다. 여기서 스킬에 대한 객체 배열을 키 입력에 맞게 함수를 호출하면 된다.
스킬 커맨드
커맨드 패턴은 여러 스킬을 하나의 형태로 추상화하여 관리하는 것이 목표기 때문에 하나의 추상화를 무엇으로 시킬 것인지 정해야한다. 일반적으로 인터페이스 혹은 추상 클래스로 설정할 수 있으나 이식성 혹은 추후 변경을 최소화하기 위해 인터페이스를 사용했다. 아래는 Command 인 Skill Command이다.
class P1_API ISkillCommand
{
GENERATED_BODY()
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
virtual void UseSkill() = 0;
...
};
함수는 어떤 것을 구현하느냐에 따라 추가될 수 있지만 대략적인 내용을 작성하는 것이니 스킬 사용 함수만 적었다. 만약 스킬에 대응하는 키를 입력하면 캐릭터의 Skill Component에서 UseSkill함수를 호출한다.
// 게임 로드 시 스킬을 세팅해주는 함수
void USkillComponentBase::SetSkills()
{
AP1Creature* P1Creature = Cast<AP1Creature>(GetOwner());
if (P1Creature)
{
if (P1Creature->GetClassType() == FName("Warrior"))
{
AWarriorQSkillCommand* QSkill = NewObject<AWarriorQSkillCommand>();
TScriptInterface<ISkillCommand> QInterfaceCommand;
QInterfaceCommand.SetObject(QSkill);
QInterfaceCommand.SetInterface(Cast<ISkillCommand>(QSkill));
SkillCommands.Add(QInterfaceCommand);
}
...
}
...
}
void USkillComponentBase::UseSkill(int32 SkillIndex)
{
// 스킬 인덱스를 넘어가면 return
if (Skills.Num() <= SkillIndex || SkillCommands.Num() <= SkillIndex) return;
// 스킬 사용. 사일러스와 같이 스킬을 뺏으려면 아래의 배열에서
// 캡슐화된 스킬 객체를 가져와 사용하면 된다.
SkillCommands[SkillIndex]->UseSkill();
}
해당 인터페이스를 상속하는 Concrete Command는 아래와 같다. 최종적인 Concrete Command는 아니고 이를 또 상속받는 스킬들을 생성할 것이다.
class P1_API ASkillCommandBase : public AActor, public ISkillCommand
{
GENERATED_BODY()
...
public:
UFUNCTION(BlueprintCallable)
virtual void UseSkill() override;
...
};
아래는 Warrior의 스킬 예시이다. 이름은 QWER 로 지었다.
UCLASS()
class P1_API AWarriorQSkillCommand : public ASkillCommandBase
{
GENERATED_BODY()
public:
AWarriorQSkillCommand();
public:
virtual void UseSkill() override;
...
};
Q를 눌렀을 때 어떤 동작을 할지 위의 함수에서 작성해주면 된다. 아래는 예시다.
void AWarriorQSkillInstance::UseSkill()
{
Super::UseSkill();
// 움직임 동기화 패킷
{
Protocol::C_MOVE Pkt;
// C_Move 패킷 세팅,,
SEND_PACKET(Pkt);
}
// 애니메이션 몽타주 패킷
{
Protocol::C_MONTAGE Pkt;
// C_Montage 패킷 세팅,,
SEND_PACKET(Pkt);
}
...
}
결론
직접 구현해보니 UseSkill만으로는 어림도 없고 스킬 사용 전, 중, 후 작업은 물론 애니메이션이 끝났을 경우, 캐스팅, 홀딩, 차징 스킬 등 스킬에 타입이 있을 경우 등등 고려해야 할 사항이 많아 아마 인터페이스의 함수가 늘어날 것이다. 그리고 스킬 커맨드 객체를 생성하는 것도 디자인 패턴을 활용하면 좋을 것 같고 캐스팅, 홀딩, 차징 스킬이라던지 타겟팅 시스템도 전략 패턴이나 데코레이터 패턴 등 적절한 것을 찾아 섞어서 구현하면 재밌을 것 같다.
'C++' 카테고리의 다른 글
Multi Thread를 활용한 간단한 1:N 채팅 서버 (0) | 2023.12.30 |
---|---|
[C++] 복사 생성자 (0) | 2022.09.03 |