C++ 게임 서버에서 Recast & Detour 라이브러리로 경로 찾기 구현하기

도입 문구


게임 서버에서 AI 캐릭터의 경로 찾기는 매우 중요한 요소입니다. 이 기사에서는 C++에서 Recast & Detour 라이브러리를 활용하여 효율적인 경로 찾기 시스템을 구현하는 방법을 다룹니다.

Recast & Detour 소개


Recast & Detour는 AI 경로 찾기 알고리즘을 구현하기 위한 오픈 소스 라이브러리입니다. Recast는 네비게이션 메시지를 생성하는 역할을 하며, Detour는 이 메시지를 사용해 실시간 경로를 계산합니다. 이 두 라이브러리는 AI 캐릭터가 장애물을 피하며 목표 지점으로 이동하는 데 필요한 핵심 기능을 제공합니다.

Recast


Recast는 3D 환경에서 네비게이션 메시지를 생성하는 라이브러리로, 맵의 장애물을 고려하여 AI가 이동할 수 있는 영역을 정의합니다. 이 메시지는 AI 캐릭터가 이동할 수 있는 경로를 계산하는 데 중요한 역할을 합니다.

Detour


Detour는 Recast가 생성한 네비게이션 메시지를 기반으로 경로를 계산하는 라이브러리입니다. 실시간으로 경로를 찾으며, 장애물 회피 기능도 지원합니다. Detour는 목표 지점까지의 최적 경로를 계산하고, 경로가 변경되거나 장애물이 나타났을 때 즉시 경로를 다시 계산합니다.

라이브러리 설치 및 설정


Recast & Detour를 C++ 프로젝트에 통합하려면 먼저 라이브러리를 다운로드하고 빌드해야 합니다. 이 과정에서는 필요한 의존성 파일을 설정하고, 프로젝트 환경에서 사용할 수 있도록 라이브러리를 컴파일하는 방법을 다룹니다.

1. 라이브러리 다운로드


Recast & Detour의 공식 GitHub 저장소에서 소스 코드를 다운로드할 수 있습니다. 아래의 명령어를 통해 Recast & Detour 소스 파일을 클론할 수 있습니다:

git clone https://github.com/recastnavigation/recastnavigation.git

2. 빌드 설정


Recast & Detour를 빌드하려면 CMake를 사용하여 빌드 환경을 설정합니다. 프로젝트 디렉토리 내에서 CMakeLists.txt 파일을 열고, Recast & Detour의 빌드 옵션을 지정합니다. 예를 들어, Visual Studio에서 빌드할 경우 아래의 명령어를 사용합니다:

cmake -G "Visual Studio 16 2019" .

3. 프로젝트 통합


빌드가 완료된 후, Recast & Detour의 헤더 파일과 라이브러리 파일을 C++ 프로젝트에 포함시켜야 합니다. #include 지시문을 사용하여 Recast와 Detour 관련 파일을 포함하고, 프로젝트의 링커 설정에서 해당 라이브러리를 연결합니다.

#include "Recast.h"
#include "DetourNavMesh.h"
#include "DetourNavMeshQuery.h"

이제 Recast & Detour가 프로젝트에 성공적으로 통합되었습니다. 이후에는 네비게이션 메시지를 생성하고, 경로 찾기 기능을 구현할 수 있습니다.

네비게이션 메시지 생성


Recast를 사용하여 AI가 이동할 수 있는 영역을 정의하는 네비게이션 메시지를 생성하는 과정은 경로 찾기의 첫 단계입니다. 이 메시지는 3D 환경에서 AI 캐릭터가 이동할 수 있는 공간을 나타내며, 장애물과 이동 불가능한 영역을 고려하여 생성됩니다.

1. 환경 설정


Recast를 사용하기 전에 먼저 3D 환경을 설정해야 합니다. 주로 .obj, .fbx 등의 형식으로 제공되는 3D 모델을 사용하여 환경을 정의합니다. 이 모델에는 지형 정보, 장애물 등이 포함되어 있어야 하며, Recast는 이를 바탕으로 네비게이션 메시지를 생성합니다.

2. 네비게이션 메시지 생성 함수


