내가 전 프로젝트에서 개발할 때 일이다.
다른 회사도 비슷하겠지만, 보통 기획서가 통과되고 회의를 거쳐 테크팀에게 날라올 일감이 점점 쌓여가면
테크팀 프로그래머들이 그 일감들을 상사에게 분배받게 된다.
일을 각각 다 분배받고 시간이 조금 지나 분배받은 일을 거의 끝냈을 시점.
기획팀 팀장이 테크팀 팀장에게 "혹시 버프시스템 언제까지 되나요?"라고 물었었다.
그런데 그 버프시스템이란게 사전에 말도 없었고 회의도 없었던거라 기획팀장이랑 테크팀장이랑 논쟁이 오고갔다.
기획팀장은 이 버프시스템이란게 없어선 안될 시스템이라는 의견이란것을 피력하고있었고
테크팀장은 왜 일정도 없이 갑자기 이렇게 말하냐고 따지고있었다.
근데 버프시스템은 게임에서 진짜 중요해서 뭔가 이 일이 나에게 올 것이라는걸 크게 짐작하고있었다.
인게임 관련 시스템은 대부분 내가 제작했으니까...
그래서 그냥 내가 빛보다 빠른속도로 제작하겠다고 했다.
만약 GAS를 썼었더라면 버프시스템이란것을 딱히 크게 제작할 필요는 없었지만,
이 프로젝트는 GAS를 안써서 기존에 있던 프로젝트의 캐릭터속성컴포넌트나, CharacterMovement들을 잘 엮어서 만들어야 했다.
처음 코드를 치기전에 구상을 먼저했다.
버프시스템은 버프마다 효과가 너무 다르기에 다른 컴포넌트와의 커플링이 될 확률이 엄청높고,
기획자의 입맛대로 확장하기 유연해야했다, 그리고 기획자가 요청하는 JSON 포맷을 맞춰 버프속성을 만들어야했기에 생각하는데 상당한 시간이 필요했다.
CDO객체
그래서 구상해낸것이 CDO객체였다.
우선 버프시스템을 만들기 위한 전략은 이러했다.
C++로 버프시스템의 작동을 구현하고 버프자체는 객체로 만들어 블루프린트기반으로 확장하고,
버프 블루프린트 내 에서 버프의 각기다른 로직을 구현하는 방식이었다.
버프클래스 및 데이터 등록은 JSON과 블루프린트로 이루어지며,
실 사용 시, 버프 Instance가 CDO에 구현된 함수와 변수를 가지고 각기 다른 동작을 수행하는 방식이다.
구상이 되니까 만드는데는 한 3~4일정도 걸렸던거같다.
UCLASS(Blueprintable,BlueprintType)
class UBuff : public UObject
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintImplementable)
void OnActivate(UBuffSystemComponent* BuffComp)
UFUNCTION(BlueprintImplementable)
void OnTick(UBuffSystemComponent* BuffComp, float TickInterval)
UFUNCTION(BlueprintImplementable)
void OnDeactivate(UBuffSystemComponent* BuffComp)
};
void UBuffSystemComponent::ActivateBuff(int32 BuffID)
{
// 버프 CDO 데이터ID에 따라서 가져오기
UBuff* buff = GetBuffCDO(BuffID);
UBuffInstance* BuffInstance = NewObject<UBuffInstance>();
BuffInstance->Init(this,buff);
BuffInstance->ActivateBuff();
}
void UBuffInstance::ActivateBuff()
{
BuffCDO->OnActivate(BuffComp);
}
그냥 이런느낌의 코드이다. (그냥 지금 기억해서 바로 손코딩한거라 컴파일오류나 크래쉬가능 ㅎㅎ)
저 UBuff를 이제 언리얼엔진에서 그냥 양산해서 그 내부에서 로직만 구현하면 된다.
BuffSystem이 있으니 타 컴포넌트도 구해서 로직을 각자 다양하게 사용하면 된다.
사실 로직 커플링을 완전히 피하진 못하는데 그냥 중요 시스템이 아닌 버프1개 커플링이 생기게 되는거라, 이게 최소화를 하는 방식이라 생각한다.
버프시스템은 꽤 크기가 큰 시스템이었는데 생각보다 QA를 돌리거나 기획자들이 테스트를 해도 버그가 생기지 않자.
이 때부터 CDO객체를 많이 쓰게 되었다.
버프말고도 이를 조건객체로도 많이썼던게 기억이난다.
조건객체가 무엇이냐면, 조건문을 객체화한거다.
TArray<UCondition*> 같은 방식으로 조건을 여러개 넣을 수 있는데, 이게 확장력이랑 유연성이 죽여준다.
나중에 기획자가 조건 바꿔달라그래도 빠르게 대처가 가능하다.
UCLASS(Blueprintable,BlueprintType)
class UCondition : public UObject
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintImplementable)
bool CanProcess();
};
UCLASS()
class AMyCharacter : public Character
{
GENERATED_BODY()
public:
UFUNCTION()
void Attack();
private:
UPROPERTY(EditAnywhere,BlueprintReadOnly,meta = (AllowPrivateAccess = "true"))
TArray<UCondition*> Conditions;
};
void AMyCharacter::Attack()
{
for(UCondition* Cond : Conditions)
{
if(Cond->CanProcess == false)
{
return;
}
}
// 공격..
}
미리 컨디션BP들을 여러개 만들어두고 각기 다른 컨디션객체를 등록해두면 된다.
만약 컨디션BP에 공용변수가 있고, 이 공용변수가 각기 달라야한다?
그럼 UConditiond을 EditInlineNew으로 만들고 TArray 메타지정자에 instanced쓰면된다.
사실 뭐 이러면 CDO의 이점을 일어버리긴하지만..
암튼 전 프로젝트에서 CDO를 굉장히 활용많이했던것같다.
'Unreal Engine' 카테고리의 다른 글
언리얼엔진은 메모리관리를 어떤방식으로 하나 (0) | 2022.11.27 |
---|---|
언리얼엔진 PBR이 뭐더냐? (1) | 2022.11.25 |
UPROPERTY는 어떻게 동작하는가? (1) | 2022.11.25 |