평생 배우는 개발자

2차 프로젝트 회고 본문

원티드 포텐업 게임개발 4기

2차 프로젝트 회고

개발지식 블로그 2026. 3. 15. 21:30
반응형

소감

원티드 포텐업 게임개발 4기 2차 프로젝트를 클리어했다! 1차 프로젝트에서 완성했던 QuadTree 시각화에 더해 A star Algorithm까지 완성시켜서 의미가 있었던 프로젝트였다. 이번 프로젝트는 QuadTree와 A* Algorithm을 시각화하는 게 목표였고 잘 이루어 냈다고 생각한다. 다만, 아쉬운 점은 맵을 크게 정해서 여러 actor들을 이동시킬 때 FPS가 하락하는 현상이 남아있고, actor가 이동 시에 다른 actor를 선택하면 이전 actor의 Path가 화면에 계속 남아있는 버그 등이 발견되었다는 것이다. 다음 프로젝트는 마지막 점검을 깐깐하게 해서 최적화 및 버그를 잘 잡아내야겠다.

이번에도 1차 프로젝트와 같이 정말 창의적인 아이디어로 만든 게임이 즐비했고, 배워야 할 점이 많다는 것을 여실히 깨달았다. 다음 프로젝트 부터는 언리얼로 진행을 하는데 어떤 프로젝트가 나올지 긴장도 되고 기대도 된다. 지금까지 배운 점을 바탕으로 재미있는 프로젝트를 진행하고 싶은 마음이 생겨났다.

📌 프로젝트 이름

        콘솔 뱀서라이크 V2

1. 프로젝트 개요

  • 게임 소개:
    콘솔 환경에서 Quadtree와 A* Algorithm 기술을 시각화하는 데모입니다.
  • 개발 기간:
     총 7일(2026-03-04 ~ 2026-03-10 + 발표 준비 2026-03-11 )
  • 참여 인원:
    개인 프로젝트
  • 현황(결과):

기술 스택

항목 Skill Set
Core Tech Custom Game Loop(GameLevel Tick), Actor Update Pipeline
Gameplay Systems Stat/Interface(IStatHolder), LevelUp + Pause, Hit Invincibility
Collision Quadtree 
Optimization 점유 유닛 그리드
Trouble Shoot double delete,mouse input 강제 초기화, 파싱 오류
Environment Windows Console, Visual Studio, Git, Obsidian, Trello

 

 

주요 기능

기능 설명
Player 이동 및 조작 마우스 드래그 기능, 마우스 우클릭으로 이동
충돌 처리 Quadtree 공간 분할로 확장
A* Algorithm Actor의 이동에 최적의 이동 계산
디버그 모드 화면에 Quadtree 디버그 라인 표시, A* algorithm, closed path, best path 표시

 

2. 기획 목표 

🔸기획의도 및 목표

  • 게임 분석:
    QuadTree 기술과 A* Algorithm 계산을 안정적으로 실행하고 시각화하는 것.
  • 구현 목표:
    • 안정적인 QuadTree계산과 시각화
    • 안정적인 A* Algorithm 계산과 시각화
    • 마우스 인풋을 활용한 드래그 기능 및 클릭 기능
  • 성장 목표:
    • 엔진 루프(GameLevel Tick) 구조 이해와 제어(dt/pause)
    • 자료구조 기반 최적화(Quadtree, A* Algorithm, 유닛 점유 그리드)
    • 문서화 습관(일지, 이슈 분류, 재발 방지 기록) - Obsidian, Trello, Git

🔸기록 방식

  • 문서화:
    • 솔로 프로젝트였기 때문에 기록에 집중
    • Obsidian, Trello 기반 개발일지/체크리스트/이슈 로그로 문서화
  • Git 전략:
    • 개발 과정에서 예기치 못한 버그에 대비해 빠른 롤백이 가능하도록 자주 커밋
    • 솔로 프로젝트였기 때문에 branch는 만들지 않았고, 원활한 작은 기능(Git 사용 여부 및 브랜치 전략)
  • 이슈 트래킹 방식:

Obsidian 체크리스트 + 날짜별 로그 + 이슈 분류(억까 버그/내 실력 버그/배우기 좋은 버그)로 관리
Trello : 칸반, 카드 활용 즉시 Todo 생성 및 Doing, Testing, Done카드로 옮겨가며 프로젝트 팔로우 빠르게 가능

 

 

 3. 핵심기능 구현

 3-1. A* 길 찾기 알고리즘 구현

기능 소개