Recast의 핵심 함수는 rcBuildNavMesh입니다. 이 함수는 3D 환경에서 AI가 이동할 수 있는 네비게이션 메시지를 생성합니다. 생성된 메시지는 장애물을 피하고, 경로를 계산하는 데 사용됩니다. 아래는 네비게이션 메시지 생성을 위한 예시 코드입니다:

rcConfig cfg;
cfg.cs = 0.3f;  // 셀 크기
cfg.ch = 0.2f;  // 셀 높이
cfg.width = 512; // 그리드 너비
cfg.height = 512; // 그리드 높이

// 3D 환경을 로드하고 메시 생성
rcHeightfield* heightfield = rcAllocHeightfield();
if (!rcCreateHeightfield(context, *heightfield, cfg.width, cfg.height, &verts[0], numVerts, cfg.cs, cfg.ch))
{
    printf("Heightfield creation failed.\n");
    return;
}

// 네비게이션 메시지 생성
rcPolyMesh* polyMesh = rcAllocPolyMesh();
rcBuildPolyMesh(context, *heightfield, cfg, *polyMesh);

rcNavMesh* navMesh = rcAllocNavMesh();
if (!rcBuildNavMesh(context, *polyMesh, cfg, *navMesh))
{
    printf("NavMesh creation failed.\n");
    return;
}

3. 네비게이션 메시지 확인


생성된 네비게이션 메시지는 디버깅 도구를 사용하여 확인할 수 있습니다. Recast는 메시를 시각적으로 확인할 수 있는 기능을 제공하여, 생성된 경로와 장애물 회피가 제대로 이루어지는지 점검할 수 있습니다. 이 과정은 AI가 실제 환경에서 경로를 잘 계산하는지 확인하는 중요한 단계입니다.

Detour를 활용한 경로 찾기


Detour는 Recast가 생성한 네비게이션 메시지를 바탕으로 AI 캐릭터의 경로를 계산하는 라이브러리입니다. Detour는 실시간 경로 찾기와 장애물 회피 기능을 지원하며, 주어진 시작 지점과 목표 지점 간의 최적 경로를 찾아냅니다.

1. 네비게이션 메시지와 쿼리 객체


Detour를 사용하려면 먼저 Recast에서 생성된 네비게이션 메시지를 로드해야 합니다. 이후 dtNavMeshQuery 객체를 생성하여, 이 객체를 통해 경로를 계산합니다. dtNavMeshQuery는 주어진 네비게이션 메시지 내에서 경로를 검색하는 역할을 합니다.

// 네비게이션 메시지 로드
dtNavMesh* navMesh = ...;  // 이전에 생성된 navMesh

// 쿼리 객체 생성
dtNavMeshQuery* navQuery = new dtNavMeshQuery();
navQuery->init(navMesh, 1024);

2. 경로 찾기 함수


Detour의 핵심 함수는 findPath로, 시작 지점과 목표 지점 사이의 경로를 찾습니다. 이 함수는 네비게이션 메시 상에서 최적 경로를 계산하며, 장애물이나 다른 요소를 피하면서 가장 빠른 경로를 도출합니다.

// 시작점과 목표점
dtPolyRef startRef, endRef;
float startPos[3] = {startX, startY, startZ};  // 시작 위치
float endPos[3] = {endX, endY, endZ};  // 목표 위치

// 시작점과 목표점의 폴리곤 참조 얻기
navQuery->findNearestPoly(startPos, extents, filter, &startRef, nullptr);
navQuery->findNearestPoly(endPos, extents, filter, &endRef, nullptr);

// 경로 찾기
dtPolyRef path[100];
int pathCount = 0;
navQuery->findPath(startRef, endRef, startPos, endPos, filter, path, &pathCount, 100);

3. 경로 점과 경로 추적


경로가 계산되면, findPath 함수는 경로를 구성하는 폴리곤 참조 배열을 반환합니다. 이 배열을 기반으로 경로를 추적할 수 있으며, 각 폴리곤을 따라 AI 캐릭터를 이동시킬 수 있습니다.

