본문 바로가기

언리얼 + cpp

언리얼C++ 10일차(UI 애니메이션, Widget Component)

UI 애니메이션

UMG에는 애니메이션 기능이 탑재되어 있다.

이 기능을 사용하면 버튼이 클릭될 때 색이 바뀌거나 텍스트가 화면 위로 등장했다가 사라지는 등 다양한 연출을 구현할 수 있다.

UMG 에디터 내에서 Window-Animations을 클릭하면 패널이 아래에 뜬다.

새 애니메이션을 생성하거나 이미 만든 애니메이션을 선택해서 타임라인을 확인할 수 있다.

Keyframe을 이용하여 특정 시간대에 UI속성(크기, 위치, 투명도, 색상 등)을 어떻게 바꿀지 기록할 수 있다.

 

MainMenu UI에서 게임이 끝날 때 Game Over! 텍스트 위젯과 총 점수를 나타내는 Total Score 텍스트 위젯을 추가하고 화면 가운데에 오도록 배치한다. 색을 정하고 텍스트를 적어준다.

GameOver와 TotalScore 텍스트는 게임 시작시에 보이지않고 게임오버가 됐을 때만 보이도록 설정할 것이기 때문에 Visibility를 Hidden으로 설정한다.

 

이제 GameOver 텍스트를 깜빡이도록 애니메이션 효과를 구현한다.

애니메이션 패널에서 새 애니메이션을 생성한다.

새 애니메이션 생성

생성된 애니메이션을 클릭하면 오른쪽의 Add 버튼이 활성화가 된다.

애니메이션 효과를 적용할 위젯을 클릭하고 Add를 누르면 해당 위젯에 대한 애니메이션 트랙을 생성할 수 있다.

생성된 트랙의 오른쪽에 희미하게 + 버튼이 있는데 누르면 위젯의 다양한 속성 트랙들이 나온다.

깜빡이는 효과를 주려면 투명도를 조절해야 하기때문에 Render Opacity 트랙을 추가한다.

추가된 Render Opacity 트랙에 Keyframe을 이용하여 투명도 값을 시간에 따라 변화 시킬 수 있다.

시간 바를 옮기면서 Render Opacity의 값을 수정하면 자동으로 트랙에 Key가 등록된다.

나는 0초에 값을 0을 넣고 1초에 1을 넣고 0.5초 간격으로 0.5씩 줄었다 늘었다를 반복했다.

아래에 재생 버튼을 누르면 애니메이션을 테스트할 수 있다.

위 애니메이션을 C++에서 함수로 호출할 수 있도록 이벤트 그래프에서 함수를 생성한다.

함수가 호출될 때 Hidden이였던 GameOver 텍스트와 TotalScore 텍스트의 Visibility를 Visible로 변경하고 애니메이션을 재생하는 노드를 사용하여 함수를 구현한다. 이 함수를 C++에서 게임 오버일 때 호출하면 된다.

 

이제 C++로 가서 ShowMainMenu 함수에서 true일때 이 함수가 호출되도록 구현한다.

// JinPlayerController.cpp
// ShowMainMenu 함수 맨 아래에
		if (bIsRestart)
		{
			UFunction* PlayAnimFunc = MainMenuWidgetInstance->FindFunction(FName("PlayGameOverAnim"));
			if (PlayAnimFunc)
			{
				MainMenuWidgetInstance->ProcessEvent(PlayAnimFunc, nullptr);
			}

			if (UTextBlock* TotalScoreText = Cast<UTextBlock>(MainMenuWidgetInstance->GetWidgetFromName(TEXT("TotalScoreText"))))
			{
				if (UJinGameInstance* JinGameInstance = Cast<UJinGameInstance>(UGameplayStatics::GetGameInstance(this)))
				{
					TotalScoreText->SetText(FText::FromString(FString::Printf(TEXT("Total Score: %d"), JinGameInstance->TotalScore)));
				}
			}
		}

MainMenuWidgetInstance에서 PlayGameOverAnim 이름을 가진 함수를 찾아서 PlayAnimFunc에 넣는다.

ProcessEvent를 호출해서 PlayAnimFunc을 실행하고 PlayAnimFunc는 인자가 없는 함수기 때문에 nullptr을 입력한다.

Widget Component