드래그로 선택된 유닛에게 마우스 우클릭으로 이동 명령을 내리면, 해당 유닛은 현재 위치에서 목표 지점까지의 최적 경로를 A* 알고리즘으로 계산하고 그 경로를 따라 이동한다. 경로 위에 벽이 있거나 다른 유닛이 길을 막고 있으면 이를 회피하고, 이동 중에 막히면 즉시 새 경로를 다시 계산한다.

작업 과정

A* 알고리즘의 기본 구조는 Open List와 Closed List를 관리하면서 f(n) = g(n) + h(n) 공식으로 다음에 탐색할 노드를 결정하는 방식이다. 여기서 g(n)은 출발지부터 현재 노드까지의 실제 비용이고, h(n)은 현재 노드에서 목적지까지의 추정 비용인 휴리스틱이다.

휴리스틱으로는 맨해튼 거리(abs(dx) + abs(dy))를 선택했다. 상하좌우 4방향 이동만 지원하기 때문에 맨해튼 거리가 실제 이동 비용을 가장 정확하게 추정한다. 

기술적 도전 — 유닛 크기 문제

단순히 좌표 한 칸만 보는 게 아니라 유닛의 전체 크기 영역을 고려해야 했다. 예를 들어 유닛이 3 ×3 크기라면, 경로의 특정 노드에 도달했을 때 유닛의 몸체가 차지하는 3×3 영역 전체에 대해 벽이나 다른 유닛과의 충돌 여부를 확인해야 한다. 단순히 현재 좌표 한 점만 검사하면 이동 중에 벽이나 다른 유닛과 겹치는 현상이 발생한다.

이중 장애물 회피 구조

장애물 확인은 두 가지 경로로 분리했다. 벽은 IsBlockedByMap() 함수로 타일 배열을 직접 조회하고, 다른 유닛의 위치는 유닛 점유 그리드를 통해 O(1)로 조회한다. 이 두 가지 확인을 동시에 통과해야 해당 칸으로 이동 가능하다고 판단한다.

재탐색 로직

경로를 계산한 이후에도 다른 유닛이 이동해 길을 막는 상황이 발생할 수 있다. 이를 대비해 이동 중에 막히면 즉시 A*를 재실행하는 재탐색 로직을 구현했다. 단, 재탐색을 무제한으로 허용하면 여러 유닛이 동시에 재탐색을 반복할 때 FPS가 급격히 떨어진다. 실험을 통해 재탐색 횟수를 최대 3회로 제한하는 것이 경로 안정성과 성능 사이의 균형점임을 확인했다.

배운 점

알고리즘을 이론으로 이해하는 것과 실제로 게임에 통합해 돌아가게 만드는 것은 완전히 다른 문제였다. 특히 유닛 크기, 동적으로 변하는 장애물, 성능 제한 같은 현실적인 제약 조건들이 알고리즘 구현을 훨씬 복잡하게 만든다. 이론 공부와 구현 연습을 병행하는 것의 중요성을 실감했다.


3-2. 유닛 점유 그리드

기능 소개

맵의 각 타일 칸에 현재 그 칸을 점유하고 있는 유닛이 무엇인지를 기록하는 2차원 배열 구조다. A* 탐색 과정에서 특정 좌표를 이동 가능 여부를 판단할 때 이 배열을 조회한다.

작업 과정과 도입 배경

처음에는 A* 탐색 중에 유닛과의 충돌 확인을 QuadTree로 처리하려 했다. QuadTree가 공간 탐색을 빠르게 해 준다고 생각했기 때문이다. 그런데 실제로 구현해 보니 A*는 경로 하나를 계산하는 동안 수십에서 수백 번의 충돌 확인을 반복한다. 게다가 유닛 수가 많아질수록 이 반복 횟수는 더 늘어난다. QuadTree 쿼리는 그 자체로도 동적 갱신 비용이 있어서, 이렇게 고빈도로 호출하기에는 너무 무거웠다. FPS가 눈에 띄게 떨어지는 것을 확인하고 다른 방법을 찾았다.

해결책은 단순했다. 맵 전체를 2차원 배열로 만들고, 유닛이 이동할 때마다 이전 위치를 null로, 새 위치를 해당 유닛 참조로 업데이트하는 방식이다. A* 탐색 중에는 이 배열을 좌표로 직접 인덱싱하는 O(1) 조회 한 번으로 유닛 존재 여부를 확인할 수 있다.

효과

방식유닛 확인 비용특이사항
QuadTree (이전) O(log n) ~ O(n) 동적 갱신 비용 추가 발생
점유 그리드 (현재) O(1) 배열 인덱스 조회 1회

