← 이성현 포트폴리오
Code Dungeon 전투 장면 — 아누비스 마법진, 플레이어 공격, 할리퀸 수리검
C++ · DirectX 12

Code Dungeon

탐색과 전투가 섞인 3D 던전 액션 게임. 던전을 이동하며 몬스터 패턴을 익히고 보스를 공략합니다.

기간
2024.01 – 2024.07 · 졸업작품
인원
3명 — 팀장(프레임워크 · 애니메이션 시스템 · 서버), 팀원(셰이더 · 파티클 · UI), 이성현
환경
C++ · DirectX 12 자체 엔진 · ImGui · ImGuizmo
내 역할
네비게이션 메시 · A* · 몬스터 AI 4종 · 플레이어 로직 · 편집 툴 2종

AI 사용 — 네비게이션 메시 원리를 공부할 때 참고했습니다. A*는 기본 구현을 직접 했고, 이후 잦은 수정에 AI 도움을 받았습니다.

01주요 구현과 역할

  • 네비게이션 메시 — Cell · Navigation · Region · Stage를 직접 구현. 이동 검사, 점프 · 낙하, 경계 위치 보정
  • 툴과 엔진의 연결 — 툴에서 꼭짓점을 스냅해 저장하면, 엔진이 불러올 때 셀끼리 이웃으로 연결되도록 맞춤
  • A* 길찾기 — 셀 그래프 위 경로 탐색을 프레임마다 한 단계씩 진행하고, 가시선 검사로 경로 직선화
  • 몬스터 AI 4종 — 미라 · 미노타우르스 · 할리퀸 · 아누비스의 상태 전환 조건
  • 플레이어 로직 — 콤보 · 스킬 3종 · 구르기 · 넉백과 기상
  • ImGui 편집 툴 2종 — 네비게이션 메시 편집, 모델 배치(레벨 구성)
Code Dungeon 발표 영상 (4:03부터) · YouTube
팀원 작업팀장 — 프레임워크, 애니메이션 시스템(애니메이션 컨트롤러 프레임워크 · 루트 모션 재생), 서버(IOCP · Protobuf). 팀원 — 셰이더, 컴퓨트 셰이더 파티클, UI. 디퍼드 렌더링 · 멀티 카메라 · 셰도우맵 · 외곽선 렌더링 · 메모리 풀도 제 작업이 아닙니다. 저는 팀장이 만든 애니메이션 컨트롤러 프레임워크 위에서, 몬스터 4종과 플레이어의 상태 전환 조건(거리 · 피격 횟수 · 타이머 · 입력)을 작성했습니다.

02툴에서 게임까지

네비게이션 메시는 툴에서 찍어 파일로 저장하면 엔진이 읽어 들이고, 게임에서는 캐릭터마다 나눠 사용합니다. 툴에서 게임까지 단계별로 하는 일은 아래 그림과 같습니다.

TOOL · NAVIGATIONVIEW 픽킹으로 셀 찍기 맵 모델 위를 3번 픽킹 기존 꼭짓점 1.5 이내면 그 좌표로 스냅 UCell 생성 시 꼭짓점을 같은 회전 순서로 정렬 (ResortPoints) FILE 셀 단위 덤프 Navigation/<Region 이름> 셀 개수, 이어서 셀마다 색 · 꼭짓점 · 법선 · 변 이웃 · 번호 · 점프 여부 ENGINE · LOAD UNavigation::Load 셀마다 UCell을 새로 만들며 변 · 법선 다시 계산 좌표가 정확히 같은 꼭짓점끼리 이웃 연결 (ReadyNeighbor) CLIENT 캐릭터별 복제 플레이어 · 몬스터 · 수리검이 각자 Navigation을 복제 셀 데이터는 공유, 현재 셀만 각자 가짐 저장 읽기 복제 툴 좌표 보정 TOOL · NAVIGATIONVIEW 픽킹으로 셀 찍기 맵 모델 위를 3번 픽킹해 셀 하나 기존 꼭짓점 1.5 이내면 그 좌표로 스냅 꼭짓점 순서 정렬 (ResortPoints) 저장 FILE · Navigation/<Region 이름> 셀 단위 덤프 셀 개수, 이어서 셀마다 색 · 꼭짓점 · 법선 · 변 · 이웃 · 번호 · 점프 여부 읽기 ENGINE · UNavigation::Load 읽으면서 다시 계산 셀마다 UCell 생성, 변 · 법선 재계산 좌표가 정확히 같은 꼭짓점끼리 이웃 연결 (ReadyNeighbor) 복제 CLIENT 캐릭터별 복제 플레이어 · 몬스터 · 수리검이 각자 복제 셀 데이터는 공유, 현재 셀만 각자 가짐 툴 좌표 보정
편집 로직은 엔진의 URegion · UStage 안 _EDIT_NAVI 빌드 플래그 블록에 있고, Tool 프로젝트의 NavigationView가 이를 호출합니다.