// 경로 추적
for (int i = 0; i < pathCount; i++)
{
    // 각 폴리곤에 대해 이동해야 할 위치를 계산
    float nextPos[3];
    navQuery->getPolyCenter(path[i], nextPos);

    // AI 캐릭터를 nextPos로 이동시킴
    moveAICharacter(nextPos);
}

4. 장애물 회피와 경로 수정


Detour는 동적 장애물 처리와 경로 수정 기능도 지원합니다. 경로를 따라가다가 장애물이 나타나면, Detour는 자동으로 경로를 재계산하고, 새로운 최적 경로를 찾아냅니다. 이는 AI 캐릭터가 실시간으로 환경 변화에 적응할 수 있도록 도와줍니다. 경로를 추적하는 동안 계속해서 장애물 여부를 체크하고, 필요에 따라 findPath를 다시 호출하여 경로를 업데이트할 수 있습니다.

// 장애물이 감지되면 경로를 다시 계산
if (detectObstacle())
{
    navQuery->findPath(startRef, endRef, startPos, endPos, filter, path, &pathCount, 100);
}

Detour는 실시간 경로 찾기와 장애물 회피를 통해, 게임 서버에서 AI 캐릭터가 빠르고 정확하게 목표 지점에 도달할 수 있도록 지원합니다.

최적화 및 성능 개선


경로 찾기 시스템은 실시간으로 동작하는 만큼 성능이 매우 중요합니다. 특히 게임 서버에서는 수많은 AI 캐릭터가 동시에 경로를 찾을 수 있기 때문에, 경로 찾기 알고리즘의 최적화가 필요합니다. Recast & Detour는 기본적으로 효율적인 경로 찾기를 제공하지만, 여러 가지 최적화 기법을 통해 성능을 더욱 개선할 수 있습니다.

1. 네비게이션 메시의 크기 최적화


네비게이션 메시의 크기는 경로 찾기의 성능에 큰 영향을 미칩니다. 메시가 너무 크거나 복잡하면 경로 찾기 속도가 느려질 수 있습니다. 이를 최적화하기 위해, Recast는 “세분화 레벨”을 조정할 수 있는 옵션을 제공합니다. 이를 통해 메시의 세분화 정도를 조절하여, 필요 이상으로 복잡한 메시 생성을 방지할 수 있습니다.

// 세분화 레벨 조정
cfg.agentRadius = 0.5f;  // 캐릭터의 반경 설정
cfg.agentHeight = 2.0f;  // 캐릭터의 높이 설정
cfg.agentMaxSlope = 45.0f;  // 최대 기울기 설정
cfg.regionMinSize = 8;  // 지역의 최소 크기 설정
cfg.regionMergeSize = 20;  // 지역 병합 크기 설정

세분화 레벨을 적절히 설정하면, 경로 찾기 속도를 개선하면서도 충분한 정밀도를 유지할 수 있습니다.

2. 경로 캐싱 (Path Caching)


경로 캐싱은 이전에 계산한 경로를 저장하여, 같은 경로를 다시 찾을 필요 없이 빠르게 재사용하는 방법입니다. 예를 들어, 두 지점 간의 경로가 자주 요청되는 경우, 경로를 메모리에 저장해두고, 경로를 다시 계산할 필요 없이 캐시된 값을 사용할 수 있습니다.

// 경로 캐시 예시
std::map<std::pair<int, int>, std::vector<dtPolyRef>> pathCache;

// 경로를 캐시에서 찾기
auto pathIter = pathCache.find(std::make_pair(startIndex, endIndex));
if (pathIter != pathCache.end())
{
    // 캐시된 경로 사용
    useCachedPath(pathIter->second);
}
else
{
    // 경로 계산 후 캐시
    std::vector<dtPolyRef> newPath;
    navQuery->findPath(startRef, endRef, startPos, endPos, filter, newPath.data(), &pathCount, 100);
    pathCache[std::make_pair(startIndex, endIndex)] = newPath;
}

이 방법은 동일한 경로를 반복적으로 요청하는 상황에서 큰 성능 향상을 가져올 수 있습니다.

3. 경로 찾기 요청 분산 처리