위젯 컴포넌트를 이용하여 캐릭터의 머리 위에 체력을 표시할 수 있다.

  • UMG (언리얼 모션 그래픽스)로 만든 위젯 (텍스트, 이미지, 버튼 등)을 3D 월드에 붙일 수 있게 해주는 컴포넌트입니다.
    • 예: “NPC 머리 위 체력바”, “아이템 위에 ‘F 키를 누르세요’ 텍스트” 등이 가능해집니다.
  • 언리얼 엔진에서 WidgetComponent를 사용하면, 2D로만 보이던 UI를 공간 내 특정 위치에 붙여 놓고, 카메라 각도에 따라 회전하거나 크기가 달라지는 모습을 만들 수 있습니다.
  • WidgetComponent는 Actor에 부착(Attach)할 수 있는 컴포넌트이며, 특정 UUserWidget(UMG Blueprint 클래스)을 3D 상에 표시하게 해 줍니다.
    • 보통은 SetWidgetSpace(EWidgetSpace::World)로 설정하여, 월드 공간에 UI가 존재하게 만듭니다.
    • 초기 상태에서 SetVisibility(false)로 해 두고, 플레이어가 가까이 왔을 때 SetVisibility(true)로 변경하여 표시할 수 있습니다.

체력 표시용 위젯블루프린트를 만들고 텍스트블럭을 추가하고 체력을 입력한다.

10/100은 예시 텍스

이제 다시 C++로 가서 캐릭터에 위젯 컴포넌트와 체력이 갱신되는 함수를 추가한다.

// JinCharacter.h
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI")
	UWidgetComponent* OverheadWidget;
    
	void UpdateOverheadHP(); // 머리 위에 체력을 업데이트 할 함수
// JinCharacter.cpp
	// 생성자
	OverheadWidget = CreateDefaultSubobject<UWidgetComponent>(TEXT("OverheadWidget"));
	OverheadWidget->SetupAttachment(GetMesh());
	OverheadWidget->SetWidgetSpace(EWidgetSpace::Screen);
    
void AJinCharacter::BeginPlay()
{
	Super::BeginPlay();

	UpdateOverheadHP(); // 시작할 때 갱신

}

void AJinCharacter::AddHealth(float Amount)
{
	Health = FMath::Clamp(Health + Amount, 0.0f, MaxHealth);
	UpdateOverheadHP(); // 회복될 때 갱신
}

float AJinCharacter::TakeDamage(
	float DamageAmount,
	FDamageEvent const& DamageEvent,
	AController* EventInstigator,
	AActor* DamageCauser)
{
	float ActualDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);

	Health = FMath::Clamp(Health - DamageAmount, 0.0f, MaxHealth);
	UpdateOverheadHP(); // 데미지를 입을 때 갱신

	if (Health <= 0.0f)
	{
		OnDeath();
	}

	return ActualDamage;
}

void AJinCharacter::UpdateOverheadHP()
{
	if (!OverheadWidget) return;

	UUserWidget* OverheadWidgetInstance = OverheadWidget->GetUserWidgetObject();
	if (!OverheadWidgetInstance) return;

	if (UTextBlock* HPText = Cast<UTextBlock>(OverheadWidgetInstance->GetWidgetFromName(TEXT("OverHeadHP"))))
	{
		HPText->SetText(FText::FromString(FString::Printf(TEXT("%.f / %.f"), Health, MaxHealth)));
	}
}

체력 갱신이 필요할 때(시작할 때, 회복할 때, 체력 깎일 때)마다 UpdateOverheadHP 함수를 호출하여 갱신한다.

 

에디터의 BP_JinCharacter로 가서 위젯 컴포넌트에 만들어둔 WBP_HP를 넣고 위치를 조절한다.

WidgetComponent의 트랜스폼을 조절해서 위치, 회전, 크기를 조절할 수 있다.

또는 Space를 World로 변경하면 위젯이 뷰포트에서 보인다. 위치를 조절 후 다시 Screen으로 변경하면 된다.

 

현재 캐릭터가 체력이 모두 달아서 게임오버가 될때는 로그로만 출력이 되는데 이제 메인메뉴를 호출하는 로직을 구현한다.

다시 C++의 JinCharacter.cpp로 가서 OnDeath 함수에서 구현한다.

void AJinCharacter::OnDeath()
{
	AJinGameState* JinGameState = GetWorld() ? GetWorld()->GetGameState<AJinGameState>() : nullptr;
	if (JinGameState)
	{
		JinGameState->OnGameOver();
	}
}

체력이 0이 되어 게임오버가 될 때 MainMenu UI는 뜨지만 지정해놓은 시간30초가 지나면 다음 레벨로 넘어가버리는 문제가 있다.

GameState에는 SetPause 함수를 사용하여 게임을 정지시키는 함수가 있는데 이를 사용해서 UI를 불러오는 함수 마지막에 SetPause(true)를 추가하여 게임을 정지시키고 게임을 시작할 때 정지를 푼다.

// JinPlayerController.cpp
    // ShowMainMenu 함수 마지막줄
    SetPause(true); // 추가

    
    // StartGame 함수 마지막줄
    SetPause(false); // 추가