멈추지 않고 끈질기게
[Unreal] 최종 프로젝트 12일차 - 식물 성장 기능 머지 / NPC 행동 패턴 고도화 본문
※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다.
※ 해당 포스팅은 청년취업사관학교 교육 과정의 최종 프로젝트에 관한 내용을 포함하고 있습니다.
※ 해당 포스팅은 Unreal 5.4.1 버전을 기준으로 작성되었습니다.
0. 서론
오늘은 어제에 이어 다른 분이 작업하신 식물 성장 기능을 합치고, 이후에는 NPC 행동 패턴을 좀 더 고도화하는 작업 위주로 진행하였습니다. 해당 과정에서 기존 로직에서 이슈가 좀 나와서 트러블 슈팅에 시간이 좀 들어갔습니다.
1. 식물 성장 기능 머지
기존에 식물 근처에서 대화 시 서버에서 긍정/부정을 판단하여 정수 형태로 반환하는 부분까지만 구현해두었는데, 식물 성장 쪽 작업하신 팀원 분의 작업물을 합쳐서 실제로 식물이 자라는 모습까지 확인하기로 했습니다. 우선 팀원분이 작성하신 APlantActor에 GrowPlant()라는 함수가 성장하는 기능임을 확인한 다음에, 서버에서 반환한 점수가 0점보다 높다면(긍정적이라면) 해당 함수를 호출하도록 했습니다.
// TalkComponent
void UTalkComponent::SearchNearby(const FString& InputText)
{
/* NPC 탐지하는 내용 */
// NPC가 없다면 식물 탐지
TArray<FOverlapResult> PlantResults;
bool PlantOverlapped = GetWorld()->OverlapMultiByChannel(PlantResults, Origin, FQuat::Identity, ECC_Visibility, FCollisionShape::MakeSphere(300.0f), Params);
if (PlantOverlapped)
{
TArray<TObjectPtr<APlantActor>> Plants;
for (FOverlapResult& Res : PlantResults)
{
// TArray에 저장
APlantActor* Plant = Cast<APlantActor>(Res.GetActor());
if(Plant)
{
Plants.Add(Plant); UE_LOG(LogTemp, Log, TEXT("%s"), *Plant->GetName());
}
}
if(InputText.IsEmpty())
{
TalkToPlant(Plants);
}
else
{
TalkToPlantByText(Plants, InputText);
}
}
}
void UTalkComponent::TalkToPlant(const TArray<TObjectPtr<APlantActor>>& NewPlants)
{
MyGameMode->TalkToPlant(RecordFileName, RecordFilePath, NewPlants);
}
void UTalkComponent::TalkToPlantByText(const TArray<TObjectPtr<APlantActor>>& NewPlants, const FString& InputText)
{
MyGameMode->TalkToPlantWithText(InputText, NewPlants);
}
// GameMode
void AFarmLifeGameMode::TalkToPlant(const FString& FileName, const FString& FilePath, const TArray<TObjectPtr<APlantActor>>& NewPlants)
{
CurPlants = NewPlants;
HttpActor->TalkToPlant(FileName, FilePath);
}
void AFarmLifeGameMode::SetTalkScore(int32 Score)
{
if(Score == 0)
{
return;
}
UE_LOG(LogTemp, Warning, TEXT("Cur Plants Size: %d"), CurPlants.Num());
const bool IsPositive = Score > 0;
for(APlantActor* P : CurPlants)
{
// 긍정적이라면 작물 성장
if(IsPositive)
{
P->GrowPlant(); UE_LOG(LogTemp, Warning, TEXT("Grow Plant"));
}
}
}
마이크 입력 혹은 채팅 시 주변의 NPC를 먼저 탐지하고, 없다면 APlantActor를 탐지합니다. 식물들과는 꼭 1:1로 대화할 필요는 없으므로 탐지된 식물들을 TAraa
2. NPC 행동 패턴 고도화(업무 행동 추가)
추가로 NPC 행동 패턴을 고도화 하기 위해 지난번에 배치만 해 두었던 업무 행동(직업)에 내용을 추가하기로 했습니다. 일단은 NPC마다 고유의 업무 애니메이션을 추가하고, 행동 트리에서 특정 위치 이동 후 업무 상태로 변경하여 일하는 애니메이션을 보여주는 정도로 구성하였습니다.
// 커스텀 노드
EBTNodeResult::Type UBTTask_DoJob::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
EBTNodeResult::Type SuperReuslt = Super::ExecuteTask(OwnerComp, NodeMemory);
APawn* OwnerPawn = OwnerComp.GetAIOwner()->GetPawn();
if(nullptr == OwnerPawn)
{
return EBTNodeResult::Failed;
}
ANPCBase* NPC = Cast<ANPCBase>(OwnerPawn);
if(nullptr == NPC)
{
return EBTNodeResult::Failed;
}
NPC->DoJob();
return EBTNodeResult::Succeeded;
}
// NPCBase
void ANPCBase::DoJob()
{
UE_LOG(LogTemp, Warning, TEXT("[%s] Do Job"), *NPCName);
}
/////////////////////////////////////////////////////////////
// 상속받은 NPC 클래스(오버라이딩)
void ANPC_Blacksmith::DoJob()
{
Super::DoJob();
AnimInstance->PlayMontage_Custom(Montage_Work);
}
void ANPC_Blacksmith::OnHourUpdated(int32 NewHour)
{
if(NewHour == HOUR_GO_WORK)
{
NPCController->MoveToTargetLoc(WorkLoc);
NPCController->SetIsWorking(true);
return;
}
if (NewHour == HOUR_BACK_HOME)
{
NPCController->MoveToHome();
NPCController->SetIsWorking(false);
AnimInstance->StopSpecificMontage(Montage_Work);
}
}
또한 블랙보드에 IsWorking이라는 bool 타입 키를 추가하고, NPCController에서 해당 키를 변경하는 함수를 추가하여 NPC가 일하는 곳으로 이동할 때 true로 변경하도록 했습니다.
그런데 해당 방식으로 변경하고 보니, 일하는 부분이 특정 위치 이동보다 우선순위가 높으면 안될 것으로 보여 변경하였습니다. 최종적으로 1. 대화중 2. 플레이어에게 대화하려고 접근 3. 특정 위치 이동 4. 일하기 5. 주변 배회 의 순서로 구성하였습니다. 일하는 중인지 체크하는 데코레이터도 추가하여 일단 다음과 같이 완성하였습니다.
3. 기타 이슈 해결(타이머 관련)
오늘은 자잘한 이슈가 좀 있었는데, 가장 큰 이슈는 NPC 대화창을 도중에 닫을 경우 잠시 후에 크래시가 나는 이슈였습니다. 타이머 관련 이슈로 보여, 우선 대화창을 닫을 때 FTimerHandle 구조체의 IsValid() 함수를 사용해서 체크한 후에 타이머가 동작중이라면 Clear()를 호출하도록 했습니다.
// NPC 대화 UI
void UNPCConversationWidget::NativeConstruct()
{
Super::NativeConstruct();
/* 기타 내용들 */
OnVisibilityChanged.AddDynamic(this, &UNPCConversationWidget::OnHidden);
}
void UNPCConversationWidget::OnHidden(ESlateVisibility InVisibility)
{
if(InVisibility != ESlateVisibility::Hidden)
{
return;
}
// 텍스트 스트림 중이라면 해제
if(StreamHandle.IsValid())
{
GetWorld()->GetTimerManager().ClearTimer(StreamHandle);
UE_LOG(LogTemp, Error, TEXT("Stream Stop"));
Txt_Conversation->SetText(FText::FromString(TEXT("")));
CurConvState = EConvState::None;
}
}
문제는 해당 방식으로 타이머를 끄려고 해도 여전히 크래시가 발생하였고, 로그를 확인해보니 애초에 if문 안이 호출되지도 않고 있었습니다. IsValid() 함수의 설명을 보고 해당 타이머 핸들을 통해 타이머가 돌고있는지 체크하는 함수라고 생각했는데, 그게 아니었던 모양이었습니다.
그래서 찾아보니 FTimerManager에 IsTimerActive()라는 타이머 동작 여부를 체크하는 함수가 있다는 것을 알게 되었고, 해당 방식으로 수정하여 해결할 수 있었습니다.
void UNPCConversationWidget::OnHidden(ESlateVisibility InVisibility)
{
if(InVisibility != ESlateVisibility::Hidden)
{
return;
}
// 수정한 타이머 동작 여부 체크 방식
if (GetWorld()->GetTimerManager().IsTimerActive(StreamHandle))
{
GetWorld()->GetTimerManager().ClearTimer(StreamHandle);
UE_LOG(LogTemp, Error, TEXT("Stream Stop"));
Txt_Conversation->SetText(FText::FromString(TEXT("")));
CurConvState = EConvState::None;
}
}
지금까지 FTimerHandle의 IsValid() 함수를 통해 타이머를 동작 여부를 체크할 수 있는줄 알았는데, 늦기 전에 제대로 된 타이머 확인 방법을 찾아서 다행인 듯 합니다.
'포트폴리오' 카테고리의 다른 글
[Unreal] 최종 프로젝트 14일차 - wav 파일 통신 바꾸기 (0) | 2024.05.31 |
---|---|
[Unreal] 최종 프로젝트 13일차 - NPC 실전 배치 및 테스트 / 마이크 입력 중 출력 방지 (0) | 2024.05.29 |
[Unreal] 최종 프로젝트 11일차 - STT & 채팅 기능 머지 / NPC 인사 구현 (0) | 2024.05.27 |
[Unreal] 최종 프로젝트 10일차 - EQS 가이드 따라하기 / NPC 시야 구현 (0) | 2024.05.24 |
[Unreal] 최종 프로젝트 9일차 - 파일 경로 수정, 에셋 탐색 (0) | 2024.05.23 |