언리얼 엔진 5/개발 일지

[UE5] 블루아카이브 TPS게임 개발일지 (61) - 대사 시스템 (Typewriter 효과)

ciel45 2024. 2. 21. 21:52

칸나와 적의 대사를 자막처럼 넣어, 게임이 덜 심심하도록 해볼 것이다.

 

제일 먼저, ConversationManager 클래스를 만들었다. GameManager와 같이 SubSystem을 상속받은 클래스이며, 대사를 칠 캐릭터들은 ConversationManager의 SetConversation만을 호출하면 되도록 할 것이다.

 

ConversationManager.h:

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "ConversationManager.generated.h"

class UConversation;

/**
 * 
 */
UCLASS()
class KANNATPS_API UConversationManager : public UGameInstanceSubsystem
{
	GENERATED_BODY()
public:
	// 인자로 대사의 인물, 대사의 내용을 받는다.
	UFUNCTION(BlueprintCallable)
	void SetConversation(const FString& Speaker, const FString& Content);

	void SetConversationWidget(UConversation* Widget);

private:
	UPROPERTY()
	UConversation* ConversationWidget;
};

ConversationManager.cpp:

// Fill out your copyright notice in the Description page of Project Settings.


#include "Managers/ConversationManager.h"
#include "HUD/Conversation.h"


void UConversationManager::SetConversation(const FString& Speaker, const FString& Content)
{
	ConversationWidget->SetConversation(Speaker, Content);
}

void UConversationManager::SetConversationWidget(UConversation* Widget)
{
	ConversationWidget = Widget;
}

UConversation은 실제로 대사의 출력을 담당할 UserWidget을 상속받은 클래스이다. 자세한 내용은 후술할 것이다.

 

보시다시피, UConversationManager는 단순히 출력할 대사의 인물과 내용을 UConversation에 전달하기만 한다.

 

 

 

 

또한 단순히 문장이 한번에 뜨지 않고, 원작처럼 글자가 한 글자씩 나타나도록 할 것이다. (typewriter 효과)

 

 

 

먼저 대사를 담당하는 위젯 블루프린트 WBP_Conversation을 만들었다.

 

 

SpeakerText는 인물명으로, 대사가 트리거될 시 바로 완성된다. 우측 정렬되어있다.

ContentText는 대사 내용으로, 대사가 트리거될 시 한 글자씩 출력된다. 좌측 정렬되어있다.

 

완료된 대사의 페이드아웃 애니메이션도 만들었다. DamageIndicator의 애니메이션과 100% 똑같다.

 

 

우측 상단을 보면 부모 클래스가 Conversation.h로 되어있는데, 상술했던 그 클래스이다. 대사가 한 글자씩 출력되는 기능과 끝난 대사가 페이드아웃되는 기능은 저 C++ 클래스에서 구현할 것이다.

 

Conversation 클래스에 대한 설명은 코드의 주석으로 대체한다.

 

Conversation.h:

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Conversation.generated.h"

class UTextBlock;

/**
 * 
 */
UCLASS()
class KANNATPS_API UConversation : public UUserWidget
{
	GENERATED_BODY()

public:
	virtual void NativeConstruct() override; //BeginPlay와 같음.

	void SetConversation(const FString& Speaker, const FString& Content);

protected:
	UPROPERTY(BlueprintReadOnly, Transient, meta = (BindWidgetAnim)) // Transient 붙여야함.
	class UWidgetAnimation* FadeAnim; // WBP_Conversation의 페이드 애니메이션.
	
private:
	void SetContentAsSubstring(); //한 글자씩 늘려서 출력해주는 함수.
	void PlayFadeAnim(); // 애니메이션 재생 함수


	UPROPERTY(meta = (BindWidget))
	UTextBlock* SpeakerText; 

	UPROPERTY(meta = (BindWidget))
	UTextBlock* ContentText;

	// 현재 출력 중인 대사의 전체 / 일부.
	FString FullContent;
	FString CurrentContent;

	FTimerHandle TypewriterTimerHandle; //한 글자씩 치기 위한 타이머
	FTimerHandle ClearContentHandle; // 대사가 끝난 뒤 페이드아웃을 위한 타이머
};

Conversation.cpp:

// Fill out your copyright notice in the Description page of Project Settings.


#include "HUD/Conversation.h"
#include "Managers/ConversationManager.h"
#include "Components/TextBlock.h"

void UConversation::NativeConstruct()
{
	// 자기 자신을 UConversationManager에 등록한다.
	GetGameInstance()->GetSubsystem<UConversationManager>()->SetConversationWidget(this);
	SetRenderOpacity(0.f); // 초기 투명도는 0 (안보임)
}

void UConversation::SetConversation(const FString& Speaker, const FString& Content)
{
	//타이머 초기화.
	GetWorld()->GetTimerManager().ClearTimer(TypewriterTimerHandle);
	GetWorld()->GetTimerManager().ClearTimer(ClearContentHandle);

	//화자 텍스트는 한번에 설정, 내용 텍스트는 일단 비워둠.
	SpeakerText->SetText(FText::FromString(Speaker));
	ContentText->SetText(FText::GetEmpty());
	
    // 투명도 1
	SetRenderOpacity(1.f);
	
    // 전체 대사 저장.
	FullContent = Content;
	
    // 0.05초마다 SetContentAsSubstring을 호출한다.
	GetWorld()->GetTimerManager().SetTimer(TypewriterTimerHandle, this, &UConversation::SetContentAsSubstring, 0.05f, true);
}

void UConversation::SetContentAsSubstring()
{
	// 현재 내용의 길이에서 1 증가한 것을 현재 길이로
	int CurrLength = CurrentContent.Len() + 1;

	// *** Mid는 Substring과 같다. ***
	CurrentContent = FullContent.Mid(0, CurrLength);
	ContentText->SetText(FText::FromString(CurrentContent));

	// 끝까지 다 출력했다면
	if (CurrLength == FullContent.Len())
	{
		GetWorld()->GetTimerManager().ClearTimer(TypewriterTimerHandle); // 타이머 클리어

		// 2초 뒤 페이드아웃 애니메이션 재생
		GetWorld()->GetTimerManager().SetTimer(ClearContentHandle, this, &UConversation::PlayFadeAnim, 2.f, false);
	}
}

void UConversation::PlayFadeAnim()
{
	PlayAnimation(FadeAnim);
}

 

 

 

테스트를 위해 KannaCharacter의 BeginPlay에서 ConversationManager를 통해 대사를 출력하도록 해봤다.

원작과 비슷하게 한 글자씩 다다다닥 잘 나오는 것을 볼 수 있었다.