본문 바로가기

Unreal

캡슐화란 대체 뭘까.. 멍청일기

최근에 멍청 일기 정리.

 

 언리얼의 UIAnimation 은 무겁고, 대응되는 에셋이 필요하니, 

위젯의 위치 등을 코드로 제어하여 애니메이션을 재생할 수 있게 해달라는 일을 받았다.

https://github.com/benui-dev/UE-BUITween/tree/master

 

GitHub - benui-dev/UE-BUITween: Unreal 4 UMG UI tweening plugin in C++

Unreal 4 UMG UI tweening plugin in C++. Contribute to benui-dev/UE-BUITween development by creating an account on GitHub.

github.com

이건 그것을 구현해둔 오픈소스 플러그인이다.

 

사용예제는 다음과 같았다.

const float TweenDuration = 0.7f;
const float StartDelay = 0.3f;
UBUITween::Create( MyWidget, TweenDuration, StartDelay )
	.FromTranslation( FVector2D( -100, 0 ) )
	.FromOpacity( 0.2f )
	.ToReset()
	.Easing( EBUIEasingType::OutCubic )
	.Begin();

 

UBUITween의 Create는 static 함수이고, AnimationInstance 를 반환한다.

그 인스턴스에 .FromTranslation (반환값은 AnimationInstance), FromOpacity() 등의 함수로 필요 값을 셋팅하여 사용하는 함수.


예제를 만들어서 갔는데(날먹하려 했던 것 부터 잘못되었음을..ㅎㅎ) 

이렇게는 쓸 수 없다고 하셨다.

 

좋지 않은 이유는 이와 같다.

1. 내부에 어떤 함수가 있는지를 전부 알아야 사용이 가능하다.

2. 어떤 함수가 필수로 호출되어야 제대로 재생이 되는지 눈에 들어오지 않는다.

(캡슐화 되어있지 않다.)

올드한 WinAPI 스타일


 

 

그래서 수정을 해보았다.

1차 수정.

USTRUCT()
struct FUITweenArg
{
	GENERATED_BODY()
   public:
    UWidget* pWidget;
    float Duration = 1.0f;
    float Delay = 0.f;
    bool IsAdditive = false;
    EBUIEasingType EasingType = EBUIasingType::Liner;
    TOptional<FVector2D> TargetTranslation;
    TOptional<FVector2D> StarttTranslation;
    TOptional<FVector2D> TargetScale;
    TOptional<FVector2D> CurrentScale;
    TOptional<float> TargetScale;
    TOptional<float> StartOpacity;
    ... etc ....   
}


UMyWidget::애니메이션을 재생시킬 함수()
{
    FUITweenArg TweenArg;
       TweenArg.pInWidget = Target;
       TweenArg.Duration = 0.5f;
       TweenArg.TargetTranslation = TargetTrans - CurrentTrans;
       TweenArg.OnCompleteDelegate = FBUITweenSignature::CreateLambda([this, PanelCapture, InRemoveIdx, InTargetIdx]( UWidget* Owner ) {
          InsertWidget(PanelCapture, InRemoveIdx, InTargetIdx);
        } );

       GET_SYSTEM(UUPUISystem)->PlayAnimation(TweenArg);
}

 

내가 짠 의도는 이랬다. (객체지향을 위배한 나쁜 생각이니 유심히 보지 마시오)

꼭 필요한 pWidget은 맨 위에.

애니메이션 재생을 하기 위해서 유저가 공통적으로 넣어줘야 하는 값인 Duration, Delay, IsAdditive, EasingType은 기본값을 넣어서 그 다음으로.

나머지 어떤 애니메이션을 재생할지 유저가 상황에 따라 커스텀하게 설정해야 하는 값들은 TOptional로. 필요하면 넣고 안필요하면 넣지말고~~~

그러면 FUITweenArg친 뒤에 .만 찍으면 볼 수 있으니까~~

 

 

결과는? 잔뜩 혼났다.

 

이게 .SetFunction()을 그냥 구조체 arg로 바꾼 것일 뿐 어떤 캡슐화가 된 거냐고.

 

 

객체지향 자체가 객체들이 독립적으로 상황에 대한 메세지만 주고 받으면 할 일은 스스로 해야지

처음부터 끝까지 외부에서 필요한걸 다 넣어주도록 오픈해두면, 이게 무슨 객체지향이냐고.

 

필요한 것은 메소드로만 "나 지금 이 기능을 쓰는 상황이야~!" 라고 알려주라고. 그리고 거기에서 꼭 필요한 값들만 파라미터러로 넘기라고.

 

 

또한 외부에서 이렇게 인자들을 직접 셋팅해줬을 때 다음과 같은 상황이 올 수도 있다.

