멈추지 않고 끈질기게

[Unreal] 최종 프로젝트 7일차 - 감정 애니메이션 / 대화 로직 재구성 본문

포트폴리오

[Unreal] 최종 프로젝트 7일차 - 감정 애니메이션 / 대화 로직 재구성

sam0308 2024. 5. 21. 19:15

※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다.

※ 해당 포스팅은 청년취업사관학교 교육 과정의 최종 프로젝트에 관한 내용을 포함하고 있습니다.

 

※ 해당 포스팅은 Unreal 5.4.1 버전을 기준으로 작성되었습니다.

 

 

 

0. 서론

 오늘은 어제 개선된 NPC 대화 로직의 반환값을 이용하여 감정에 따른 애니메이션을 출력하도록 로직을 추가하고, 통신 간격을 최대한 덜 느껴지게 하도록 대화 로직을 개선하였습니다. 

 

 

1. 감정에 따른 애니메이션 출력

 어제 NPC 대화 로직을 갱신하면서 답변에 NPC의 감정도 같이 받아오게 되었으므로, 이를 이용해서 감정에 맞는 애니메이션을 출력하는 로직을 구현하였습니다. 

 

// NPCBase
void ANPCBase::ResponseSpeech(const FString& FilePath, const FString& Emotion)
{
	if (MediaPlayer->OpenFile(FilePath))
	{
		MediaComp->SetMediaPlayer(MediaPlayer);
		UE_LOG(LogTemp, Warning, TEXT("Open File Success : %s"), *FilePath);
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("Open File Failed : %s"), *FilePath);
	}

	if(false == Emotion.IsEmpty())
	{
		AnimInstance->PlayMontage_Emotion(Emotion);
	}
}
// NPC AnimInstance
UNPCAnimInstance::UNPCAnimInstance()
{
	/* 기타 내용들 */

	// Mapping
	EmotionMap.Add(TEXT("joy"), 0);
	EmotionMap.Add(TEXT("sadness"), 1);
	EmotionMap.Add(TEXT("surprise"), 2);
	EmotionMap.Add(TEXT("anger"), 3);
}

void UNPCAnimInstance::PlayMontage_Emotion(const FString& Emotion)
{
	if(false == EmotionMap.Contains(Emotion))
	{
		UE_LOG(LogTemp, Warning, TEXT("There is no emotion: %s"), *Emotion);
		Montage_Play(Montage_Indiff, 1.0f);
		return;
	}

	const int32 Idx = EmotionMap[Emotion];
	Montage_Play(EmotionMontages[Idx], 1.0f);
}

 

 

 

2. 대화 로직 재구성

 그리고 STT -> TTS로 대화하는 경우 다소 타임로스가 있어서, 유저가 최대한 로딩시간을 덜 느끼게 하기 위해서 대화 로직을 개선하기로 하였습니다. 기존에는 마이크 입력이 끝나자마자 대화 UI가 나와서 로딩시간이 길게 느껴지는 부분이 있어, 다음과 같은 순서로 로직을 변경하기로 했습니다.

  1. 마이크 입력 완료 후, 주변의 NPC를 탐지
  2. 대상이 있다면 해당 NPC의 이동을 멈추게 하고 통신 시작, 동시에 입력된 목소리 출력
  3. 출력 완료 후 NPC가 플레이어를 향해 회전
  4. 대화 UI 노출

 위와 같은 로직으로 구성하기 위해 마이크 입력 내용을 먼저 출력하고, 기존의 NPC를 대화 상태로 바꾸는 내용과 대화 UI가 나오는 부분을 분리할 필요가 있었습니다.

 

