본문 바로가기

언리얼5/언리얼5 개발

UE Fade 기능 만들기

Fade란

fade (【동사】바래지다, 점점 희미해지다, 사라지다, 바래다 ) 뜻, 용법, 그리고 예문 | Engoo Words.

게임뿐만 아니라 영화 애니메이션에서도 사용되는 시각연출을 의미한다

언리얼에서 기능을 구현하기위해 검색을 해봤지만 쉬운 기능임에도 생각보다 순수 코드로 완성된

참고자료가 없어 직접 C++ ,Blueprint로 페이드 기능을 만드는 방법에 대해 글을 써보려한다.

기능 로직 설명

주어진 입력값은 지속시간 위젯의 이미지 오퍼시티 시작값,도착값이 주어진다

함수가 실행되면 지속 시작동안 이미지의 오퍼시티를 시작값에서에서 도착값까지

도달하게 하면 페이드가 완성된다.

 

지속시간(duration)

지속시간은 페이드가 시작값에서 도착값까지 도달하는데 걸리는 시간을 말한다

예시로 시간의 기준을 초간위로 지정하여 duration =  1이 주어지면

1초동안 Image의 오퍼시티는 시작값에서 도착값까지 도달해야한다.

이러한 도달시간을 우리가 완성하기에 반드시 알아야할 공식이 있다.

 

알아야하는 기능

//우리가 원하는 페이드가 시작하고 끝나는데 걸리는 시간
float Duration;
//페이드가 진행중인 누적시간
float TimeDeltime;
 
// 함수가 끝나게되는 시간이 얼마나 남았는지 0~1로 알수가 있게된다 (함수가 실행중인 누적 시간) / (지속시간)
float CurrentOpacity = Math::Lerf(시작값,도착값,(TimeDeltime/Duration))

 

Timmer

유니티에서 연출의 대부분 닷트윈 , 코루틴 비동기 함수를 사용하여 작성된다

언리얼에서는 Timmer 기능을 사용하여 호출될 간격을 정하고 특정 함수를 연속적으로 호출이 가능하다.

 


필요한 위젯 만들기

WBP_Fade

사용자 위젯으로 Fade로 사용될 위젯 W

BP_Fade를 생성한다

 

캔버스 패널과 이미지를 가져오고

이미지의 이름을 ImgFade로 지정한다

계층구조에 ImgFade의 변수 활성화를

반드시 체크해준다

 

 

 

 

 


블루프린트

위젯 변수 , 함수 , 커스텀 이벤트 정의

 

위젯 멤버변수

  • DelTime(float) : 함수가 시작될때 호출되는 시간의 누적된 값을 저장합니다
    해당 변수는 보간처리와 함수가 끝날때 필요로합니다.
  • EndData : 페이드 목적되는 컬러의 값을 의미합니다
    언리얼의 컬러 시스템은 구조체(R,G,B,A)로 구성되어있습니다 우리는 A의 값만을 사용합니다
  • StartData : 페이드가 시작되는 컬러의 값을 의미합니다
  • TimmerHandle : 타이머의 컨트롤을 위해 반드시 필요한 변수입니다.
    해당 변수는 타이머의 종료,호출된 간격 시간등을 사용하기위해 필요로합니다
  • Duration : 함수가 시작하고 끝날때까지의 걸리는 시간입니다.
  • ImgFade : 위젯의 이미지 레퍼런스를 캐싱합니다.

위젯 함수

  • Fade : 함수가 호출될시 인자값 정의에 맞게 페이드가 진행됩니다
  • Custom Event Fading :  SetTime가 일정한 시간간격으로 호출될 함수입니다.

 

Fade 함수 파라미터

  • Duration : 위젯의 함수 Duration은 Fade 함수 파라미터로 정의됩니다.
  • EndData : 위젯의 함수 EndData은 Fade 함수 파라미터로 정의됩니다.
  • StartData : 위젯의 함수 StartData은 Fade 함수 파라미터로 정의됩니다.
    이외 함수가 초기화되는 값도 여기서 정의합니다.
  • TimeInterval : 타이머를 호출할 초단위 기준의 간격값 입니다.
    자료 사진처럼 SetTime에 연결해주시면됩니다.

페이드 함수 설명

 