서버에서 여러 AI 캐릭터의 경로 찾기를 동시에 처리하려면, 경로 찾기 요청을 분산 처리하는 방식도 성능 향상에 도움이 됩니다. 멀티스레딩을 사용하여 각 AI 캐릭터의 경로 찾기 작업을 별도의 스레드에서 처리할 수 있습니다. 이를 통해 서버가 동시에 여러 경로를 계산할 수 있게 되어, 전체 성능을 향상시킬 수 있습니다.

#include <thread>

// 경로 찾기를 별도 스레드에서 처리
void findPathInBackground(dtNavMeshQuery* navQuery, dtPolyRef startRef, dtPolyRef endRef, float* startPos, float* endPos)
{
    std::vector<dtPolyRef> path;
    int pathCount = 0;
    navQuery->findPath(startRef, endRef, startPos, endPos, nullptr, path.data(), &pathCount, 100);
}

// 스레드 생성
std::thread pathThread(findPathInBackground, navQuery, startRef, endRef, startPos, endPos);
pathThread.detach();

이 방법은 서버의 성능을 최적화하는 데 유용하며, 특히 여러 AI 캐릭터가 동시에 경로를 찾아야 하는 경우에 효과적입니다.

4. 경로 업데이트 간격 조정


AI 캐릭터가 실시간으로 경로를 추적하는 동안, 경로를 자주 업데이트하는 것은 성능에 부담을 줄 수 있습니다. 경로를 너무 자주 업데이트하지 않도록 간격을 조정하는 것이 중요합니다. 예를 들어, 일정 시간 간격으로 경로를 재계산하거나, 캐릭터가 경로를 벗어났을 때만 경로를 업데이트하도록 설정할 수 있습니다.

// 경로 업데이트 주기 설정
float lastUpdateTime = 0.0f;
float updateInterval = 1.0f; // 1초마다 경로 업데이트

if (currentTime - lastUpdateTime > updateInterval)
{
    // 경로 업데이트
    navQuery->findPath(startRef, endRef, startPos, endPos, filter, path, &pathCount, 100);
    lastUpdateTime = currentTime;
}

이렇게 하면 경로 계산 빈도를 줄여 성능을 개선할 수 있습니다.

5. 동적 장애물 회피 최적화


게임 환경에서 장애물은 동적으로 변화할 수 있기 때문에, 장애물 회피 기능도 효율적으로 처리해야 합니다. Detour는 장애물 회피를 위해 경로를 재계산하는 기능을 제공하지만, 장애물이 나타날 때마다 경로를 즉시 재계산하는 대신, 일정 시간 간격으로 경로를 수정하도록 설정하는 것이 성능 개선에 도움이 됩니다.

// 일정 간격마다 장애물 회피 경로 재계산
if (currentTime - lastRecalcTime > recalcInterval)
{
    if (detectObstacle())
    {
        navQuery->findPath(startRef, endRef, startPos, endPos, filter, path, &pathCount, 100);
    }
    lastRecalcTime = currentTime;
}

이 방법은 불필요한 경로 재계산을 줄여 서버의 부하를 낮출 수 있습니다.

디버깅 및 문제 해결


게임 서버에서 경로 찾기 시스템을 구현하는 과정에서 다양한 오류가 발생할 수 있습니다. Detour와 Recast를 사용할 때 흔히 겪을 수 있는 문제와 이를 해결할 수 있는 방법에 대해 설명합니다.

1. 경로 찾기 실패


경로 찾기가 실패하는 가장 일반적인 이유는 경로를 찾을 수 없을 정도로 시작 지점과 목표 지점이 너무 멀거나, 장애물로 완전히 막혀 있을 때입니다. 이를 해결하려면, 경로가 실패한 경우에 대한 처리를 추가하고, 문제가 발생한 지점을 로그로 기록하여 원인을 파악해야 합니다.

if (pathCount == 0)
{
    std::cerr << "경로 찾기 실패: 경로가 존재하지 않음" << std::endl;
    // 실패한 경로 정보 로그
    logError(startPos, endPos);
}

또한, 경로를 찾을 수 없는 경우 대체 경로를 제공하거나, 다른 행동을 취하는 로직을 구현하는 것도 하나의 방법입니다.

2. 경로 중단 문제