툴 좌표 보정 — 엔진은 두 셀의 꼭짓점 좌표가 정확히 같아야 두 셀을 이웃으로 연결합니다. 그런데 마우스로 찍은 점은 조금씩 어긋나기 때문에, 툴에서 기존 꼭짓점 1.5 안을 찍으면 그 꼭짓점 좌표를 그대로 사용하게 했습니다.

04이동 · 카메라

  • 점프 가능 셀 — 셀에 점프 가능 토글을 두고, 그 셀에서 경계를 넘으면 FindCell로 아래 셀을 찾아 낙하를 시작합니다. y가 셀 높이보다 낮아지면 착지로 보고 위치를 맞춥니다. 점프 · 낙하 중이 아닐 때는 셀 높이를 따라 움직입니다.
  • 경계 위치 보정 — 현재 셀 밖으로 나가면 셀의 세 변 중 가장 가까운 변 위의 점으로 XZ만 옮기고 높이는 유지합니다. 그 점이 꼭짓점이면 그 꼭짓점을 공유하는 이웃 셀로 현재 셀을 바꿉니다. 이 보정은 몬스터와 카메라에 사용합니다.
  • 카메라 — 자기 Navigation과 시작 셀을 따로 둡니다. 플레이어와 거리가 30 이내일 때 이동에 실패하면 높이는 그대로 두고 XZ만 경계로 보정하고, 30을 넘으면 플레이어 위치로 되돌린 뒤 그 자리의 셀을 다시 찾습니다.
점프 가능 셀에서 낙하
벽을 따라 미끄러지는 이동 — 벽에 붙어도 카메라가 벽 안으로 들어가지 않음

05몬스터 4종

팀장이 만든 애니메이션 컨트롤러 프레임워크가 상태에 맞는 애니메이션을 재생하고 루트 모션을 처리합니다. 저는 그 위에서 몬스터마다 상태를 바꾸는 조건(거리 · 피격 횟수 · 타이머)을 작성했습니다.

  • 감지 · 해제 거리 분리 — 플레이어가 감지 거리 안으로 들어오면 추적을 시작하고, 그보다 먼 해제 거리 밖으로 나가야 추적을 멈춥니다. 경계 근처에서 추적이 켜졌다 꺼졌다 반복하지 않도록 두 거리를 나눴습니다.
  • 추격 이동 — A* 경로로는 다음에 갈 지점만 정하고, 실제 이동 속도는 애니메이션 상태마다 정해 둔 값을 사용합니다.
  • 배회 · 복귀 — 플레이어를 찾지 못하면 미라 · 미노타우르스 · 할리퀸은 5초마다 주변 셀 하나를 무작위로 골라 그쪽으로 경로를 찾습니다. 아누비스는 원래 자리로 돌아갑니다.