// STT Component
void USTTComponent::BeginPlay()
{
	Super::BeginPlay();

	Player = CastChecked<ACharacter>(GetOwner());
	MyGameMode = CastChecked<AFarmLifeGameMode>(GetWorld()->GetAuthGameMode());

	MediaComp = Cast<UMediaSoundComponent>(Player->GetComponentByClass(UMediaSoundComponent::StaticClass()));
	MediaPlayer = NewObject<UMediaPlayer>();
	MediaPlayer->OnEndReached.AddDynamic(this, &USTTComponent::OnPlayEnded);

	if(MediaComp)
	{
		if (MediaPlayer->OpenFile(DefaultFilePath))
		{
			MediaComp->SetMediaPlayer(MediaPlayer);
			UE_LOG(LogTemp, Warning, TEXT("[STTComponent] Initialize Complete"));
		}
	}
}

void USTTComponent::PlayVoice(const FString& FilePath)
{
	if(nullptr == MediaComp)
	{
		UE_LOG(LogTemp, Warning, TEXT("No MediaSoundComponent"));
		return;
	}

	FTimerHandle Handle;
	GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([this, FilePath]()
	{
		if (MediaPlayer->OpenFile(FilePath))
		{
			MediaComp->SetMediaPlayer(MediaPlayer);
			UE_LOG(LogTemp, Warning, TEXT("Open File Success : %s"), *FilePath);
		}
		else
		{
			UE_LOG(LogTemp, Warning, TEXT("Open File Failed : %s"), *FilePath);
		}
	}), 0.3f, false);
}

void USTTComponent::OnPlayEnded()
{
	if(false == MediaPlayer->IsClosed())
	{
		MediaPlayer->Close();
		MyGameMode->StartConversation();
	}
}

 

 이를 위해서 플레이어에 MediaSoundComponent를 추가하고, STT용 컴포넌트의 BeginPlay()에 위와 같이 MediaSoundComponent를 초기화하는 코드를 추가하였습니다. 그리고 MediaPlayer의 OnEndReached에서 게임모드의 대화 시작 함수를 호출하도록 작성했습니다. PlayVoice()의 경우 마이크 입력이 끝난 직후에 바로 호출하면 간헐적으로 이슈가 발생하여, Timer로 약간의 딜레이를 주고 실행하도록 했습니다.

 

void ANPCBase::StartWait()
{
	NPCController->StartConversation();
}

void ANPCBase::StartConversation()
{
	if(ACharacter* Player = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0))
	{
		// Turn to player
		FVector DirectionVec = (Player->GetActorLocation() - GetActorLocation()).GetSafeNormal();
		DirectionVec.Z = 0;
		const FRotator TargetRot= DirectionVec.ToOrientationRotator();
		SetActorRotation(TargetRot);

		AnimInstance->PlayMontage_Conv();
	}
}

 

 그리고 기존의 BT에서 InConversation key를 true로 설정하는 부분과 회전 및 대화 애니메이션을 시작하는 부분을 나누어습니다. 

 

 

3. 기타(OpenVoice 코드 병합)

 팀원분이 좀 더 나은 TTS를 사용하기 위해 OpenVoice 사용법을 익혀서 새로운 TTS 코드를 웹서버에 추가하였습니다. 환경 설정이 좀 번거로웠지만, 대신에 코드 병합 과정 자체는 파일 경로 등 몇 줄만 수정하면 되어서 생각보다 금방 끝났습니다. 아직 학습용 리소스가 적어서 목소리별 구분은 다시 배제하였지만, 리소스 추가에 따라 좀 더 다양한 목소리를 들을 수 있을 것으로 기대됩니다. 

 

 다만 chat-gpt에서 제공하는 기본 tts-1 모델을 사용할 때보다 아무래도 소요시간이 길어지는 단점은 있었습니다. 기존의 로직에서는 wav 파일 인코딩을 수정하기 위해 ffmpeg를 통한 변환까지 사용했었는데, 해당 부분이 빠졌음에도 불구하고 기존보다 2~3초 정도 시간이 더 걸리는 듯 합니다. 결국 퀄리티 vs 속도의 문제가 되었는데, 해당 부분은 내일 회의자리에서 논의할 필요가 있겠습니다.