때때로 AI 캐릭터가 경로를 따라가던 중, 중간에 경로를 벗어나거나 정해진 목표 지점에 도달하지 못하는 경우가 발생할 수 있습니다. 이는 경로 추적 중 중간 폴리곤이 잘못 계산되었거나, 장애물로 인해 경로를 벗어날 때 발생할 수 있습니다. 이 문제를 해결하려면 경로 추적 중에 폴리곤의 유효성을 점검하고, 경로를 벗어난 경우 재계산을 시도해야 합니다.

// 경로 벗어났을 경우 경로 재계산
if (isOffPath(currentPos, path))
{
    std::cerr << "경로에서 벗어남: 경로 재계산 시도" << std::endl;
    navQuery->findPath(startRef, endRef, startPos, endPos, filter, path, &pathCount, 100);
}

경로 벗어남을 감지하는 로직을 추가하여, AI 캐릭터가 올바른 경로를 따라가도록 유도할 수 있습니다.

3. 경로 계산 지연


경로 찾기 계산이 너무 오래 걸려 게임 서버의 성능에 영향을 미칠 수 있습니다. 이 문제를 해결하기 위해, 경로 찾기 알고리즘의 효율성을 높이거나, 경로 찾기를 비동기적으로 처리하는 방법을 고려할 수 있습니다. 예를 들어, 긴 경로 계산은 별도의 스레드에서 처리하고, 그 동안 다른 작업을 계속할 수 있도록 할 수 있습니다.

// 비동기 경로 찾기 (예: 멀티스레딩)
void findPathAsync(dtNavMeshQuery* navQuery, dtPolyRef startRef, dtPolyRef endRef, float* startPos, float* endPos)
{
    std::thread pathThread([=]()
    {
        std::vector<dtPolyRef> path;
        int pathCount = 0;
        navQuery->findPath(startRef, endRef, startPos, endPos, nullptr, path.data(), &pathCount, 100);
        // 경로 처리 후 AI 캐릭터 이동
        processPath(path);
    });
    pathThread.detach();
}

이렇게 하면 경로 찾기가 서버의 다른 처리 흐름을 차단하지 않도록 할 수 있습니다.

4. 네비게이션 메시 로딩 문제


Recast의 네비게이션 메시가 올바르게 로딩되지 않거나, 잘못된 크기의 네비게이션 메시가 로드될 경우 경로 찾기 오류가 발생할 수 있습니다. 네비게이션 메시가 제대로 로드되었는지, 크기가 적절한지 확인하는 것이 중요합니다.

if (!navMesh)
{
    std::cerr << "네비게이션 메시 로딩 실패" << std::endl;
    return;
}

또한, 네비게이션 메시를 생성할 때 적절한 에이전트 크기와 환경에 맞게 설정을 조정하는 것이 중요합니다. 너무 큰 네비게이션 메시나 너무 작은 에이전트 크기는 경로 찾기 오류를 유발할 수 있습니다.

5. 장애물 처리 문제


동적 장애물이 경로 계산에 영향을 미칠 수 있습니다. 장애물이 움직이거나 등장할 때, 경로 찾기가 이를 실시간으로 반영하지 않으면 경로가 잘못될 수 있습니다. 이 문제를 해결하려면, 장애물 변화에 대응하여 경로를 동적으로 수정하는 로직을 추가해야 합니다.

// 장애물 감지 및 경로 재계산
if (detectDynamicObstacle())
{
    navQuery->findPath(startRef, endRef, startPos, endPos, filter, path, &pathCount, 100);
}

이렇게 실시간으로 장애물을 감지하고, 필요한 경우 경로를 재계산하는 방법을 구현하면 경로 추적의 정확성을 높일 수 있습니다.

6. 디버깅 도구 사용


Detour와 Recast는 디버깅을 위한 도구와 기능을 제공합니다. 예를 들어, 네비게이션 메시의 구조를 시각적으로 확인하거나, 경로 찾기 과정을 로그로 출력하여 문제를 쉽게 추적할 수 있습니다.

// 디버그 메시지 출력
navQuery->drawNavMeshPolyRefs(startPos, endPos);

디버그 정보를 활용하면 경로 찾기 과정에서 발생하는 문제를 빠르게 식별하고 해결할 수 있습니다.