페이드 함수 역할은 다음과 같습니다

  1. 인자값 초기화
  2. imgFade 투명도 초기화
  3. 타이머 핸들러 정의 
  4. 타이머 호출될 커스텀 이벤트 정의
  5. 타이머 호출 간격 정의

 

위와 같이 연결해주시면 타이머에 정의된 커스텀 이벤트를 일정 간격으로 호출됩니다.

그럼 다름으로 Fade를 이루는 가장 중요한 커스텀 이벤트를 알아보겠습니다.

 

 

Fading

 

  • 핸들러 존재 여부 확인
    커스텀 함수의 호출이 유요한 호출인지 확인을 처음 해줍니다.
    만약 거짓이라면 TimmerHandle이 존재하지않는다는 의미가 되어 별다른 처리는 필요로하지않습니다
  • DelTime만들기
    시간의 흐름에 맞는 투명도 값을 구하기위해서는 함수가 지속되고있는 시간을 알아야합니다
    DelTime을 위와같이 누적 시켜서 시간이 얼마나 지났는지 알기위해 만들어줍니다.

 

  • 보간된 투명도 값 만들기
    DelTime / Duration 값을 Lerp의 Alpha 인자로 전달한다
  • 이미지 Color Set
    Lerp의 반환된 값을 ImageFade 투명도에 정의합니다.

 

지속시간 확인
DelTime이 Duration보다 크거나 같은지 체크합니다
조건이 성립되면 투명도를 End값으로 정의합니다. 혹시나 모를 Fade 시간값의 오류가 있을수있으니 안전하게 해줍니다.
계속 사용했던 DelTime도 다시 0으로 정의하여 위젯 함수를 재활용할수있게 정의합니다.

마지막으로 TimmerHandle를 종료해줍니다.

 

결과물

 

만든기능이 정상동작하는지 확인하기위해 엑터를 하나 만들고 엑터에게서 위젯의 생성과 함수 호출을 해줍니다.

저는 테스트로 5초동안 1.0로 이미지값을 초기화하고 투명도는 0으로 만들겁니다 프레이 단위는
저는 0.01f 호출하게 만들었는데 영상 촬영에서 프레임 드랍이 발생하는것을 생각한것뿐이니 실제로는 이렇게 호출할 필요는 없습니다

 

영상

 


언리얼 C++

블루프린트로 Fade를 만들었기에 시간상 이번에는 로직의 설명은 건너띄고 코드만을 공개합니다.

 

#pragma once

#include "CoreMinimal.h"
#include "UI/UPUserWidget.h"
#include "Components/Image.h"
#include "Interface/CharacterMovementInterface.h"
#include "UPFadeUserWidget.generated.h"

DECLARE_DELEGATE(FOnFadeEndDelegate);

UENUM()
enum class EFadeType : int8
{
    FadeIn = 0,
    FadeOut = 1,
};

USTRUCT(BlueprintType)
struct FDelegateWrapper
{
    GENERATED_BODY()
    FDelegateWrapper(){}
    FDelegateWrapper(const FOnFadeEndDelegate& Delegate) : OnEndCallback(Delegate) {}

    FOnFadeEndDelegate OnEndCallback;
};




UCLASS()
class UNREALPORTFOLIO_API UUPFadeUserWidget : public UUPUserWidget
{
    GENERATED_BODY()
protected:
    virtual void NativeConstruct() override;

    
    UPROPERTY(EditAnywhere , Category = Fade)
    EFadeType Type;

    UPROPERTY(EditAnywhere , Category = Fade)
    float Duration;

    UPROPERTY()
    float DelTime;
    
    UPROPERTY(EditAnywhere, Category = Fade)
    TObjectPtr<class UImage> Img;

    TScriptInterface<ICharacterMovementInterface> MovementInterface;

public:
    UFUNCTION(BlueprintCallable)
    UImage* GetImage();

    
    FOnFadeEndDelegate* EndCallbackDelegate;

public:
    
    void StartFade(TScriptInterface<ICharacterMovementInterface> MovementCharacter);
    
private:
    UFUNCTION(BlueprintCallable)
    void StartFadeIn();
    UFUNCTION(BlueprintCallable)
    void StartFadeOut();
    UFUNCTION(BlueprintCallable)
    void FadeIn();

private:
    void FadeOut();
    FTimerHandle ActionTimer;
};

 