UMyWidget::애니메이션을 재생시킬 함수()
{

	float AnimDuration = GetDurationTime();
    
    FUITweenArg TweenArg;
       TweenArg.pInWidget = Target;
       TweenArg.Duration = AnimDuration;
       TweenArg.TargetTranslation = TargetTrans - CurrentTrans;
      
       GET_SYSTEM(UUISystem)->PlayAnimation(TweenArg);
       
       //위험한 상황에 처했습니다~!
       Target = nullptr;		//외부에서 해제해버렸을 때 방어 로직이 있을까요? 
       AnimDuration = NewValue;	// 외부에서 셋팅값으로 쓰던 지역변수를 건들여버린다면?
       
}

 

이렇게 셋팅할 수 있는 가능성을 모두 열어둔다고, 파라미터를 다 열어버리는 것은 제대로 된 객체지향이 절대 아니다.

게다가 너무 많은 권한을 주어서, 코드도 위험해지기 쉽다!

 


 

2차 수정안은 이렇다.

   GET_SYSTEM(UUISystem)->ClearWidgetAnimation(Target);
   GET_SYSTEM(UUISystem)->SetWidgetAnimationOpacity(Target, 1.f, 0.f);
   GET_SYSTEM(UUISystem)->SetWidgetAnimationScale(Target, FVector2D(0.f), FVector2D(1.f));
   GET_SYSTEM(UUISystem)->SetWidgetAnimationEasingType(EBUIEasingType::Linear);
   GET_SYSTEM(UUISystem)->SetWidgetAnimationEndDelegate(
   		FBUITweenSignature::CreateLambda([this, PanelCapture, InRemoveIdx, InTargetIdx]
        ( UWidget* Owner ) {InsertWidget(PanelCapture, InRemoveIdx, InTargetIdx);

    } ));
   GET_SYSTEM(UUISystem)->PlayAnimation(Target, 0.5f, 0, false)

Opacity 를 조절하는 애니메이션 인자만을 함수로,

델리게이트 셋팅에 필요한 인자만을 함수로.... 했다는 점에서 

이 전보다 조금은 더 캡슐화되어있다.

 

 

 

하지만 이렇게 세터들과 플레이를 나눠버린다면

사용자는 func1에서 SetOpacity, fun2에서 SetScale, func3에서 PlayAnimation 하는 등 편파적으로 사용할 수 있고,

그러면 정말 디버깅하기 난해한 구조가 되어버릴것이다.

 

한 번에 셋팅할 것은 하나로 묶어줘야한다.

 

그리고 이것으로도, Animation을 재생하기 위해선

어떤 것들만 필수로 넣어줘야하는지 정말 알기 어렵다. 사용자 입장에서는 쓰기 싫은 코드다. 내부를 들여다봐야만 제대로 알 수 있으니까.

그 점에서는 파라미터들을 함수로 빼버렸을 뿐, 앞선 코드들과 차이가 없다고 볼 수 있다.

 

 

 


그럼 보기 좋은 코드는 어떤걸까...

 

USTRUCT()
struct MonsterInfo
{
	int id;
	float speed;
	float hp;
	float mp;
	float attack;
	float defend;
	FVector SpawnPoint;
	FRotator SpawnRot;
	void* SpawnFunc;
	void* DeadFunc;
};

UCLASS()
AMYMonster
{
	void Init1(MonsterInfo info);
	void Init2(int id, float speed = 0.f, float hp = 0.f, float mp = 0.f, float attack = 0.f, float defend = 0.f, FVector SpawnPoint = FVector::ZeroVector, FRotator SpawnRot = FRotator::ZeroRotator, void* SpawnFunc = nullptr, void* DeadFunc = nullptr);
    
}

Init1과 Init2 중 어떤 게 더 좋은 함수일까~

 

직접 호출해보면 알 수 있다.

 

Init1을 호출하려고 하니 넣어줘야하는 파라미터가 뜬다.

사용자 입장에서는 MonsterInfo가 뭔지 잘 모르니 일단 기본생성자로 넣어본다.

기본 생성자로는 대체 뭘 넣어줘야하는건지. 이 Info로 몬스터의 어떤 것을 셋팅해줘야하는지 알기 힘들다.

 결국 f12를 눌러 MonsterInfo 내부를 찾아갈 수밖에 없다.

 

 

내부는 또 이렇다. 뭐가 꼭 넣어줘야하는건지 알 방법이 없다.

그러면 많은 몬스터함수 사용자들이 몬스터를 이렇게 사용할 것이다..

 

몬스터 스폰~  내부에 Init이 있네. 호출!

기본 MonsterInfo()에는 뭘 넣어줘야하는거지?  그냥 호출만 일단 해주자.

 

 

이러면 만약 사용하는 함수 PlayAnimation 내부에서 몬스터 id를 체크하는 검증이 있을 경우, 