유닛 수가 늘어날수록 성능 차이는 더욱 극명해진다. QuadTree는 데이터가 많을수록 탐색 비용이 늘어나지만, 점유 그리드는 유닛 수와 무관하게 항상 O(1)을 유지한다.

배운 점

"공간 탐색이 필요하면 무조건 공간 분할 자료구조"라는 선입견이 얼마나 위험한지 배웠다. 해결하려는 문제의 접근 패턴을 먼저 분석하는 것이 중요하다. 고빈도 반복 조회가 필요한 상황에서는 복잡한 자료구조보다 단순한 배열이 훨씬 나은 선택일 수 있다. 자료구조는 용도에 맞게 선택해야 한다.


3-3. QuadTree 공간 분할 충돌 최적화

기능 소개

맵 공간을 재귀적으로 4등분(좌상, 우상, 좌하, 우하)해서 그 구역 안의 Actor를 저장하는 트리 자료구조다. 팀 간 충돌 판정 시 전체 유닛을 순회하는 대신, 대상 유닛 주변 영역과 겹치는 후보만 빠르게 추출하는 데 사용한다.

작업 과정

매 프레임 모든 활성 Actor를 위치 기반으로 QuadTree에 삽입한다. Team A 유닛의 충돌 판정이 필요할 때 Query()를 호출하면 해당 유닛의 영역과 겹치는 후보 목록만 반환된다. 반환된 목록에서 Team B 유닛만 필터링한 뒤, TestIntersect()로 AABB 충돌과 layer/mask 조건을 동시에 확인해 최종 충돌 여부를 결정한다.

점유 그리드와의 역할 분리

QuadTree와 점유 그리드를 함께 쓰는 것이 중복처럼 보일 수 있다. 하지만 두 구조는 해결하는 문제가 완전히 다르다.

점유 그리드는 A* 탐색 중 특정 타일 칸에 유닛이 있는지를 확인하는 O(1) 룩업 테이블이다. 그리드 단위의 이산적 공간에서 작동한다.

QuadTree는 A*와 독립적으로, 팀 간 실시간 충돌 판정 시 근거리 후보를 추려내는 연속 공간 인덱스다. 유닛의 실제 픽셀/좌표 영역 기반으로 작동한다.

두 구조가 각자의 역할을 맡아야 시스템 전체가 성능과 정확성을 함께 갖출 수 있다.

효과

QuadTree 없이 단순 순회로 충돌을 판정하면 O(n²) 비용이 발생한다. 유닛 수가 10개면 100번, 30개면 900번의 비교가 필요하다. QuadTree를 도입하면 공간적으로 멀리 있는 유닛은 후보에서 처음부터 제외되므로 실제 비교 횟수가 대폭 줄어든다.

배운 점

하나의 문제를 하나의 완벽한 자료구조로 해결하려는 생각보다, 각 구조의 강점을 파악하고 문제의 특성에 맞게 조합하는 것이 더 현실적이고 효과적인 설계라는 것을 배웠다. QuadTree, 점유 그리드, A*는 서로 다른 레이어에서 서로 다른 문제를 해결하며 시스템을 완성한다.


3-4. 마우스 입력 & 드래그 UI

기능 소개

마우스 좌클릭 드래그로 범위 안의 유닛을 다중 선택하고, 우클릭으로 선택된 유닛 전체에 이동 명령을 일괄 전달하는 UI 시스템이다.

작업 과정

콘솔 환경에서 마우스 입력을 처리하기 위해 Windows API 기반의 MOUSE_EVENT_RECORD를 활용했다. 드래그 선택은 마우스 버튼을 누른 시작 좌표와 현재 좌표로 사각형 영역을 계산하고, 해당 영역 안에 포함된 Actor를 선택 상태로 전환하는 방식으로 구현했다. 선택된 유닛은 시각적으로 구분되도록 표시 처리도 함께 했다.

트러블 슈팅

레벨이 바뀔 때 system("cls")를 사용했는데 이 함수가 mouse inputsystem mode가 초기화되던 문제를 발견하고, 최초에만 설정하던 mode를 매 level이 변경될 때마다 변경해 주어서 해결하였다.


4. 프로젝트 기록 & 추적 시스템

작업 과정

Obsidian, Git, Trello 세 가지 도구를 각각의 역할에 맞게 운영했다.