몬스터감지 / 해제패턴
미라50 / 80누운 형태 · 선 형태 2종. 범위에 들어오면 관이 열리며 일어나 도발 후 추적. 손 휘두르기 2종, 맞으면 짧은 경직. 멀어지면 주변 이웃 셀을 골라 배회
미노타우르스100 / 120발차기로 멀리 넉백. 플레이어가 쓰러져 있으면 물러났다가 달려와 더 강한 발차기(아래 상태도의 미노타우르스 탭). 보스라 세 번째 피격까지만 경직하고, 이후 5초는 맞아도 멈추지 않고 공격(데미지는 그대로 받음)
할리퀸80 / 150전용 방에서 전투. 탐지 시 도발로 시작. 멀면 전방 3방향 수리검, 중간이면 점프 돌진, 가까우면 백스텝 · 회축 · 덤블링 발차기. 수리검마다 네비게이션 메시를 달아 기둥 · 벽을 뚫지 않음
아누비스70 / 80특정 위치를 지킴. 일정 시간마다 8초 보호막(그동안 피해 없음, 근접 시 넉백). 중거리(15–40)에서 마법진, 근접 시 밀쳐내기. 계단 때문에 높이 차가 있으면 마법진 대신 다가감. 플레이어가 죽거나 멀어지면 원래 자리로 복귀

상태 이름은 코드의 트리거 이름입니다. 전환 조건은 제가 작성했고, 애니메이션 재생 프레임워크(컨트롤러 · 루트 모션)는 팀장이 만들었습니다.

대기 처음 감지하기 전 감지 거리 50 이내 · 처음 깨어남 WAKEUP · openLaying / openStanding 곧바로 도발 모드 도발 TAUNT taunt 재생되면 공격 모드 추격 WALKF 근접 공격 ATTACK01 ↔ 02 거리 10 이내면 공격, 멀면 추격 공격은 직전과 다른 쪽으로 번갈아 해제 거리 80 이상 또는 플레이어 사망 배회 IDLE 또는 WALKF (무작위) WALKF면 5초마다 주변 셀로 경로 다시 50 이내면 깨어남 없이 도발 피격 · 사망 — 어느 상태에서나 맞을 때마다 gotHit 재생 (횟수 제한 없음) 체력 0이면 DEAD
미라 상태 전환 조건. 전환은 CMummyAnimController.cpp, 감지 · 해제 거리와 배회 경로 주기는 CMummy.cpp에 있습니다.
배회 WALK 감지 거리 100 이내 공격 모드 추격 WALK2 근접 공격 ATTACK ↔ KICK_1 거리 10 이내면 공격, 멀면 추격 공격 종류는 3초마다 교대 해제 거리 120 이상 플레이어가 넉백으로 쓰러진 지 1.5초 초과 돌진 모드 후퇴 2초 달리기 목표 근처까지 강한 발차기 KICK_2 · 1초 쓰러진 순간의 플레이어 위치를 목표로 고정 1초 뒤 공격 모드로 피격 — 어느 상태에서나 세 번째 피격까지 hit_1 경직 그 뒤 5초 동안 피격 동작 없음 (데미지는 그대로 들어감)
미노타우르스 상태 전환 조건. 플레이어가 죽어도 배회로 돌아가고, 체력이 0이 되면 어느 상태에서든 사망 상태로 바뀝니다.
배회 IDLE · WALK 중 2초마다 무작위 감지 거리 80 이내 도발 TAUNT Idle break 재생되면 공격 모드 — 서 있을 때 50 초과 ATTACK1 · 수리검 3방향 ×2 40–50 WALK · 다가감 10–40 JUMP · 점프 돌진 10 미만 ATTACK2 / DODGE · 무작위 해제 거리 150 이상 · 플레이어 사망 웅크림 — CROUCH Jump Forward 동작 80%에서 넘어옴 10 미만 ATTACK3 40–50 JUMP · 다시 점프 돌진 10–40 50 초과 ATTACK4 · 수리검 6개 동작 80%에서 WALK로 피격 · 사망 — 어느 상태에서나 세 번째 피격까지 Get Hit 경직 그 뒤 HIT_COOLDOWN_DURATION 동안 피격 동작 없음 체력 0이면 DEATH
할리퀸 상태 전환 조건. 전환은 CHarlequinnAnimController.cpp, 감지 · 해제 거리는 CHarlequinn.cpp에 있고, 피격 뒤 쉬는 시간은 HIT_COOLDOWN_DURATION 상수로 둡니다.
경계 GUARDIDLE · 원래 자리 · 원래 방향 감지 거리 70 이내 전투 자세 TOATTACKIDLE 재생되면 공격 모드 공격 모드 — ATTACKIDLE ATTACK3 · 보호막 — 먼저 확인 꺼진 채 3초 지나면 · 8초 동안 피해 없음 보호막 차례가 아니면 거리로 40 초과 WALK · 다가감 15–40 CAST · 마법진 높이 차 0.2 넘으면 WALK 15 미만 ATTACK1 / ATTACK4 · 무작위 해제 거리 80 이상 또는 플레이어 사망 복귀 원래 자리로 WALK 경계 자세로 TOGUARDIDLE 원래 자리 2 이내면 경계 자세로 걷는 중 다시 감지되면 곧바로 공격 모드 동작 90%에서 GUARDIDLE로 피격 · 사망 — 어느 상태에서나 세 번째 피격까지 Hit Reaction 경직 횟수를 되돌리는 코드가 없어 그 뒤로는 경직 없음 보호막 중에는 피해 없음 · 체력 0이면 DEATH
아누비스 상태 전환 조건. 전환은 CAnubisAnimController.cpp, 감지 · 해제 거리, 보호막 8초, 원래 자리로 가는 경로는 CAnubis.cpp에 있습니다.
미노타우르스
미노타우르스
넉백으로 쓰러진 플레이어에게 달려오는 미노타우르스
미노타우르스 — 쓰러진 플레이어에게 돌진
할리퀸 수리검 공격
할리퀸 — 수리검
아누비스 마법진 공격
아누비스 — 마법진