에러가 날 것이다. Monster 내부를 뜯어보지 않으면 ID가 필요한 줄 알 수 없으니, Monster를 쓰고싶은 사용자는 Monster에 대해 공부하는 시간을 가질 수 밖에 없다. "블랙박스"로 사용할 수 없다.

 

 

 

 

반면 Init2는 다르다.

너무 길어서 조금 잘랐다. 

Init2를 적는 순간, id, speed, hp 등 사용자가 셋팅하고 싶을 요소들이 주르르 파라미터로 나열된다.

이 중에 id만 기본값이 들어있지 않는 것으로 봐서는, 사용자는 몬스터의 id를 반드시! 셋팅해줘야 하나보다.  

한 눈에 알 수 있다.

 

 


Easy to Learn Hard to Master

클래스는 이렇게 짜는게 좋다고 한다.

 

다시 애니메이션으로 돌아와서... 이렇게 짜기로 했다.

 

USTRUCT()
struct FUITweenArg
{
	GENERATED_BODY()
   public:
   UITweenArg(UWidget* w, FVector2D Trnaslation, float Duration = 1.f );
   UITweenArg(UWidget* w, float Opacity, float Duration = 1.f);
   UITweenArg(UWidget* w, FVector2D Trnaslation, float Opacity, float Duration = 1.f);   
   
   void SetCompleteDelegate(FDelegate);
   
   private:
   UITweenArg();
   
   public:
    UWidget* pWidget;
    float Duration = 1.0f;
    float Delay = 0.f;
    bool IsAdditive = false;
    EBUIEasingType EasingType = EBUIasingType::Liner;
    TOptional<FVector2D> TargetTranslation;
    TOptional<FVector2D> StarttTranslation;
    TOptional<FVector2D> TargetScale;
    TOptional<FVector2D> CurrentScale;
    TOptional<float> TargetScale;
    TOptional<float> StartOpacity;
    FDelegate BeginAnimDel;
    FDelegate CompleteAnimDel;
    ... etc ....   
}


UMyWidget::애니메이션을 재생시킬 함수()
{
	  //간단 이동~	
	   FVector2D TargetPostion = GetAnimTargetPostion();
       GET_SYSTEM(UUPUISystem)->PlayAnimation(FUITweenArg(TargetWidget1,TargetPostion));
       
       // BeginDelegate 같이 특수한 셋팅들은 내부를 좀 뜯어봐서 셋팅할 수 있도록
       float TargetOpacity;
       FUITweenArg arg(TargetWidget2,TargetOpacity);
       arg.SetCompeleteDelegae = []();
       GET_SYSTEM(UUISystem)->PlayAnimation(arg);
}

이동, 투명도 조절 등 많이 쓰일 상황들은 생성자에서 바로 만들수 있게 제공한다.

자주 함께 셋팅하고 싶을 CompeleteDelegate 셋팅도 함수로 빼주었다. 쉽게 알아볼 수 있도록.

 

 

그런데 만약 좀 특수한 상황... Translation 조절하면서 Scale 조절 하면서 Opacity 바꾸고 싶은 사람은? 

그사람은 내부 조금 뜯어보면 인자들을 넣어서 조절할 수 있겠네~ 할 것이다.

 

 

근데 아까 인자에 직접 접근하는 건 사용할 때 디게 위험하다고 하지 않았나!

 

 

이때 좋은 방법이 있다.

bool UUISystem::PlayAnimation(FUITweenArg& InArg )
{
	IsValidArg(InArg);
    
    PlayAnimationImp(InArg);
}

bool IsValidArg(FUITweenArg& InArg)
{
	return InArg.pWidget != nullptr;
}

bool UUISystem::PlayAnimationImp(FUITweenArg& InArg .. etc Args )
{
	CreateAnimInstance(InArg.pWidget)->Play(InArg.Duration);
    
    ...
    
}

호출하는 함수 내부에 Imp 따로 두면 좋다.

TArray의  Insert 내부이다.

 

처음에 Check 로 Valid한 값이 들어왔는지 체크하고,

그 다음에 내부의 Impl함수를 호출해준다.

 

 

실구현은 Impl에 있는데 왜 굳이 뎁스만 높아지게 이렇게 하나??

 

1. Insert 내부 로직을 바꾸고 싶을 때, Insert 자체를 수정하면 에러가 나기 쉽다.

InsertImp를 수정하고, 필요한 파라미터만 가진 Insert는 변경하지 않음으로서 

사용자 입장에서는 내부가 바뀐지도 모르게 엔진 내부를 유지보수 할 수 있다.

 

 

2. 실제 함수 실행하기 전에  Valid 체크 하기 좋다.

 

 

 

 

 

 

 

 

 

 

 

 

으음 잘 이해했나 모르겠네

'Unreal' 카테고리의 다른 글

언리얼의 모듈이란.. Build.cs Uproject Target.cs  (0) 2023.07.10