Recast & Detour의 응용 사례


Recast & Detour는 단순히 경로 찾기뿐만 아니라 다양한 방식으로 활용될 수 있습니다. 게임 서버에서 실제로 적용할 수 있는 몇 가지 응용 사례를 소개합니다. 이러한 응용은 게임의 AI 행동을 더욱 풍부하고 자연스럽게 만들어 줄 수 있습니다.

1. 실시간 장애물 회피 시스템


게임에서 AI 캐릭터는 실시간으로 움직이는 장애물들을 피해야 할 때가 많습니다. Recast & Detour의 경로 찾기 시스템은 장애물을 피하는 경로를 찾을 수 있지만, 추가적인 실시간 장애물 회피 기능을 구현할 수도 있습니다. 예를 들어, 캐릭터가 목표 지점으로 가는 도중에 장애물이 나타나면, 이를 인식하고 장애물을 피할 수 있는 경로를 자동으로 다시 계산하도록 할 수 있습니다.

// 장애물 회피 경로 찾기
if (isObstacleDetected(currentPos))
{
    std::vector<dtPolyRef> newPath;
    int pathCount = 0;
    navQuery->findPath(startRef, endRef, startPos, endPos, filter, newPath.data(), &pathCount, 100);
    updateCharacterPath(newPath);  // 새로운 경로로 이동
}

이 방식은 AI 캐릭터가 동적인 장애물에 대응할 수 있도록 돕습니다.

2. 복잡한 경로 네비게이션


일부 게임에서는 단순한 A->B 경로 찾기가 아닌, 복잡한 경로를 찾아야 할 때가 많습니다. 예를 들어, 여러 목표 지점에 대한 최적 경로를 찾아야 하는 경우가 있을 수 있습니다. Recast & Detour의 경로 찾기 기능을 활용하여, AI가 여러 목적지로 최단 경로를 선택하거나, 여러 지점을 경유하는 경로를 찾을 수 있습니다.

// 여러 목표 지점 경유
std::vector<dtPolyRef> multiGoalPath;
int pathCount = 0;
navQuery->findPath(startRef, goal1Ref, startPos, goal1Pos, filter, multiGoalPath.data(), &pathCount, 100);
navQuery->findPath(goal1Ref, goal2Ref, goal1Pos, goal2Pos, filter, multiGoalPath.data(), &pathCount, 100);

이와 같이 여러 경유 지점에 대해 경로를 찾음으로써, AI가 복잡한 경로를 탐색할 수 있습니다.

3. 팀워크 기반 AI 경로 찾기


팀 기반의 게임에서는 여러 AI 캐릭터가 협력하여 목표를 향해 나아가야 할 때가 많습니다. 이 경우 각 AI 캐릭터가 독립적으로 경로를 찾고, 이를 서로 협력하여 효율적인 경로를 생성해야 합니다. Recast & Detour의 경로 찾기 시스템을 활용하여, 각 캐릭터가 다른 캐릭터의 위치를 고려하여 경로를 변경하는 방식으로 협력적 경로 탐색을 구현할 수 있습니다.

// AI 팀워크를 고려한 경로 변경
for (auto& ai : aiTeam)
{
    std::vector<dtPolyRef> path;
    navQuery->findPath(ai.startRef, ai.endRef, ai.startPos, ai.endPos, filter, path.data(), &pathCount, 100);
    adjustPathForTeam(ai, path);
}

이 방식은 AI가 상호작용하며 목표를 향해 나아가도록 하는 데 유용합니다.

4. 다중 경로 찾기 및 리소스 최적화


게임 서버에서는 다수의 AI 캐릭터가 경로를 찾아야 할 때, 경로 찾기 요청이 집중되면 성능 문제가 발생할 수 있습니다. 이를 해결하기 위해, Recast & Detour에서는 여러 경로를 동시에 찾을 수 있는 기능을 활용하여 리소스를 최적화할 수 있습니다. 여러 AI 캐릭터의 경로를 동시에 계산하거나, 경로 계산의 우선순위를 두어 서버 부하를 분산시킬 수 있습니다.

