Sparta Unreal 부트캠프 106일차

정찬호·2026년 5월 1일

코딩 테스트

프로그래머스 - 배달

실패 코드 - 이전 코드 + 예외 처리 추가

#include <iostream>
#include <vector>
#include <queue>

using namespace std;

struct Node
{
    int start;
    int end;
    int cost;
};

void BFS(const vector<vector<int>>& graph, vector<int>& currentDist, int start,  int limitCost)
{
    queue<Node> q;
    for(int i = 0; i < currentDist.size(); i++)
    {
        if(currentDist[i] < limitCost)
        {
            Node node;
            node.start = start;
            node.end = i;
            node.cost = currentDist[i];
            q.push(node);
        }
    }
    
    while(!q.empty())
    {
        Node curNode = q.front();
        q.pop();
        
        int start = curNode.end;
        int curCost = curNode.cost;
        
        for(int i = 0; i < graph[start].size(); i++)
        {
            int end = i;
            int newCost = curCost + graph[start][i];
            if(newCost  > limitCost)
            {
                continue;
            }
            else if(newCost < currentDist[i])
            {
                currentDist[i] = newCost;
                Node newNode;
                newNode.start = start;
                newNode.end = end;
                newNode.cost = newCost;
                q.push(newNode);
            }
        }
    }
}

int solution(int N, vector<vector<int> > road, int K) {
    int answer = 0;
    vector<vector<int>> graph(N, vector<int>(N, K + 1));
    vector<int> minDists;
    
    for(vector<int> row : road)
    {
        int startNode = row[0] - 1;
        int endNode = row[1] - 1;
        if(startNode == endNode)
        {
            continue;
        }
        
        int cost = row[2];
        int curCost = graph[startNode][endNode];
        
        if(curCost > cost)
        {
            graph[startNode][endNode] = cost;
            graph[endNode][startNode] = cost;
        }
    }
    
    for(int cost : graph[0])
    {
        minDists.push_back(cost);
    }
    
    BFS(graph, minDists, 0, K);
    
    for(int dist : minDists)
    {
        if(dist <= K)
        {
            answer++;
        }
    }
    return answer;
}

도움을 좀 받았습니다.
1. 기존 BFS 대신 다익스트라 알고리즘(priority_queue 사용)을 고려해 보기
2. 시작점에서 시작점으로 가는 비용을 0으로 처리해라.

2번은 잘 모르겠네요. 시작 노드로 다시 돌아갈 일도 없고, 돌아간다고 해도 오히려 비용이 커질 뿐이라 중간에 제거될 루트일 텐데 굳이? minDists[0] = 0;

아 자신의 마을에 배달한다는 걸 생각 못하고 있었네요. 그러면 0으러 초기화 하는게 맞겠네요.
마을에서 나가고 돌아오는 최소값이 K를 넘을 가능성을 생각 못하고 있었네요.

일단 정답 코드

#include <iostream>
#include <vector>
#include <queue>

using namespace std;

struct Node
{
    int start;
    int end;
    int cost;
};

void BFS(const vector<vector<int>>& graph, vector<int>& currentDist, int start,  int limitCost)
{
    queue<Node> q;
    for(int i = 0; i < currentDist.size(); i++)
    {
        if(currentDist[i] < limitCost)
        {
            Node node;
            node.start = start;
            node.end = i;
            node.cost = currentDist[i];
            q.push(node);
        }
    }
    
    while(!q.empty())
    {
        Node curNode = q.front();
        q.pop();
        
        int start = curNode.end;
        int curCost = curNode.cost;
        
        for(int i = 0; i < graph[start].size(); i++)
        {
            int end = i;
            int newCost = curCost + graph[start][i];
            if(newCost  > limitCost)
            {
                continue;
            }
            else if(newCost < currentDist[i])
            {
                currentDist[i] = newCost;
                Node newNode;
                newNode.start = start;
                newNode.end = end;
                newNode.cost = newCost;
                q.push(newNode);
            }
        }
    }
}

int solution(int N, vector<vector<int> > road, int K) {
    int answer = 0;
    vector<vector<int>> graph(N, vector<int>(N, K + 1));
    vector<int> minDists;
    
    for(vector<int> row : road)
    {
        int startNode = row[0] - 1;
        int endNode = row[1] - 1;
        if(startNode == endNode)
        {
            continue;
        }
        
        int cost = row[2];
        int curCost = graph[startNode][endNode];
        
        if(curCost > cost)
        {
            graph[startNode][endNode] = cost;
            graph[endNode][startNode] = cost;
        }
    }
    
    for(int cost : graph[0])
    {
        minDists.push_back(cost);
    }
    minDists[0] = 0;
    
    BFS(graph, minDists, 0, K);
    
    for(int dist : minDists)
    {
        if(dist <= K)
        {
            answer++;
        }
    }
    return answer;
}