Obsidian은 개발 과정에서 만든 모든 기술 지식의 허브였다. A*와 QuadTree를 설계하는 과정에서 떠오른 아이디어, 구조 결정의 근거, 트러블슈팅 원인과 해결 과정을 모두 마크다운 문서로 정리했다. 특히 알고리즘 설계 노트는 막힐 때마다 다시 읽으면서 현재 상태를 점검하는 기준점이 되었다.

Git은 기능 단위로 커밋해 변경 이력을 관리했다. 버그가 발생했을 때 어느 커밋 이후부터 문제가 생겼는지 이력을 추적하고, 필요하면 롤백해 문제 지점을 복원할 수 있었다. 이는 디버깅 시간을 크게 단축시켜 주었다.

Trello는 할 일 관리 보드로 운영했다. TODO/DOING/DONE의 칸반 방식으로 기능 구현 상태를 시각화했고, 버그가 발생하면 즉시 카드를 만들어 이슈를 추적했다.

배운 점

도구를 쓰는 것 자체가 목적이 되어선 안 된다. 각 도구가 해결하는 문제를 명확히 이해하고, 그 도구가 가장 잘 맞는 용도에만 쓰는 것이 중요하다. Obsidian은 지식 축적, Git은 이력 관리, Trello는 작업 흐름 관리로 역할을 분리했기 때문에 세 도구가 서로 충돌하지 않고 보완하는 구조가 되었다.

 

 

5. 트러블 슈팅

ISSUE 1. 마우스 입력 모드 초기화 문제

문제 상황

팀 생성 후 마우스 우클릭으로 이동 명령을 내리면 정상적으로 동작해야 하는데, 특정 시점 이후부터 마우스 클릭이 전혀 반응하지 않거나 엉뚱한 동작을 했다. 처음에는 이동 명령 로직 자체의 문제라고 생각하고 해당 부분을 집중적으로 들여다봤지만 코드에는 문제가 없었다.

원인

범위를 넓혀 추적하던 중 system("cls")를 호출하는 시점과 문제가 발생하는 시점이 일치한다는 것을 발견했다. 레벨이 바뀌는 시점에 콘솔 화면을 지우기 위해 system("cls")를 실행하면 콘솔의 입력 모드 설정이 초기화되는 부작용이 있었다. 마우스 입력을 받으려면 SetConsoleMode()로 ENABLE_MOUSE_INPUT 플래그를 활성화해야 하는데, system("cls") 호출 이후 이 설정이 기본값으로 되돌아가면서 마우스 입력이 무시되었다.

해결 과정

문제의 핵심이 system("cls")의 부작용임을 파악한 뒤, 레벨이 바뀌거나 화면을 지울 때마다 ProcessInput()에서 명시적으로 마우스 입력 모드를 다시 설정하는 것으로 해결하였다. 레벨 전환 시마다 SetConsoleMode()를 호출해 마우스 모드를 명시적으로 복원하는 코드를 추가했다. 이후 마우스 입력이 안정적으로 동작했다.

배운 점

system()류 함수는 운영체제 수준에서 프로세스를 실행하기 때문에 현재 프로그램의 상태에 예상치 못한 부작용을 줄 수 있다. 특히 콘솔 모드 설정처럼 프로세스별로 관리되는 상태는 외부 명령 실행 후 변경될 수 있다는 점을 알아두어야 한다. 중요한 시스템 설정은 외부 호출 전후로 명시적으로 복원하는 방어적 코딩이 필요하다.

ISSUE 2. Actor 삭제 시 메모리 충돌

문제 상황

레벨을 종료하거나 전환할 때 게임이 갑자기 크래시 되는 현상이 반복적으로 발생했다. 항상 같은 시점, 즉 레벨 삭제 과정에서 터졌기 때문에 해당 흐름을 집중적으로 살펴봤다. 디버거로 확인하니 이미 해제된 메모리 주소에 다시 delete를 시도하는 이중 해제(double free) 오류였다.

원인

Actor 객체의 포인터가 여러 곳에서 관리되고 있었다. Actor는 씬의 Actor 목록에도 등록되어 있고, 팀의 유닛 목록에도 같은 포인터가 들어 있었다. 레벨을 삭제하는 과정에서 씬이 먼저 Actor를 delete 하고, 이후 팀도 동일한 Actor를 delete 하려 시도하면서 이미 해제된 메모리를 다시 해제하는 문제가 발생했다.

해결 과정

SafeDelete() 매크로 혹은 함수를 도입했다. 이 함수는 포인터가 nullptr인지 먼저 확인하고, nullptr가 아닌 경우에만 delete를 수행한 뒤 포인터를 nullptr로 설정하는 구조다. 삭제 후 포인터를 nullptr로 만들기 때문에 같은 포인터로 두 번 delete를 시도해도 두 번째 호출에서는 아무런 동작도 하지 않는다. 크래시가 완전히 사라졌다.

 
 