// 다중 경로 계산 및 리소스 분산
std::vector<std::thread> pathThreads;
for (auto& ai : aiTeam)
{
    pathThreads.push_back(std::thread([=]()
    {
        std::vector<dtPolyRef> path;
        int pathCount = 0;
        navQuery->findPath(ai.startRef, ai.endRef, ai.startPos, ai.endPos, filter, path.data(), &pathCount, 100);
        processPath(ai, path);
    }));
}

for (auto& thread : pathThreads)
{
    thread.join();
}

멀티스레딩을 활용한 다중 경로 찾기 방식은 게임 서버의 처리 성능을 극대화하는 데 유리합니다.

5. 복잡한 지형에서의 경로 찾기


복잡한 지형에서 AI가 경로를 찾는 것은 매우 어려운 작업일 수 있습니다. Recast는 비정형적인 지형을 처리할 수 있는 능력을 갖추고 있으며, 이를 활용해 AI가 복잡한 지형을 효과적으로 탐색하도록 할 수 있습니다. 특히, 고산지대나 동적인 환경에서 경로를 찾아야 할 때 유용합니다.

// 복잡한 지형에서 경로 찾기
if (isTerrainComplex(terrainData))
{
    std::vector<dtPolyRef> path;
    int pathCount = 0;
    navQuery->findPath(startRef, endRef, startPos, endPos, filter, path.data(), &pathCount, 100);
    handleComplexTerrainPath(path);
}

Recast & Detour의 강력한 네비게이션 메시 기능을 통해, 복잡한 지형에서도 효과적으로 경로를 탐색할 수 있습니다.

성능 최적화


Recast & Detour를 게임 서버에 통합할 때, 경로 찾기 시스템이 성능에 미치는 영향을 최소화하는 것이 중요합니다. 게임 서버는 실시간으로 다수의 AI 경로 계산을 처리해야 하므로, 성능 최적화가 필수적입니다. 여기에서는 경로 찾기 성능을 최적화할 수 있는 몇 가지 방법을 소개합니다.

1. 경로 찾기 캐시 활용


경로 찾기 계산은 많은 연산을 요구하기 때문에, 동일한 경로를 반복해서 계산하는 것은 비효율적입니다. 경로가 이미 계산되었을 경우 이를 캐시하여 재사용하는 방식으로 성능을 최적화할 수 있습니다. 예를 들어, 이미 계산된 경로를 저장해두고, 동일한 경로 요청이 들어오면 캐시된 경로를 반환하도록 할 수 있습니다.

// 경로 캐시 시스템
std::unordered_map<std::string, std::vector<dtPolyRef>> pathCache;

std::vector<dtPolyRef> findPathWithCache(dtNavMeshQuery* navQuery, dtPolyRef startRef, dtPolyRef endRef, const float* startPos, const float* endPos)
{
    std::string cacheKey = generateCacheKey(startPos, endPos);
    if (pathCache.find(cacheKey) != pathCache.end())
    {
        return pathCache[cacheKey];  // 캐시된 경로 반환
    }

    std::vector<dtPolyRef> path;
    int pathCount = 0;
    navQuery->findPath(startRef, endRef, startPos, endPos, nullptr, path.data(), &pathCount, 100);

    pathCache[cacheKey] = path;  // 새로운 경로 캐시 저장
    return path;
}

이 방식은 동일한 경로를 반복적으로 계산하지 않아 성능을 크게 향상시킬 수 있습니다.

2. 경로 탐색 범위 제한


경로 찾기 시스템에서 불필요한 부분까지 경로 탐색을 하게 되면 성능이 저하될 수 있습니다. 따라서, 경로 탐색 범위를 제한하는 방법을 사용할 수 있습니다. 예를 들어, 시작 지점과 목표 지점 사이의 거리나, 경로 탐색 깊이를 설정하여 탐색 범위를 제한하는 방식입니다.

// 경로 탐색 범위 제한
const float MAX_PATH_DISTANCE = 1000.0f;  // 최대 경로 탐색 거리

if (calculateDistance(startPos, endPos) > MAX_PATH_DISTANCE)
{
    std::cerr << "경로 탐색 범위 초과" << std::endl;
    return {};  // 범위를 초과하면 경로 계산 안 함
}