06플레이어

  • 콤보 — 좌클릭 · 우클릭 각각 3단계. 앞 단계가 재생 중일 때 다시 눌러야 다음 단계로 이어지고, 아니면 1단계부터 시작합니다.
  • 스킬 3종(Q · 1 · 2) — 각각 쿨타임이 있고, 다른 스킬을 시전하는 중에는 사용할 수 없습니다.
  • 점프 제한 — 구르기 · 피격 · 기상 · 콤보 · 스킬 동작 중에는 점프 입력을 받지 않습니다.
  • 구르기 — 구르기 · 점프 · 기상 중에는 피격 콜라이더 크기를 0으로 줄이고, 구르다 벽에 닿으면 직전 위치로 되돌립니다.
  • 넉백 · 기상 — 넉백 공격에 맞으면 몬스터가 보는 방향으로 플레이어를 돌린 뒤, 넉백 애니메이션의 루트 모션으로 약 1.75초 동안 밀어냅니다. 2초가 지나면 Space로 일어나고, 기상 중에는 피격을 무시합니다. 아누비스 보호막에 밀릴 때는 아누비스가 보는 방향의 반대로 돌립니다.
대기 IDLE 이동 WALK* · Shift면 RUN* WASD 8방향으로 이동, 입력이 모두 없으면 대기 입력 — 대기 · 이동 중에 Space JUMP · 점프 · 낙하하는 동안 공격 · 뒤로 이동 입력이 없을 때만 구르기 · 피격 · 기상 · 콤보 · 스킬 중엔 무시 C ROLL_F · B · L · R 누른 방향으로, 방향이 없으면 앞 좌클릭 WATTACK1 → 2 → 3 앞 단계 재생 중에 다시 눌러야 다음 단계 우클릭 SATTACK1 → 2 → 3 좌클릭과 같은 방식 Q · 1 · 2 RATTACK · COMBO5 · COMBO9 쿨타임이 끝났고 다른 스킬이 시전 중이 아닐 때 반응 — 어느 상태에서나 피격 HIT_BACK 넉백이 아닌 공격에 맞으면 넉백 GOTKICKED → RISE1 → 대기 넉백 공격에 맞으면 쓰러짐(dead01) 쓰러진 지 2초 뒤 Space로 일어남 체력 0 DEAD → IDLE 3.5초 뒤 G로 시작 지점에서 부활 체력 2500으로
플레이어 상태 전환 조건(CUserWarriorAnimController.cpp, 넉백 판정은 CWarriorPlayer.cpp 충돌 처리). 상태 이름은 코드의 트리거 이름이고, 시간은 초 단위로 바꿔 적었습니다. 전환 조건은 제가 작성했고, 애니메이션 재생 프레임워크는 팀장이 만들었습니다.
플레이어 콤보 공격
콤보 공격
플레이어 구르기
구르기