다익스트라 적용

#include <iostream>
#include <vector>
#include <queue>

using namespace std;

int solution(int N, vector<vector<int> > road, int K) {
    int answer = 0;
    vector<vector<int>> graph(N, vector<int>(N, K + 1));
    vector<int> minDists(N, K + 1);
    minDists[0] = 0;
    
    for(vector<int> row : road)
    {
        int startNode = row[0] - 1;
        int endNode = row[1] - 1;
        if(startNode == endNode)
        {
            continue;
        }
        
        int cost = row[2];
        int curCost = graph[startNode][endNode];
        
        if(curCost > cost)
        {
            graph[startNode][endNode] = cost;
            graph[endNode][startNode] = cost;
        }
    }
    
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> que;
    que.push(make_pair(0,0));
    
    while(!que.empty())
    {
        pair<int,int> nowNode = que.top();
        que.pop();
        
        int start = nowNode.second;
        int cost = nowNode.first;
        for(int i = 0; i < graph[start].size(); i++)
        {
            int newCost = cost + graph[start][i];
            if(minDists[i] > newCost)
            {
                minDists[i] = newCost;
                que.push(make_pair(newCost, i));
            }
        }
    }
    
    for(int dist : minDists)
    {
        if(dist <= K)
        {
            answer++;
        }
    }
    return answer;
}


Unreal Engine 고급 기술 활용

1. BTTask_FindInteractiveObject 태스크 테스트

올바른 결과

  • 예상 결과: Character AI는 BTT_FindInteractiveObject의 로직대로 태그를 가진 가장 가까운 Object를 탐색한 후, 해당 Interactive Object의 Location으로 이동

현재 상태

  • 아무런 움직이 없는 상태로 스폰된 위치에 정지되어 있음

버그 분석 중

  • 배치된 오브젝트에 태그가 정상적으로 부여된 것을 확인
  • 태그를 가진 가장 가까운 오브젝트는 BB에 업데이트가 되는 것을 확인
    ===> BTT_FindInteractiveObject는 로직대로 태그를 가진 가장 가까운 Object를 탐색하여 BB에 관련 데이터를 업데이트 하고 있음, 움직이지 않는 것은 별개의 문제

== 이미지 BT

위 이미지처럼 BT도 정상적으로 배치 및 값 설정이 되어 있음.

== 이미지 Object의 배치 문제인지 확인하기 위해 수정 진행

중앙에 매쉬가 생기도록 높이를 낮춘 경우 Agent가 이동하는 것을 확인할 수 있었습니다.

해결 : AcceptableRadius 5.0f -> 150.0f

AI 튜터 에이타니의 도움을 받으면 여러 가능성(단순 오타, Nav Mesh 설정, MoveTo 노드 설정)을 지워나간 결과 AcceptableRadius 값이 낮아 도착점 근처에도 도달하지 못하기에 이동을 시도하자마자 실패처리가 나왔던 것으로 보입니다.

==> abort 문제가 여전히 발생 중인 것을 확인했습니다.
BP를 다시 확인할 결과, MoveTo 노드에 같혀서 흐름이 빠져나오질 못하고 있습니다

모든 문제의 원인이 이놈이지 않을까 싶습니다. 강제로 Agent를 끓어다 배치하니 Interact 몽타주를 정상적으로 동작해서 상호작용이 되는 것을 확인했습니다. 감지 자체는 정상적으로 되고 있는 것을 보아 MoveTo를 사용한 이동에서 문제가 발생하고 있는 것으로 추측됩니다. 아예 체크 리스트를 분리해야 겠습니다.

결론 : 감지는 정상적으로 진행. 이동에서 문제 발생 중. 이동에 관한 문제는 별개의 체크 리스트로 분리.


  1. InteractiveBox 태스크 테스트
    InteractiveBox 는 레벨 내에서 BTTask_InteractWithObject의 코드 ExecuteTask()로 InteractiveBox(자신)의 Interact_Implementation()메서드를 실행시키는 지 확인합니다.

오브젝트의 상호작용을 호출하는 코드는
정상적인 결과

  • InteractiveBox 는 BTTask_InteractWithObject의 코드 ExecuteTask()로 InteractiveBox(자신) 내의 Interact_Implementation()메서드를 실행시키고 그 결과, 자신의 머티리어을 ActiveMaterial로 전환시킨다.

