언리얼 엔진 5/개발 일지

[UE5] 블루아카이브 TPS게임 개발일지 (64) - 프로젝트 마무리

ciel45 2024. 3. 8. 19:54

프로젝트를 오늘까지 끝내기로 마음먹고, 어제 밤을 샜다.

 

우선 대사가 겹쳐서 사라지는걸 막기 위해 큐를 이용해 대사들을 빠짐없이 보여주도록 하였다.

 

Conversation.h

/*생략*/

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

/*생략*/

private:
	/*생략*/

	TQueue<TPair<FString, FString>> ConversationQueue;
	TQueue<FString> MessageQueue;
};

 

Conversation.cpp

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

/*생략*/

void UConversation::SetConversation()
{
	// 큐에서 하나 빼기
	TPair<FString, FString>* Conversation = ConversationQueue.Peek();

	if(Conversation == nullptr) return;

	FString& Speaker = Conversation->Key;
	FString& Content = Conversation->Value;


	//Content 초기화
	FullContent = "";
	CurrentContent = "";

	//타이머 초기화.
	GetWorld()->GetTimerManager().ClearTimer(TypewriterTimerHandle);

	// 마지막 대사를 출력한 참이었다면
	if (GetWorld()->GetTimerManager().IsTimerActive(ClearContentHandle))
	{
		GetWorld()->GetTimerManager().ClearTimer(ClearContentHandle);//메시지 안지워지게 타이머 초기화

		FTimerHandle TimerHandle;
		GetWorld()->GetTimerManager().SetTimer(TimerHandle, [this]() {SetConversation(); }, 2.f, false); // 2초 딜레이, 재귀호출
		return;
	}

	//화자 텍스트는 한번에 설정, 내용 텍스트는 일단 비워둠.
	SpeakerText->SetText(FText::FromString(Speaker));
	ContentText->SetText(FText::GetEmpty());

	// 투명도 1
	SpeakerText->SetRenderOpacity(1.f);
	ContentText->SetRenderOpacity(1.f);
	Column->SetRenderOpacity(1.f);

	// 전체 대사 저장.
	FullContent = Content;

	// 0.05초마다 SetContentAsSubstring을 호출한다.
	GetWorld()->GetTimerManager().SetTimer(TypewriterTimerHandle, this, &UConversation::SetContentAsSubstring, 0.03f, 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); // 타이머 클리어

		//큐에서 하나 빼기
		ConversationQueue.Pop();

		// 큐가 비었다면
		if (ConversationQueue.IsEmpty())
		{
			// 5초 뒤 페이드아웃 애니메이션 재생
			GetWorld()->GetTimerManager().SetTimer(ClearContentHandle, [this]() {PlayAnimation(FadeAnim); }, 5.f, false);
		}
		else
		{
			//다음 대화 출력
			GetWorld()->GetTimerManager().SetTimer(ResumeConversationHandle, [this] {SetConversation(); }, 2.f, false);
		}
	}
}


void UConversation::SetMessage()
{
	// 마지막 메시지를 출력한 참이었다면
	if (GetWorld()->GetTimerManager().IsTimerActive(ClearMessageHandle))
	{
		GetWorld()->GetTimerManager().ClearTimer(ClearMessageHandle);//메시지 안지워지게 타이머 초기화

		FTimerHandle TimerHandle;
		GetWorld()->GetTimerManager().SetTimer(TimerHandle, [this](){SetMessage();}, 4.f, false); // 4초 딜레이, 재귀호출
		return;
	}

	GetWorld()->GetTimerManager().ClearTimer(ClearMessageHandle);

	FString* Content = MessageQueue.Peek();

	if(Content == nullptr) return;

	MessageText->SetText(FText::FromString(*Content));

	// 투명도 1
	MessageText->SetRenderOpacity(1.f);

	MessageQueue.Pop();
	if (MessageQueue.IsEmpty())
	{
		GetWorld()->GetTimerManager().SetTimer(ClearMessageHandle, [this]() {PlayAnimation(MessageFadeAnim); }, 4.f, false);
	}
	else
	{
		//다음 메시지 출력
		GetWorld()->GetTimerManager().SetTimer(ResumeMessageHandle, [this] {SetMessage(); }, 4.f, false);
	}
}

void UConversation::GetConversation(const TPair<FString, FString>& Content)
{
	bool ShouldStartConversation = ConversationQueue.IsEmpty();

	ConversationQueue.Enqueue(Content);

	if(ShouldStartConversation)
		SetConversation();
}

void UConversation::GetMessage(const FString& Content)
{
	if (!MessageQueue.IsEmpty())
	{
		if (*MessageQueue.Peek() == Content)
		{
			return;
		}
	}

	bool ShouldShowMessage = MessageQueue.IsEmpty();

	MessageQueue.Enqueue(Content);

	if (ShouldShowMessage)
		SetMessage();
}

 

그 결과로 이런 주고받는 대화도 가능해졌다.

FTimerHandle AlertHandle;
	GetWorld()->GetTimerManager().SetTimer(AlertHandle, [this]()
		{
			GetWorld()->GetGameInstance()->GetSubsystem<UConversationManager>()->SetConversation
			(
				TEXT("PMC 병사"), TEXT("공안국장이 배신했다! 공격 개시!")
			);
			GetWorld()->GetGameInstance()->GetSubsystem<UConversationManager>()->SetConversation
			(
				TEXT("칸나"), TEXT("배신이라니.. 난 너희들 편이었던 적이 없어.")
			);
		}, 5.f, false);

 

 

 

그리고 게임의 메인화면, 게임오버화면, 클리어화면들을 만들었다.

 

 

게임 클리어를 판정하는 블루프린트도 만들었다.

이벤트 그래프가 많이 복잡한데, 요약하자면 GameManager에 ActiveEnemies의 수를 따지는 CheckIfCleared 함수를 만들었고, 그것의 호출 결과에 따라 클리어 여부가 결정된다.

 

이 대사는 원작에서 90% 그대로 가져왔다.

 

https://www.youtube.com/watch?v=7DBlUX81iAg&t=323s&ab_channel=Ciel45

 

사실, 원래 시네마틱 연출도 넣고 섬광탄, 소총 등도 등장하는 이것저것 많은 게임을 만들어보고 싶었는데,

핑계라면 핑계겠지만 회사다니면서 남는 시간에 프로젝트를 하려니 이것도 굉장히 힘들었다..

그래도 언리얼 엔진 지식은 많이 쌓은 것 같다.

어제 밤을 새서 셀프 크런치모드로 완성했는데, 덕분에 이제부터는 운영체제 공부에 전념할 수 있을 것 같다.