실패 코드
#include <string>
#include <vector>
#include <queue>
#include <cmath>
using namespace std;
int getNodeCnt(const vector<vector<int>>& graph, vector<int>& parents, int startNode, pair<int, int> excludeLine)
{
int nodeCnt = 0;
queue<int> nextNodes;
nextNodes.push(startNode);
while(!nextNodes.empty())
{
int nowNode = nextNodes.front();
nextNodes.pop();
for(int newNode : graph[nowNode])
{
if(nowNode == excludeLine.first && newNode == excludeLine.second)
{
continue;
}
else if(nowNode == excludeLine.second && newNode == excludeLine.first)
{
continue;
}
if(parents[newNode] == startNode)
{
continue;
}
else if(parents[newNode] != -1)
{
return -1;
}
parents[newNode] = startNode;
nextNodes.push(newNode);
nodeCnt++;
}
}
return nodeCnt;
}
int solution(int n, vector<vector<int>> wires) {
int answer = 100;
vector<vector<int>> graph(100, vector<int>());
for(vector<int>& row : wires)
{
graph[row[0]].push_back(row[1]);
graph[row[1]].push_back(row[0]);
}
for(int i = 0; i < graph.size(); i++)
{
for(int j = 0; j < graph[i].size(); j++)
{
vector<int> parents(graph.size(), -1);
pair<int, int> excludeLine = make_pair(i, j);
int temp1 = getNodeCnt(graph, parents, i, excludeLine);
int temp2 = getNodeCnt(graph, parents, j, excludeLine);
if(temp1 == - 1 || temp2 == -1)
{
continue;
}
else if(temp1 == 100 && temp2 == 100)
{
continue;
}
answer = min(answer, abs(temp1 - temp2));
}
}
return answer;
}
문제를 다시보니 Wires(r각 송전탑을 연결하는 선들)의 개수가 n -1, 송전탑보다 하나 작은 수이며 모두 연결되어 있다는 것은 하나를 끊으면 무조건 둘로 나뉘는 상황이라는 것이니 기존에 코드를 상당히 다르게 바꿀 수 있을 것 같네요. 일단 위회해서 연결되는 루트는 존재하지 않는 다는 거니까요.
실패 코드
#include <string>
#include <vector>
#include <queue>
#include <cmath>
using namespace std;
int getNodeCnt(const vector<vector<int>>& graph, int startNode, vector<bool>& visited, pair<int, int> excludeLine)
{
int nodeCnt = 0;
queue<int> nextNodes;
nextNodes.push(startNode);
visited[startNode] = true;
while(!nextNodes.empty())
{
int nowNode = nextNodes.front();
nextNodes.pop();
for(int newNode : graph[nowNode])
{
if(visited[newNode])
{
continue;
}
if(nowNode == excludeLine.first && newNode == excludeLine.second)
{
continue;
}
else if(nowNode == excludeLine.second && newNode == excludeLine.first)
{
continue;
}
nextNodes.push(newNode);
visited[newNode] = true;
nodeCnt++;
}
}
return nodeCnt;
}
int solution(int n, vector<vector<int>> wires) {
int answer = 100;
vector<vector<int>> graph(100, vector<int>());
for(vector<int>& row : wires)
{
graph[row[0]].push_back(row[1]);
graph[row[1]].push_back(row[0]);
}
for(int i = 0; i < graph.size(); i++)
{
for(int j = 0; j < graph[i].size(); j++)
{
vector<bool> visited(graph.size(), false);
pair<int, int> excludeLine = make_pair(i, j);
int cnt1, cnt2;
cnt1 = getNodeCnt(graph, i, visited, excludeLine);
cnt2 = getNodeCnt(graph, j, visited, excludeLine);
answer = min(answer, abs(cnt1 - cnt2));
}
}
return answer;
}
여전히 틀리지만 코드의 수는 확실히 줄었습니다. 오답도 이전의 코드와 동이하게 나오고 있고요. 어디서 실수한 거람?
지금 방식
BFS를 사용해서 시작노드를 끊은 선의 시작점, 끝점으로 하고 있습니다.
추천 방식
DFS를 사용. 바닥 노드에서 다른 노드로 이동할 때 걸리는 선의 개수를 기록해서 대조한다?
Unrooted와 Rooted는 루트의 결정 여부 차이이지 호환은 문제없이 된다.
Rooted로 그래프를 변경
인덱스의 어긋남 인덱스로 만들어서 사용하자송전탑 번호를 그대로 사용하니 그래프의 크기 100과 어긋남이 발생
송전탑 번호에 1을 빼서 인덱스로 만들기
이제 견본 테스트 케이스는 통과하나, 제출 시 정답률은 46.2%
튜터님 조언 : 인덱스 같은 것은 변수로 만들어야 오류율을 줄이기 좋다
#include <string>
#include <vector>
#include <queue>
#include <cmath>
using namespace std;
int getNodeCnt(const vector<vector<int>>& graph, int startNode, vector<bool>& visited, pair<int, int> excludeLine)
{
int nodeCnt = 0;
queue<int> nextNodes;
nextNodes.push(startNode);
visited[startNode] = true;
while(!nextNodes.empty())
{
int nowNode = nextNodes.front();
nextNodes.pop();
for(int newNode : graph[nowNode])
{
if(visited[newNode])
{
continue;
}
if(nowNode == excludeLine.first && newNode == excludeLine.second)
{
continue;
}
else if(nowNode == excludeLine.second && newNode == excludeLine.first)
{
continue;
}
nextNodes.push(newNode);
visited[newNode] = true;
nodeCnt++;
}
}
return nodeCnt;
}
int solution(int n, vector<vector<int>> wires) {
int answer = 100;
vector<vector<int>> graph(100, vector<int>());
for(vector<int>& row : wires)
{
int startNode = row[0] - 1;
int endNode = row[1] - 1;
graph[startNode].push_back(endNode);
graph[endNode].push_back(startNode);
}
for(int i = 0; i < graph.size(); i++)
{
for(int j = 0; j < graph[i].size(); j++)
{
vector<bool> visited(graph.size(), false);
pair<int, int> excludeLine = make_pair(i, j);
int cnt1, cnt2;
cnt1 = getNodeCnt(graph, i, visited, excludeLine);
cnt2 = getNodeCnt(graph, j, visited, excludeLine);
answer = min(answer, abs(cnt1 - cnt2));
}
}
return answer;
}
이어서 그래프가 아니라 트리니 visited 사용 없이 스택이나 큐로도 개수를 구할 수 있다는 조언을 주셨습니다. 내일 다시 한 번 알아봐야겠네요.
내비게이션 모디파이어는 AI가 이동 경로를 계산할 때 사용하는 내비게이션 메시(NavMesh)의 특정 영역에 속성을 부여하거나 비용(Cost)을 수정하여 AI의 이동 경로를 제어하는 도구입니다.
목적 : AI가 특정 구역을 피하게 하거나, 특정 구역으로만 다니게 유도하거나, 혹은 완전히 지나가지 못하게 설정합니다.
비용(Cost) 시스템 : AI는 목적지까지 가장 '비용'이 적게 드는 경로를 찾는다고 합니다. 모디파이어를 통해 특정 구역의 비용을 높이면 AI는 더 멀더라도 비용이 낮은 깨끗한 길을 선택하게 됩니다.
NavModifier는 Volume과 Component의 2 종류가 존재합니다.
레벨 에디터의 Plcae Actor(액터 배치) 패널에서 배치할 수 있는 브러시 형태의 액터입니다.
액터 내부에 추가하 수 있는 컴포넌트입니다.
모디파이어가 NavMesh를 어떻게 바꿀지 결정하는 데이터 세트입니다. NavArea 클래슬르 상속받아 직접 만들수도 있습니다.
내비게이션 링크 프록시는 내비게이션 경로가 직접적으로 이어지지 않는 두 내비게이션 메시 영역을 간접적으로 연결합니다.
일반적으로 분리된 내비게이션 메시를 연결하는 다리를 생성하고, 목적지로 가는 연속된 경로를 이용할 수 없을 때 에이전트가 목적지를 향해 플랫폼에서뒤어내리거나 점프하도록 지시하는데 사용됩니다.
포인트 링크(Point Links)
Smart Links(스마트 링크)
단순한 위치 연결을 넘어, AI가 해당 링크에 도달햇을 때 특수 동작(애니메이션)을 수행하도록 제어할 수 있는 기능입니다.
최신 버저의 Unreal Engine에는 일일이 프록시를 배치하지 않아도 자동 내비게이션 링크 생성 기능을 지원한다고 합니다.
언리얼 엔진의 내비게이션 시스템에서 핵시점인 액터로, 레벨 내의 콜리전 지오메트리를 분석하여 AI 캐릭터가 이동할 수 있는 보행 가능 영역(NavMesh)을 생성하고 관리한다고 합니다.
주요 개념 및 역할
핵심 설정 항목(Generaction 섹션)
RecastNavMesh 액터를 선택한 후 Details(상세) 패널에서 조정할 수 있는 주요 설정들입니다.
주의 : 이 설정들은 AI의 캡슐 컴포넌트 크기와 일치해야 정확한 이동이 가능하다고 합니다.
성능 및 정밀도 최적화
최적화 및 주의사항
Navigation Invoker(내비게이션 인보커)는 대규모 오픈 월드나 무한 맵에서 내비게이션 시스템을 효율적으로 운영하기 위한 핵심 도구입니다.
인보커는 “필요한 곳에만 실시간으로 경로를 생성”하는 역할을 합니다.
인보커 컴포넌트를 액터에 추가하면 다음과 같은 핵심 설정을 조절할 수 있습니다.
Tile Generation Radius (생성 반경) : 에이전트를 중심으로 NavMesh를 생성할 거리입니다. AI의 감지 범위나 이동 속도를 고려하여 설정합니다. (인식 범위보다 약간 크게?)
Tile Removal Radius(제거 반경) : 에이전트가 멀어졌을 때 생성된 NavMesh를 파기할 거리입니다.
메모리 최적화 : 오픈 월드 게임에서 수 킬로미터에 달하는 NavMesh를 미리 빌드하여 저장하면 데이터 용량이 매우 커집니다. 인보커는 현재 활동 중인 에이전트 주변만 계산하므로 메모리 점유율이 낮습니다.
로딩 시간 단축 : 레벨을 로드할 때 거대한 NavMesh 데이터를 불러올 필요가 없어 초기 로딩 속도가 향상됩니다.
동적 환경 대응 : Dynamic Runtime Generation과 결합하여, 실시간으로 변하는 지형이나 장애물에도 즉각적으로 반응하는 경로를 생성합니다.
Invokers Maximum Distance from Seed : 프로젝트 세팅에서 설정 가능합니다. 플레이어(Seed)로부터 너무 멀리 떨어진 곳에 있는 인보커(멀리 있는 AI 등)는 NavMesh 생성을 중단하게 하여 CPU 자원을 절약한다고 합니다. (멀티에서는 어떻게 동작할 지 궁급하네요.)
Fixed Tile Pool Size : 월드 파티션(World Partition)을 사용하는 경우, 생성되는 타일의 최대 개수를 제한하여 내비게이션 시스템이 사용하는 메모리 상한선을 강제로 고정할 수 있습니다.
각각의 목적과 부하의 성격이 다르므로, 프로젝트의 규모와 AI의 복잡도에 따라 우선순위를 결정해야 한다고 합니다.
대규모 레벨 기준
| 시스템 | 핵심 용도 | 성능 부하 (CPU/Mem) | 우선순위 및 사용 고려 사항 |
|---|---|---|---|
| Nav Invoker | 대규모 맵 최적화 | CPU 상(실시간 빌드) / Mem 최저 | 1순위 (월드 규모 기준): 오픈 월드라면 가장 먼저 고려. 에이전트가 많을수록 CPU 부하가 증가하므로 생성 반경 최적화 필수. |
| Nav Modifier | 경로 선호도 및 속성 제어 | CPU 저 / Mem 저 | 2순위 (디테일 기준): AI의 지능적인 움직임(길 찾기 전략)이 필요할 때 사용. Null 영역을 적절히 써서 불필요한 경로 계산을 차단. |
| Nav Link Proxy | 단절된 메시 연결 | CPU 중(링크 유효성 체크) / Mem 저 | 3순위 (특수 상황): 점프, 사다리, 낙하 등 일반 보행으로 불가능한 경로에만 배치. 과도한 배치는 경로 탐색 복잡도를 높임. |
소규모 레벨 기준
소규모 레벨에서는 모든 NavMesh를 미리 빌드해 두는 것이 훨씬 이득이므로, 실시간 계산이 필요한 시스템의 순위가 뒤로 밀립니다.
| 시스템 | 소규모 레벨에서의 역할 | 우선순위 변화 | 이유 |
|---|---|---|---|
| Nav Modifier | 지형 속성 및 정교한 AI 제어 | 1순위 | 좁은 공간일수록 AI가 장애물을 정교하게 피하거나 특정 경로를 선호하게 만드는 디테일한 설정이 중요해집니다. |
| Nav Link Proxy | 복층 구조 및 수직 이동 | 2순위 | 소규모 맵은 수직적 디자인(점프, 사다리)이 많은 경우가 많아, 끊어진 메시를 잇는 링크의 중요도가 높아집니다. |
| Nav Invoker | 실시간 생성 (거의 사용 안 함) | 3순위 | 맵 전체를 메모리에 올려도 무리가 없으므로, CPU를 소모하며 실시간으로 NavMesh를 생성할 이유가 사라집니다. |
Static NavMesh + Dynamic Obstacles (권장 전략) :
인보커를 사용하는 대신, 맵 전체에 NavMesh를 미리 빌드(Static)합니다.
대신 움직이는 문이나 상자 같은 액터에는 Nav Modifier를 사용하거나, 해당 액터의 충돌 설정에서 Can Ever Affect Navigation을 켜서 해당 부분만 실시간 업데이트되도록 합니다. 이를 통해 불필요한 전체 재빌드 부하를 eliminate(제거)합니다.
정교한 구역 분리 (Modifier 중심) : 맵이 좁으므로 AI가 끼이는 현상을 방지해야 합니다. Nav Modifier를 사용해 벽 근처나 좁은 틈새에 NavArea_Obstacle을 설정하여 AI가 벽에 비비지 않고 중앙으로 걷도록 유도합니다.
수직적 경로 최적화 (Link Proxy 중심) : 소규모 레벨의 재미 요소인 ‘지름길(점프 구간)‘을 Nav Link Proxy로 연결합니다. 맵 전체가 이미 빌드되어 있으므로 링크의 연결 안정성이 매우 높습니다.
소규모 레벨에서는 Navigation Invoker를 사용하지 않는 것이 오히려 최적화입니다. 인보커는 NavMesh를 ‘생성’하는 데 CPU를 쓰지만, Static NavMesh는 이미 생성된 데이터를 ‘조회’만 하기 때문입니다.
소규모 레벨에서는 시스템의 복잡도를 줄여 잠재적인 버그를 eliminate(제거)하고, 대신 풍부한 Nav Modifier와 Nav Link Proxy를 통해 AI의 행동 품질을 높이는 데 집중하는 것이 정석이라 합니다.
| 비교 항목 | 대규모 월드 (Open World) | 소규모 레벨 (Arena/Indoor) |
|---|---|---|
| 최우선 과제 | 메모리 점유율 및 로딩 시간 감소 | 경로의 정확도 및 AI의 반응성 |
| 핵심 시스템 | Navigation Invoker | Navigation Modifier |
| Generation 방식 | Dynamic (인보커 주변 실시간) | Static 또는 Dynamic Obstacles |
| CPU 관리 | 실시간 타일 생성 부하 관리 | 복잡한 경로 탐색(A*) 계산 관리 |
| 오류 관리 | NavMesh 미생성 구역 진입 방지 | 좁은 길 끼임 및 비효율적 경로 eliminate |
해당 옵션은 옵션은 멀티플레이어 환경에서 서버의 부하를 관리하기 위한 매우 중요한 최적화 설정이라고 합니다.
멀티플레이어 환경에서 ‘Seed(시드)’는 일반적으로 서버에 접속한 각 플레이어의 위치(PlayerController의 위치 또는 ViewTarget)를 의미합니다.
협동(Co-op) 게임 : 보통 플레이어들이 뭉쳐 다니므로 Maximum Distance from Seed를 비교적 타이트하게 잡아도 서버 부하가 낮습니다.
배틀로얄/오픈월드 : 플레이어가 광범위하게 퍼지므로, Seed 거리를 너무 크게 잡으면 서버 메모리가 부족해질 수 있습니다. 이 경우 NavMesh 타일의 크기(Tile Size)를 키우고 해상도를 낮추어 전체적인 계산량을 eliminate(제거)하는 전략을 병행해야 합니다.
이 옵션은 “플레이어가 있는 곳 근처의 인보커만 서버가 신경 쓰겠다”는 필터링 메커니즘으로 작동하여 멀티플레이어 서버 최적화에 기여합니다.
RVO(Reciprocal Velocity Obstacles)란 움직이는 객체들이 서로의 속도와 방향을 고려하여 충돌을 피하는 방법입니다. 쉽게 말해 동적 액터들이 서로를ㄹ 장애물로 인식하고 충돌을 피하는 알고리즘입니다.
경로 재계산 없이 속도와 방향만 조절하여 실시간 회피가 가능하므로 다수의 액터가 있을 때 효율적, 자연스러운 움직임을 제공하는 장점이 있습니다.
GetCharacterMovement()->bUseRVOAvoidance = true;
GetCharacterMovement()->AvoidanceConsiderationRadius = 200.0f;
GetCharacterMovement()->AvoidanceWeight = 0.5f;
GetCharacterMovement()->bUseRVOAvoidance = true;
RVO 회피 시스템의 활성화 여부를 결정하는 가장 기본적인 설정입니다.
GetCharacterMovement()->AvoidanceConsiderationRadius = 200.0f;
AI가 다른 오브젝트를 감지하고 회피를 시작하는 거리를 결정합니다.
GetCharacterMovement()->AvoidanceWeight = 0.5f;
해당 클래스를 상속받은 캐릭터의 회피 우선순위를 결정합니다.
시나리오 두 AI 집단이 있고 목표지점은 반대편 집단의 너머에 존재합니다.
길목은 Modifier를 사용해서 좁게 설정했습니다.
=== RVO 활성 화면
=== RVO 비활성 화면
Nav Link 자동 생성하기
5.5 이상 부터는 Nav Link를 자동생성해 주는 기능이 생겼다고 합니다.
=== Nav Link 자동 생성 체크

여기서 Generated라 써져 있는 것을 클릭하면 됩니다.
=== 생성 전

=== 생성 후

맵에 Nav Link(녹색 곡선)이 추가된 것을 확인할 수 있습니다.
이 상태로는 그냥 NavLink가 추가되었을 뿐 AI가 점프를 하는 것은 불가능합니다.
추가로 작업이 필요합니다.

=== Generation -> Nav Mesh Jump Down Config

=== 내부 설정

=== 클래스 집어넣기

=== 최종 설정 이미지

=== 테스트 화면
QA는 진행하지 못했습니다. 목표하던 학습량에 도달하지 못해 아쉽습니다. 내일은 QA를 우선적으로 진행하고 그 뒤 Unreal 강의 2챕터를 진행하고자 합니다.