07A* 길찾기

네비게이션 메시의 셀을 노드로 삼아 몬스터가 플레이어까지의 경로를 찾습니다.

  • 비용 — 노드에 F = g + h를 두고 우선순위 큐로 정렬합니다. 간선 비용 g와 휴리스틱 h 모두 셀 중심 사이의 3D 맨해튼 거리(|dx| + |dy| + |dz|)를 사용합니다. 노드마다 계산하는 값이라 제곱근 없이 가벼운 쪽을 골랐습니다. 대신 실제 직선 거리 기준의 최단 경로와는 다를 수 있어, 찾은 경로는 가시선 검사로 직선화해 보완합니다.
  • 프레임 분할 — 탐색 상태를 구조체에 담아 두고, 한 프레임에 노드 하나씩 탐색을 이어 갑니다.
  • 도착 판정 — 몬스터의 현재 셀과 플레이어의 현재 셀은 같은 셀 컨테이너의 원소라 포인터로 비교합니다.
  • 재탐색 — 플레이어를 쫓는 몬스터는 1초마다(아누비스는 0.5초) 경로를 다시 찾습니다.
  • 직선화 — 메시 위에서 2.0 간격으로 가시선 검사를 해 직선으로 갈 수 있는 중간 점은 건너뛰어 지그재그 움직임을 줄였습니다.
SE ① 셀 중심 연결 A* 결과 그대로면 지그재그 S ② 2.0 간격 검사 실선 통과 점선은 메시 밖 ✕ SE ③ 직선화 결과 검사에 실패하기 직전 점만 남김
개념도. OptimizePath는 마지막으로 남긴 점에서 다음 점까지 2.0 간격으로 셀 안인지 검사하고, 통과하면 그 사이 점을 생략합니다.
if (GetTimeAccumulator() >= 1.0) {                     // 1초마다 새로 시작
    m_PathFindingState = spNavigation->StartPathFinding(
        CurrentMobPos, CurrentPlayerPos, CurrentMobCell, CurrentPlayerCell);
    m_isPathFinding = true;
    SetTimeAccumulator(0.0);
}
if (m_isPathFinding) {
    if (spNavigation->StepPathFinding(m_PathFindingState)) {  // 프레임당 노드 하나
        m_isPathFinding = false;
        if (m_PathFindingState.pathFound) {
            m_AstarPath = spNavigation->OptimizePath(          // 가시선으로 직선화
                m_PathFindingState.path, CurrentMobPos, CurrentPlayerPos);
            m_currentPathIndex = 0;
        }
    }
}
CMinotaur.cpp — 플레이어를 찾았을 때의 경로 갱신 (돌진 분기 · 지역 변수 선언 줄임)CMinotaur.cpp

08편집 툴 2종

편집 로직은 엔진의 Region · Stage에 빌드 플래그(_EDIT_NAVI)로 넣고, Tool 프로젝트의 NavigationView가 이를 호출합니다. 모델 배치(레벨) 툴도 같은 Tool 프로젝트에 있습니다.

네비게이션 편집

