UI 애니메이션 및 3D위젯 UI(체력 바) 구현하기
4-2강에서는 게임 흐름에 맞춰 대략적인 UI를 만들어 봤다면
이제 캐릭터의 체력이 3D 위젯으로 바 형태로 표시되게 하고
게임 오버 이후 나타나는 UI의 애니메이션이랑 총점 등이 표시되게 구현을 하는 것이 목표다.
UI 애니메이션
UI 애니메이션은 Main UI에서 작동된다.
Main UI 만드는 동안 건드린 것들
블로그를 작성하는 시점에서는 강의에서 들었던 내용에서 수정을 많이했기 때문에 최종 결과물이 이렇게 다르게 나온다.
먼저 처음에 게임오버 메뉴를 따로 안만들고 메인 메뉴에서만 모든 작업을 처리하기로 했다.
그래서 메인 메뉴에서는 저기 게임오버 텍스트와 총점 텍스트가 안보여야 하니..
여기서, GameOver 텍스트를 Hidden으로 안 보이게 설정했다.
UI 애니메이션 추가
강의에서는 GameOver 텍스트가 게임 종료시에 깜빡깜빡 거리는 효과를 만들기로 했다.
이렇게 애니메이션을 만들고 이름은 F2키를 눌러 정할 수 있다.
이렇게 Anim_TimeOver 애니메이션을 클릭 후 화면의 UI요소를 클릭후 추가를 누르면 해당 창이 뜨는데 저기서 원하는 버튼을 추가하면 애니메이션이 실행될 때
그것과 연결된 버튼들의 애니메이션이 동작하게 된다.
그렇게 버튼을 만들었다면 이제 그 버튼에서 실행될 애니메이션을 직접 구현해줘야 한다.
애니메이션에 어떤 속성을 조정해 어떤 애니메이션을 정할지 여기서 정할 수 있다. 애니메이션과 연결한 텍스트 블록 옆에 +버튼을 눌러 가져올 수 있다. 게임 오버 텍스트에 깜빡깜빡 효과를 주기 위해 투명도와 관련된 렌더 오파시티(render opacity)를 눌러줬다.
오른쪽에 보이는 2개의 빨간 도형과 이어지는 선은 마우스로 움직일 수 있는데 저걸 특정 시간대로 움직이고 나서 왼쪽에서 가져온 값을 조정하면 된다.
예를 들어, 0초에 (투명도 1), 1초에 (투명도 0), 2초에 (투명도 1)이라고 지정해 놓으면
그 사이의 투명도는 시간이 지날때마다 0~1초에서는 투명도가 1->0으로, 1~2초에서는 투명도가 0->1로
서서히 자동으로 변한다.
나는 총 애니메이션 시간을 4초로 정했고 1초마다 투명도가 0과 1이 변경되게 하여
깜빡깜빡 거리는 효과를 구현했다!
저 파란색으로 선택된 자석 모양은 '스냅'이라는 건데 저걸 선택 해제를 해주고 나서
빨간 도형과 이어지는 선을 움직이면 좀 더 정밀한 조정이 가능했다.
예를 들어, 저 옵션을 켰을때는 시간대가 1.05초에 맞춰졌다면 끄고나면 시간대가 1.00초에 정확히 맞출 수 있게 된다.
만든 애니메이션과 블루프린트 노드 연결
디자이너 탭에서 텍스트 블록에 변수 여부를 체크해주면 그래프 탭에서 해당 텍스트 블록을 변수로 활용가능하다. 즉, 블루프린트 노드로 쓸 수 있게 된 것이다.
이렇게 최종본을 만들었다. 만약 게임오버가 된다면 메인 화면에 안보이던 게임오버 텍스트와 총 점수를 보이게 해주는 것이다. 그리고 Play Animation이라는 언리얼엔진에 내장된 함수로 ..
애니메이션에서 변수를 갖고와 연결시켜주면 디자인 탭에서 열심히 작성한 애니메이션이 실제 게임에서도 나타난다.
C++ 코드와 블루프린트를 연결해주기 위한 작업..
if (MainMenuWidgetClass)
{
MainMenuWidgetInstance = CreateWidget<UUserWidget>(this, MainMenuWidgetClass);
if (MainMenuWidgetInstance)
{
MainMenuWidgetInstance->AddToViewport();
bShowMouseCursor = true;
SetInputMode(FInputModeUIOnly());
}
if (UTextBlock* ButtonText = Cast<UTextBlock>(MainMenuWidgetInstance->GetWidgetFromName(TEXT("StartButtonText"))))
{
ButtonText->SetText(FText::FromString(TEXT("Start")));
}
if (bIsRestart)
{
UFunction* PlayAnimFunc = MainMenuWidgetInstance->FindFunction(FName("PlayGameOverAnim"));
if (PlayAnimFunc)
{
MainMenuWidgetInstance->ProcessEvent(PlayAnimFunc, nullptr);
}
if (UTextBlock* TotalScoreText = Cast<UTextBlock>(MainMenuWidgetInstance->GetWidgetFromName("TotalScoreText")))
{
if (USpartaGameInstance* SpartaGameInstance = Cast<USpartaGameInstance>(UGameplayStatics::GetGameInstance(this)))
{
TotalScoreText->SetText(FText::FromString(
FString::Printf(TEXT("Total Score: %d"), SpartaGameInstance->TotalScore)
));
}
}
}
}
1.
if (UTextBlock* ButtonText = Cast<UTextBlock>(MainMenuWidgetInstance->GetWidgetFromName(TEXT("StartButtonText"))))
{
ButtonText->SetText(FText::FromString(TEXT("Start")));
}
이 강의를 듣던 당시에는 메인 메뉴화면에 Start 버튼만 있었기에 그것을 확실히 설정해주는 작업이 필요했다. 왜냐하면 게임 오버가 되면 그 버튼의 텍스트를 그대로 'ReStart'로 바꿀 계획이었기 때문이다.
GetWidgetFromName << 블루프린트 위젯에서 디테일 창 바로 아래 이름을 정할 수 있었는데 그 이름으로 UI요소를 판별해 가져오는 것이다.
2.
if (bIsRestart)
{
UFunction* PlayAnimFunc = MainMenuWidgetInstance->FindFunction(FName("PlayGameOverAnim"));
if (PlayAnimFunc)
{
MainMenuWidgetInstance->ProcessEvent(PlayAnimFunc, nullptr);
}
if (UTextBlock* TotalScoreText = Cast<UTextBlock>(MainMenuWidgetInstance->GetWidgetFromName("TotalScoreText")))
{
if (USpartaGameInstance* SpartaGameInstance = Cast<USpartaGameInstance>(UGameplayStatics::GetGameInstance(this)))
{
TotalScoreText->SetText(FText::FromString(
FString::Printf(TEXT("Total Score: %d"), SpartaGameInstance->TotalScore)
));
}
}
}
이것도 마찬가지로 Start->ReStart로 텍스트를 변경할 계획이었기 때문에 bIsRestart라는 플래그 변수를 써서 게임오버했을 경우만 저 값이 true로 바뀌어 메인 메뉴에서 ReStart버튼을 표시하도록 했다. 그와 별개로 설명할 부분이 있다.
UFunction* PlayAnimFunc = MainMenuWidgetInstance->FindFunction(FName("PlayGameOverAnim"));
if (PlayAnimFunc)
{
MainMenuWidgetInstance->ProcessEvent(PlayAnimFunc, nullptr);
}
바로 여기서 UFunction*타입으로 UUserWidget*타입인 MainMenuWidgetInstance에서 FindFunction()을 이용해서 FName타입으로 위젯 블루프린트에서 애니메이션 함수로 만들어 놓았던 것을 끌어올 수 있는 것이다!
ProcessEvent(PlayAnimFunc, nullptr);를 이용해서 메인메뉴 블루프린트에서 만든 함수를 호출(실행)했다. 1번째인자는 UFunction*타입인 그 함수의 객체,
2번째인자는 그 함수가 매개변수가 있으면 그 값을 넣고 없다면 nullptr을 넣는다.
원래는 Start와 ReStart 텍스트 넣는 부분까지 정리하려 그랬지만 시간이 없어 생략했다.
이제 3D 위젯 UI로 체력바를 어떻게 만들지 생각해보자.
3D 위젯 UI 구현하기
내가 만든 캐릭터 클래스의 헤더파일에 멤버 변수로 추가한 것
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI")
UWidgetComponent* OverheadWidget;
UWidgetComponent란 UI를 3D월드에 배치할 수 있게 도와주는 도구다.
2D UI를 3D공간의 특정 객체 위에다 붙일 수 있게하는 컴포넌트다.
그래서 체력바는 캐릭터에 붙여야되니까 캐릭터 클래스에다가 만들었다.
위젯 컴포넌트에는 2가지 모드가 있다.
스크린 모드
플레이어의 카메라 시점과 상관없이 화면에 고정된 것처럼 보인다. 그래서 스크린으로 설정하는 게 맞다.
월드 모드
캐릭터의 움직임에 따라 체력바의 글씨도 같이 돌아가게 될 것이다. 그러면 옆을 보고 있을 때 글씨가 안보이게 된다.
OverheadWidget = CreateDefaultSubobject<UWidgetComponent>(TEXT("OverheadWidget"));
OverheadWidget->SetupAttachment(GetMesh());
OverheadWidget->SetWidgetSpace(EWidgetSpace::Screen);
그래서 코드로는 캐릭터 클래스의 소스 파일에 들어가
생성자에 이 부분을 추가했다. GetMesh()는 코드에서는 스태틱 메시 컴포넌트를 만드는 부분이 없지만 블루프린트에서 자동으로 루트 컴포넌트와 스태틱 메시가 추가되기 때문에
그 부분을 코드에서 갖고오기 위해 GetMesh()를 쓴 것이다.
SetWidgetSpace(EWidgetSpace::Screen); << 그리고 이런식으로 스크린 모드로 설정이 가능하다.
체력바 위젯 블루프린트 만들기
그냥 텍스트 블록 하나 가져다 놓고 위치를 가운데 위에다 놓으면 끝이다.
이제, 체력 위젯 블루프린트랑 코드에서 만든 OverheadWidget을 연결하고
체력 위젯 블루프린트의 텍스트 블록에 실시간으로 변하는 체력의 정보까지 연결하면 끝이다.
체력바 업데이트 코드
void ASpartaCharacter::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("%.0f / %.0f"), CurrentHealth, MaxHealth)));
}
}
1.
OverheadWidget->GetUserWidgetObject();로 블루프린트로 만든 유저 위젯의 객체를 가져올수 있었다.
그리고 그 객체의 특정 UI요소를 가져오기 위해 OverheadWidgetInstance->GetWidgetFromName()를 썼다.
HPText->SetText(FText::FromString(FString::Printf(TEXT("%.0f / %.0f"), CurrentHealth, MaxHealth)));
마지막으로, 위 코드를 통해 실시간으로 변하는 체력 정보를 업데이트할 수 있도록 했다.
만든 체력바 위젯 블루프린트 캐릭터 클래스에 적용하기
이 작업을 해줘야 정상적으로 캐릭터 클래스에 있는..
UUserWidget* OverheadWidgetInstance = OverheadWidget->GetUserWidgetObject();
이 코드에서 실제 위젯의 객체인 위젯 블루프린트를 가져오면서 코드가 동작하게 될 것이다.
캐릭터 블루프린트에서 체력바 위치 조정하기
일단 캐릭터 위에 체력바가 안보이니 보이도록 World로 바꿔준다.
월드로 바꾸면 이렇게 체력바가 눈에 보이는데 캐릭터랑 많이 띄워줘야 하니 그렇게 위치를 조정 후 다시 Screen으로 바꿔준 뒤 설정을 마친다. 왜 다시 Screen으로 바꾸나면 그래야 캐릭터 시점이 회전할 때 체력도 회전하지 않기 때문이다.
그때 체력바마저 회전해버린다면 캐릭터의 시야나 얼굴을 덮거나 가릴 수 있다.
SetPause()란?
void ASpartaGameState::OnGameOver()
{
if (APlayerController* PlayerController = GetWorld()->GetFirstPlayerController())
{
if (ASpartaPlayerController* SpartaPlayerController = Cast<ASpartaPlayerController>(PlayerController))
{
SpartaPlayerController->SetPause(true);
SpartaPlayerController->ShowGameOverMenu();
}
}
}
게임오버 함수에서 SetPause()라는 함수를 썼는데 이는 컨트롤러의 내장된 함수이며,
게임이 멈추는 기능이다. 괄호 안의 인자로 true가 들어가면 멈추고 false면 다시 재개된다.
말 그대로 게임 내의 모든 액터(캐릭터, 오브젝트, AI 등)의 동작을 중지시킨다.
물리 엔진도 동작하지 않는다.
주의할 점은 Tick()도 실행이 멈추게 되는데 그래서 그런지 SetPause(true)일 때
Delay나 Timer 등이 실행되지 않았다.
파티클과 사운드로 게임 효과 연출하기
'Unreal 게임 개발 온라인 학습' 카테고리의 다른 글
[2025-02-10 / Day 21] 게임 UI 설계와 시각 및 사운드 효과 적용하기 (1/2) (0) | 2025.04.20 |
---|---|
[2025-02-07 / Day 20] 아이템 스폰 & 캐릭터 체력, 점수 관리 시스템 & 게임 루프 설계 (0) | 2025.04.20 |
[2025-02-06 / Day 19] 아이템 클래스 설계 & 충돌 이벤트로 획득 아이템 구현 (0) | 2025.04.20 |
[2025-02-04 / Day 18] 캐릭터 컨트롤 및 기본 애니메이션 구현하기(2/2) (0) | 2025.04.20 |
[2025-02-03 / Day 17] 캐릭터 컨트롤 및 애니메이션 구현하기(1/2) (1) | 2025.04.20 |