cpp
// SafeDelete 예시
template <typename T>
void SafeDelete(T*& ptr) {
    if (ptr != nullptr) {
        delete ptr;
        ptr = nullptr;
    }
}

배운 점

하나의 객체를 여러 컨테이너에서 동시에 소유권을 갖는 구조는 생명 주기 관리가 복잡해진다. 이 문제를 근본적으로 해결하려면 소유권을 명확히 한 곳에만 두고 다른 곳은 약한 참조만 유지하거나, 현대 C++의 shared_ptr/weak_ptr 같은 스마트 포인터를 활용하는 것이 좋다. 이번에는 SafeDelete로 방어적으로 처리했지만, 소유권 설계 자체를 처음부터 명확히 하는 것이 더 본질적인 해결책이다.

ISSUE 3. 다수 유닛 이동 명령 시 FPS 급락 (핵심 이슈)

문제 상황

유닛 수가 적을 때는 괜찮았지만 10개 이상의 유닛에 동시에 이동 명령을 내리면 게임이 버벅거리기 시작했다. 유닛 수가 늘어날수록 FPS 하락이 두드러졌고, 30개 이상이 되면 거의 슬라이드쇼 수준이 되었다. 처음에는 A* 알고리즘 자체의 탐색 복잡도 문제라고 생각했다.

원인 분석 과정

프로파일링과 코드 검토를 통해 병목이 A* 알고리즘의 탐색 자체가 아니라, 탐색 중 매 노드마다 호출하는 충돌 확인 로직에 있다는 것을 발견했다.

당시 구조는 A* 탐색 중에 특정 좌표가 다른 유닛에 의해 막혀 있는지 확인하기 위해 매번 QuadTree에 쿼리를 날리고 있었다. 경로 하나를 계산하는 동안 수십~수백 번의 QuadTree 쿼리가 발생하고, 여러 유닛이 동시에 A*를 실행하면 이 쿼리 횟수가 곱해진다. 여기에 QuadTree는 매 프레임 전체를 재구성해야 하는 갱신 비용도 있었다. 결과적으로 O(n²)에 가까운 연산이 매 프레임 발생하고 있었다.

해결 과정

핵심 아이디어는 A* 탐색 중 사용하는 유닛 확인 자료구조를 QuadTree에서 점유 그리드로 교체하는 것이었다.

점유 그리드는 맵과 동일한 크기의 2차원 배열로, 각 칸에 그 칸을 점유한 유닛의 포인터(또는 null)를 저장한다. 유닛이 이동할 때마다 이전 위치를 null로, 새 위치를 해당 유닛으로 갱신한다. A* 탐색 중에는 grid [y][x]!= null이면 이동 불가 칸으로 판단한다. 배열 인덱스 조회 한 번이므로 O(1)이다.

추가로 재탐색 횟수를 최대 3회로 제한했다. 여러 유닛이 동시에 재탐색을 무한히 반복하면 프레임당 처리량이 폭증하기 때문이다. 3회 안에 경로를 찾지 못하면 현 위치에서 대기하도록 했다.

수치 비교

유닛 수QuadTree 방식 (이전)점유 그리드 방식 (이후)
10개 버벅거림 시작 원활
30개 슬라이드쇼 수준 정상 범위 FPS
확인 비용 O(log n) + 갱신 비용 O(1)

배운 점

성능 문제를 만났을 때 "이 알고리즘이 느린 것"이라고 바로 단정하지 말고, 어느 구체적인 연산이 얼마나 자주 실행되는가를 먼저 파악해야 한다. 이번 경우 A* 자체는 문제가 아니었다. 문제는 A* 내부에서 고빈도로 반복 호출되는 확인 로직이었다.

 

시각자료

  • 주요 기능 화면:
    • A* Algorithm으로 구현 한 Title
    • 랜덤맵 생성(미로맵)
    • TeamA (파란 Actor)
    • TeamB (빨간 Actor)
    • 보라색 pixel(Closed Path)
    • 노란색 pixel(Best Path)
    • 화면상 Quad로 나눠지는 Debug Line

A* Algorithm으로 구현 한 Title
랜덤맵 생성(미로맵)
TeamA
보라색 pixel(Closed Path)
TeamA와 TeamB
Quadtree 시각화

반응형

'원티드 포텐업 게임개발 4기' 카테고리의 다른 글

1차 프로젝트 회고  (7) 2026.02.13