맵 모델을 띄우고 픽킹으로 셀을 찍어 메시를 만듭니다. 「Modify Navigation」을 켜면 편집이 시작됩니다.

  • 셀 생성 — 픽킹할 때마다 그 위치에 큐브를 두고, 세 번째 클릭에서 셀을 만듭니다. 기존 꼭짓점 1.5 안을 찍으면 그 좌표를 그대로 사용해, 엔진이 두 셀을 이웃으로 연결할 수 있게 했습니다(02절 참고).
  • 셀 번호 — 새 셀에는 만든 순서대로 번호가 붙습니다. 셀을 지우면 번호를 다시 매기고 이웃을 다시 계산합니다.
  • 되돌리기 — BackSpace로 꼭짓점 설정 중이면 마지막 픽킹을, 아니면 마지막 셀을 지웁니다.
  • 지연 삭제 — ImGui가 셀 목록을 도는 중에 셀이 지워지지 않도록, 지울 셀을 모아 두었다가 다음 Tick 시작에 한꺼번에 지우고 번호와 이웃을 다시 계산합니다. 엔진의 UActorGroup과 같은 방식입니다.
  • 렌더 범위 지정 — 셀이 많아지면 디버그 큐브도 늘어 프레임이 떨어져, 그릴 셀의 번호 범위를 정할 수 있게 했습니다.
  • 셀 검사 — Pick_On_Cell로 셀을 골라 번호 · 이웃 번호를 보고, 점프 가능 여부를 바꾸거나 지웁니다. 번호 오류를 이웃 정보로 찾아 고쳤습니다.
  • 저장 · 불러오기 — Region마다 이름을 붙여 Navigation 폴더에 파일로 저장하고, 폴더째 다시 불러옵니다.
픽킹으로 꼭짓점을 찍는 화면
꼭짓점 픽킹
세 꼭짓점으로 셀이 만들어진 화면
세 번째 클릭에서 셀 생성

모델 배치(레벨) 툴

맵 모델 · 오브젝트 · 몬스터를 한 화면에서 배치하고, 바이너리 파일로 저장해 클라이언트에서 불러오는 레벨 툴입니다.

목록에서 모델을 골라 배치하고, 배치 묶음을 MapLayouts로 저장하고 불러옵니다. 일반 모델(ModelViewer)과 애니메이션 모델(AnimModelViewer)은 따로 다루고, ImGuizmo로 이동 · 회전 · 크기를 조절합니다.

모델 배치 툴 화면 — ModelViewer와 AnimModelViewer 창, MapLayouts 저장과 불러오기 버튼
모델 배치 — ConvertModels, MapLayouts 저장 · 불러오기
모델 배치 툴 화면
모델 배치
ImGuizmo로 몬스터 변환 조작
ImGuizmo 변환 조작

09한계와 다시 만든다면

한계다시 만든다면
셀 번호 하드코딩플레이어 시작 위치, 캐릭터 Navigation 시작 위치, 게임 종료 트리거가 셀 번호로 고정돼 있어, 툴에서 셀 하나만 지워도 번호가 당겨져 어긋납니다. → 맵에 스폰 · 트리거 마커를 두고 위치로 셀을 찾겠습니다.
네비게이션 툴 최적화셀이 많아지면 꼭짓점마다 그리는 디버그 큐브가 늘어 편집 중 프레임이 떨어졌고, 그릴 셀의 번호 범위를 정하는 것으로만 피했습니다. → 큐브를 한 번에 그리는 인스턴싱이나 화면에 보이는 셀만 그리는 방식으로 바꾸겠습니다.
편집 코드가 엔진에 섞임편집 로직이 엔진 URegion · UStage 안 _EDIT_NAVI 블록에 있습니다. → Tool 쪽 래퍼로 분리하겠습니다.
저장 포맷에 버전 없음셀 정보를 순서대로 기록할 뿐이라 구조가 바뀌면 옛 파일을 구분할 수 없습니다. → 매직 · 버전 헤더를 두겠습니다.
몬스터 로직 중복몬스터 4종이 추격 · 배회 · 복귀의 경로 탐색 블록을 각자 가지고 있습니다. → CMob 또는 경로 추종 컴포넌트로 모으겠습니다.
A* 1초 재시작프레임당 노드 하나라 60fps 기준 초당 약 60노드를 봅니다. 1초마다 새로 시작하므로 그보다 먼 경로는 끝나기 전에 다시 시작될 수 있습니다. → 프레임당 노드 예산을 두고, 탐색 중에는 재시작을 미루겠습니다.
이웃 셀 조회가 선형 탐색A*가 이웃 번호로 셀을 찾을 때 컨테이너 전체를 돌며 번호를 비교합니다. → 번호와 배열 위치를 일치시켜 바로 인덱싱하겠습니다.