#include "UI/UPFadeUserWidget.h"
#include "Kismet/KismetSystemLibrary.h"
#include "Interface/CharacterMovementInterface.h"

void UUPFadeUserWidget::NativeConstruct()
{
    Super::NativeConstruct();
    Duration = 1.0f;
    Img =  Cast<UImage>(GetWidgetFromName(TEXT("ImageFade")));
    if(Img)
    {
       Img->SetColorAndOpacity(FLinearColor:: Black);
       Img->SetOpacity(0.0f);
       Type = EFadeType::FadeIn;
    }
}

UImage* UUPFadeUserWidget::GetImage()
{
    return  Img;
}



void UUPFadeUserWidget::StartFade(TScriptInterface<ICharacterMovementInterface> MovementCharacter)
{
    if(ActionTimer.IsValid())  {  return;    }
    //이동 제한
    if(MovementCharacter)
    {
       MovementInterface = MovementCharacter;
       MovementInterface->SetCharacterMovementMod(MOVE_None);
    }
    
    switch (Type)
    {
       case EFadeType::FadeIn:StartFadeIn();  break;
       case EFadeType::FadeOut:StartFadeOut();    break;
    }
}

void UUPFadeUserWidget::StartFadeIn()
{
    DelTime = 0.0f;
    GetWorld()->GetTimerManager().ClearTimer(ActionTimer);
    Img->SetColorAndOpacity(FLinearColor:: Black);
    Img->SetOpacity(0.0f);
    GetWorld()->GetTimerManager().SetTimer(ActionTimer,this,&UUPFadeUserWidget::FadeIn,0.01f,true,Duration);

}

void UUPFadeUserWidget::StartFadeOut()
{
    DelTime = 0.0f;
    GetWorld()->GetTimerManager().ClearTimer(ActionTimer);
    Img->SetColorAndOpacity(FLinearColor:: Black);
    Img->SetOpacity(1.0f);
    GetWorld()->GetTimerManager().SetTimer(ActionTimer,this,&UUPFadeUserWidget::FadeOut,0.01f,true,Duration);
}

void UUPFadeUserWidget::FadeIn()
{
    if(!ActionTimer.IsValid())
    {
       UE_LOG(LogTemp,Log,TEXT("Not Found ActionTimer"));
       return;
    }
    
    const float StartData = 0.0f;
    const float EndData = 1.0f;
    DelTime += UKismetSystemLibrary::K2_GetTimerElapsedTimeHandle(GetWorld(),ActionTimer);
    const float Alpha = DelTime / Duration;
    const float Opacity = FMath::Lerp(StartData,EndData,Alpha);
    Img->SetOpacity(Opacity);

    if(DelTime >= Duration)
    {
    
       GetWorld()->GetTimerManager().ClearTimer(ActionTimer);
       Img->SetOpacity(EndData);
       Type = EFadeType::FadeOut;
       MovementInterface->SetCharacterMovementMod(MOVE_Walking);
    }
    
}

void UUPFadeUserWidget::FadeOut()
{
    if(!ActionTimer.IsValid())
    {
       return;
    }
    const float StartData = 1.0f;
    const float EndData = 0.0f;
    
    DelTime += UKismetSystemLibrary::K2_GetTimerElapsedTimeHandle(GetWorld(),ActionTimer);
    const float Alpha = DelTime / Duration;
    const float Opacity = FMath::Lerp(StartData,EndData,Alpha);
    Img->SetOpacity(Opacity);

    if(DelTime >= Duration)
    {
       UE_LOG(LogTemp,Log,TEXT("Switch FadeIn"));
       GetWorld()->GetTimerManager().ClearTimer(ActionTimer);
       Img->SetOpacity(EndData);
       Type = EFadeType::FadeIn;
       if(MovementInterface)
       {
          MovementInterface->SetCharacterMovementMod(MOVE_Walking);  
       }
       
    }
}

 

소스 코드의 특이 사항이 있다면 블루프린트에서 사용되는 타이머와 C++에서 사용되는 타이머가

다릅니다 C++ 타이머는 호출된 시간값을 가져올수없기에 해당 블루프린트 헤더를 추가해서 작업했습니다.

UKismetSystemLibrary