이 방법은 불필요한 경로 탐색을 방지하고, 서버 자원을 절약할 수 있습니다.

3. 멀티스레딩을 통한 병렬 처리


게임 서버에서 동시에 다수의 경로 찾기를 처리해야 하는 경우, 멀티스레딩을 활용하여 경로 찾기 작업을 병렬 처리할 수 있습니다. 이를 통해 CPU 자원을 효율적으로 활용하고, 경로 찾기 시간을 단축할 수 있습니다.

// 멀티스레딩을 활용한 경로 찾기
std::vector<std::thread> pathThreads;

for (auto& ai : aiTeam)
{
    pathThreads.push_back(std::thread([=]()
    {
        std::vector<dtPolyRef> path;
        int pathCount = 0;
        navQuery->findPath(ai.startRef, ai.endRef, ai.startPos, ai.endPos, nullptr, path.data(), &pathCount, 100);
        processPath(ai, path);
    }));
}

for (auto& thread : pathThreads)
{
    thread.join();  // 모든 스레드가 완료될 때까지 대기
}

멀티스레딩을 통해 경로 찾기를 병렬로 처리하면 여러 AI 캐릭터의 경로를 동시에 계산할 수 있어 성능이 크게 향상됩니다.

4. 경로 최적화 기법 적용


경로 찾기 결과가 나왔을 때, 해당 경로를 최적화하는 방법을 적용하여, 불필요한 경로를 제거하거나 경로의 길이를 줄일 수 있습니다. 예를 들어, 경로상의 불필요한 점들을 제거하거나, 경로 상에 장애물이 없는 구간을 이어서 최적화할 수 있습니다.

// 경로 최적화
void optimizePath(std::vector<dtPolyRef>& path)
{
    for (size_t i = 0; i < path.size() - 2; ++i)
    {
        if (isPathSegmentClear(path[i], path[i + 2]))  // 경로 구간 최적화
        {
            path.erase(path.begin() + i + 1);  // 불필요한 중간 점 제거
        }
    }
}

이 방식은 계산된 경로를 최적화하여 경로 길이를 최소화하고, AI 캐릭터가 더 빠르게 목적지에 도달할 수 있게 합니다.

5. 경로 찾기 요청 우선순위 설정


모든 경로 찾기 요청이 동일한 우선순위를 가지지 않는 경우, 우선순위를 설정하여 중요한 요청을 먼저 처리하도록 할 수 있습니다. 예를 들어, 플레이어의 AI 캐릭터에 대한 경로를 더 높은 우선순위로 설정하고, NPC 캐릭터의 경로는 낮은 우선순위로 설정하여 자원을 효율적으로 사용할 수 있습니다.

// 경로 찾기 우선순위 설정
void processPathRequest(int priority, dtNavMeshQuery* navQuery, dtPolyRef startRef, dtPolyRef endRef)
{
    if (priority == HIGH_PRIORITY)
    {
        // 중요한 요청 우선 처리
        navQuery->findPath(startRef, endRef, startPos, endPos, nullptr, path.data(), &pathCount, 100);
    }
    else
    {
        // 낮은 우선순위 요청
        std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 잠시 대기
        navQuery->findPath(startRef, endRef, startPos, endPos, nullptr, path.data(), &pathCount, 100);
    }
}

우선순위를 설정함으로써 게임 서버의 부하를 효율적으로 분산시킬 수 있습니다.

요약


본 기사에서는 C++ 게임 서버에서 Recast & Detour 라이브러리를 활용해 경로 찾기를 구현하는 방법과 성능 최적화 기법을 다뤘습니다. Recast & Detour는 고성능 경로 찾기 시스템을 제공하며, 이를 게임 서버에 통합함으로써 효율적인 AI 경로 계산을 구현할 수 있습니다. 경로 캐시, 범위 제한, 멀티스레딩, 경로 최적화, 우선순위 설정 등을 통해 성능을 극대화할 수 있는 방법을 제시했습니다. 이러한 최적화 기법을 적용하면 게임 서버에서의 경로 찾기 효율성을 크게 향상시킬 수 있습니다.