테스트 결과

  • 해당 오브젝트로의 이동이 불가능한 문제가 발생해 강제로 테스트 가능한 상황을 만들었습니다.(AI Agent를 강제로 끓어다 올려놓기)
  • 강제로 거리가 가까워지니 Agent가 몽타주를 재생하며 공격을 하고 그에 따라 머티리얼이 변경되는 것을 확인했습니다.

  1. BTTask_InteractWithObject 태스크 테스트
    2번 태스크와 같이 테스트되었습니다. haracter AI는 레벨 내 배치된 Interactive Object(Box)와 Behavior Tree 및 Blackboard 등을 통해 BTTask_InteractWithObject의 로직대로 상호작용하는 지를 테스트합니다.

정상 결과

  • BTTask_InteractWithObject 로직대로 상호작용 몽타주 재생 후 노티파이로 오브젝트와의 상호작용을 호출합니다. 몽타주 재생이 확인되고, 오브젝트도 상호작용에 대한 피브백이 발생합니다.

테스트 결과

  • Object로의 추적은 발생했으나 몽타주가 재생되지 않았습니다. 물론 Object에 대한 Interact도 발생하지 않았습니다.

버그 분석 중

BT를 확인한 결과, 1번 태스크의 MoveTo와 같이 InteractWithObject에 흐름이 들어가자 마자 끊기는 것을 확인했습니다.

=== BT 이미지

=== Intearct With Object Details
몽타주와 BlackboardKey는 정상적으로 등록되어 있습니다. InteractionDistance는 150으로 설정되어 있습니다.

시도 1. InteractionDistance 값을 300로 변경하기
아무런 변화가 없습니다. 원래의 값으로 되돌립니다.

시도 2. 코드 분석
BTTask_InteractWithObject 코드를 분석하던 중 오타로 추정되는 부분을 확인했습니다.

UObject* TargetObject = BlackboardComp->GetValueAsObject(GetSelectedBlackboardKey());
	AActor* TargetActor = Cast<AActor>(TargetObject);
	
	if (TargetActor)
	{
		return EBTNodeResult::Failed;
	}

조건문에 TargetActor를 !TargetActor로 수정한 후 테스트를 진행했으나 아직 문제가 지속되고 있습니다.

시도 3. MoveTo 수정
이전 테스트 1에서의 수정한 것은 현재 테스트 중인 태스크용 액터에서는 적용되지 않았습니다. AcceptableRadius를 크게 늘려 보았지만 효과는 없었습니다.

시도 4. Visual Logger 사용해보기
콘솔에 vislog를 입력하면 visual logger가 나옵니다.
여기서 런타임에 로그가 남는데 각 Controller의 동작 타임라인 것을 기록해주어서 시간별 로그를 확인할 수 있습니다.

=== visual Logger 이미지

로그를 확인해보니 다음과 같은 내용이 나와있었습니다.

LogBehaviorTree (VeryVerbose) Suspending branch actions.
LogBehaviorTree (VeryVerbose) Resuming branch actions.
LogBehaviorTree (VeryVerbose) UBehaviorTreeComponent::ProcessExecutionRequest Active Node BT_CoolGuy::Wait[2]
LogBehaviorTree (Verbose) Deactivate node: BT_CoolGuy::Wait[2]
LogBehaviorTree (Verbose) Testing node: BT_CoolGuy::Sequence[0]
LogBehaviorTree (VeryVerbose) UBehaviorTreeComponent::ProcessPendingExecution
LogBehaviorTree (VeryVerbose) Suspending branch actions.
LogBehaviorTree (VeryVerbose) Logging All Aux Nodes
LogBehaviorTree (Log) Execute task: BT_CoolGuy::Move To[3]
LogBehaviorTree (Verbose) 'Move To' task implementing move with task AITask_MoveTo_1653
LogAINavigation (Log) MoveTo: Location(X=260.000 Y=-820.000 Z=380.000) Mode(partial path) Filter(None) AcceptanceRadius(500.0 + overlap) StartsFromPreviousPath(No))
LogPathFollowing (Log) OnPathFinished: Invalid[]
LogBehaviorTree (VeryVerbose) BT(88383) schedule next tick 0.000000, asked 0.000000.
LogBehaviorTree (Log) Task BT_CoolGuy::Move To[3] finished: Failed
LogBehaviorTree (Verbose) Request evaluation queued up for node(BT_CoolGuy::Move To[3]) with result(EBTNodeResult::Failed)
LogBehaviorTree (VeryVerbose) Resuming branch actions.
LogBehaviorTree (Log) Execution request by BT_CoolGuy::Move To[3] (result: Failed)
LogBehaviorTree (VeryVerbose) BT(88383) schedule next tick 0.000000, asked 0.000000.
LogBehaviorTree (VeryVerbose) Suspending branch actions.
LogBehaviorTree (VeryVerbose) Resuming branch actions.
LogBehaviorTree (VeryVerbose) BT(88383) schedule next tick 0.000000, asked 340282346638528859811704183484516925440.000000.

AI 튜터 에이타니에게 분석을 요청한 결과 Z 위치 값으로 인해 문제가 생겼을 가능성을 제시했습니다.
강제로 Z 값을 0으로 통일하고 시도해봤지만 효과는 없었습니다.

시도 5. 갖에로 Interact 상황 만들기
ClosestObject로 사용할 액터들 중 하나를 얇고 넓찍한 판으로 만들어 agent를 강제로 올려두어 보았습니다.
정상적으로 상호작용이 진행되어 몽타주 재생 -> ClosestObject의 머티리얼 변경을 확인할 수 있었습니다.

결론 : 감지 및 상호작용은 문제 없음. 문제는 이동.

4. 이동 문제 분석하기

이놈 때문에 낭비된 시간이 너무 아깝씁니다.
기존에 다른 태스크들을 테스트하면서 여러 시도를 했었기에 중복되는 시도는 생략하겠습니다.

시도 1. 아예 ClosestObjectLocation에 값을 집어넣을 때 투영된 값을 집어넣기
Can Ever Affect Navigation은 기본적으로 체크되어 있었기에 문제가 되질 않고, 다른 여러 설정을 건드려봐도 변화는 없었고, 화면에 위치 투영이 안 된다는 문구도 나오니 그냥 집어넣을 때 강제로 투영해서 집어넣기로 결정했습니다.

// BlackboardComp->SetValueAsVector("ClosestObjectLocation", ClosestObject->GetActorLocation());
		FNavLocation ProjectedLocation;
		UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetCurrent(GetWorld());

		// Query Extent X,Y를 크게 잡아 검은 영역 바깥 NavMesh까지 탐색
		if (NavSystem && NavSystem->ProjectPointToNavigation(
			ClosestObject->GetActorLocation(), 
			ProjectedLocation, 
			FVector(1000, 1000, 500)))  // 범위를 넓게
		{
			BlackboardComp->SetValueAsVector("ClosestObjectLocation", ProjectedLocation.Location);
		}
		else
		{
			// 투영 실패 시 GetRandomReachablePointInRadius로 근처 도달 가능한 점 사용
			FNavLocation RandomLocation;
			if (NavSystem && NavSystem->GetRandomReachablePointInRadius(
				ClosestObject->GetActorLocation(), 
				300.f,  // 액터 주변 300 범위
				RandomLocation))
			{
				BlackboardComp->SetValueAsVector("ClosestObjectLocation", RandomLocation.Location);
			}
			else
			{
				return EBTNodeResult::Failed;
			}
		}

정상적으로 이동이 되는 것을 확인했습니다!

엔진 버전이 달라서 MoveTo 지원 기능이 다른 건지.. 아니면 어딘가에서 실수가 있던 건지 모르겠지만 강제 투영으로 해결이 되서 다행이네요.

5. BTService_EmotionChanger태스크 테스트

Behavior Tree 및 Blackboard 등을 통해 BTService_EmotionChanger의 로직대로 상호작용하지를 테스트합니다.

정상 결과

  • haracter AI는 BTService_EmotionChanger의 로직대로 감정 상태가 달라지고, 최종 감정에 따라 몸동작이 빠르거나 느려져야 합니다.

테스트 결과

  • 상태가 Sad로만 고정되는 문제가 발생했습니다.

시도 1. 상태 변경 코드 분석

// 랜덤 감정 상태 가져오기
            EEmotionalState NewState = GetRandomEmotionalState();
			
			// 현재 감정과 다를 경우에만 변경 -> 굳이 uint8로 변경한뒤 다시 변경하는 이유가?
			uint8 CurrentEmotion = BlackboardComp->GetValueAsEnum(EmotionalStateKey);
			if (static_cast<EEmotionalState>(CurrentEmotion) == NewState)
			{
				// 블랙보드에 새 감정 상태 설정
				BlackboardComp->SetValueAsEnum(EmotionalStateKey, static_cast<uint8>(NewState));
				

주석에는 새로운 감정 값이 기존 감정과 다를때만 변경인데 지금은 같을 때만 대입해주는 로직으로 되어 있습니다. 오타가 있었군요.

수정했습니다.

// 랜덤 감정 상태 가져오기
            EEmotionalState NewState = GetRandomEmotionalState();
			
			// 현재 감정과 다를 경우에만 변경 -> 굳이 uint8로 변경한뒤 다시 변경하는 이유가?
			uint8 CurrentEmotion = BlackboardComp->GetValueAsEnum(EmotionalStateKey);
			if (static_cast<EEmotionalState>(CurrentEmotion) != NewState)
			{
				// 블랙보드에 새 감정 상태 설정
				BlackboardComp->SetValueAsEnum(EmotionalStateKey, static_cast<uint8>(NewState));
				

=== 로그 이미지

이제 정상적으로 동작합니다.

profile
게임 개발 지망생입니다.

0개의 댓글