Post

C++: 팀 프로젝트- 던전 탐험대(Dungeon Explorer)

C++: 팀 프로젝트- 던전 탐험대(Dungeon Explorer)

https://github.com/3924js/DungeonExplorer https://www.youtube.com/watch?v=GuemXqiNJm0&t=56s 내일 배움 캠프에서 첫 팀프로젝트로 만든 C++과제이다. 나까지 6명이서 협업하여 만들었고, 나는 레이아웃, 로그 출력, 스프라이트/애니메이션을 만들었다.

기획

과제로 주어진 기초적인 텍스트 기반 RPG에서 크게 벗어나지 않고자 했다. 기본으로 주어진 필수요소들은 다음과 같다.

  • 캐릭터: 플레이어 캐릭터를 만들어야 한다. 게임 진행에 필요한 기본적인 정보를 모두 가지고 있어야 한다.
  • 몬스터: 기본적인 몬스터의 정보를 가지는 부모클래스를 바탕으로, 상속받는 3개의 자식 몬스터 클래스를 구현해야 한다.
  • 아이템: 각종 아이템의 기초가되는 부모클래스와, 플레이어가 사용할 포션, 각종 아이템을 상속해 구현해야 한다.
  • 로그 출력: 상황에 따라 적절한 로그 메세지를 출력하고 통계치로 기록되어 나중에 볼 수 있어야 한다.
  • 레벨업: 게임 진행에 따라 캐릭터 레벨이 증가하고 그에 따라 스텟이 성장해야 한다.
  • 전투: 자동으로 전투를 진행하며 플레이어는 승리 보상으로 골드와 경험치등을 획득한다. 도전과제로 2개가 추가로 있었다.
  • 보스 전투: 몬스터와의 전투에 이어 보스몬스터와의 전투를 구현해야 한다.
  • 상점 구현: 아이템을 사고 팔 수 있는 상점을 구현해야 한다.

보면 알겠지만 괭장히 기본적인 텍스트기반 게임이다. 그 틀에서 크게 벗어나지 않고 각자 미리 나누어진 파트를 맡아서 구현을 해나가서 살을 붙였다. 그래서 만들어진 최종적인 게임플로우는 다음과 같다.

  1. 플레이어의 입력을 받아 캐릭터를 생성한다.
  2. 로비에선 3가지 선택지가 주어진다.
    1. 전투를 한다.
      • 레벨에 따라 스테이지가 결정되고 스테이지와 레벨, 주사위 랜덤 값에 따라 몬스터를 랜덤으로 생성한다.
      • 플레이어와 몬스터가 번갈아가며 공격을 실행한다. 수치와 주사위 값에 따라 데미지를 못 줄 수도 있다.
        • 플레이어는 가장 전방의 몬스터를 공격한다.
          • 3번째 공격마다 모든 몬스터를 공격하는 스킬을 사용한다.
        • 몬스터들이 한번씩 공격한다.
      • 플레이어와 몬스터 모두 체력이 0이하가 되면 쓰러진다.
        • 몬스터가 모두 쓰러지면 전투가 종료된다.
        • 플레이어가 쓰러지면 통계를 출력하고 게임을 종료한다.
      • 레벨 10이 되면 보스몬스터와 전투하고, 보스를 이기면 통계 출력과 함께 게임이 끝난다.
    2. 상점을 이용한다.
      • 아이템을 사거나 팔 수 있다.
    3. 인벤토리를 확인한다.
      • 가진 아이템을 확인하거나 장착/사용할 수 있다.

        구조

        image 각자 구현할 영역은 간략히 다음처럼 나눠졌다.

        • 게임 매니저: 게임의 전반적인 진행을 관리한다. GameManager, GameFlowManager등이 들어간다.
        • 전투 매니저: 전투의 진행을 담당한다. BattleManager, BattleSupply, RandomManager등이 들어간다.
        • 캐릭터/직업: 캐릭터의 기본 정보를 저장한다. Character, Warrior, Stat등이 들어간다.
        • 몬스터/개별몹/보스: 전투할 몬스터들에 대한 정보를 저장한다. Monster, CreateMonster 등이 들어간다.
        • 상점/인벤토리/아이템: 사용될 아이템과 그 아이템을 관리하는 인벤토리와 상점이다. Item, Inventory, Store등이 들어간다.
        • 통계/스프라이트/출력 시스템: 출력과 통계와 관련된 정보들을 관리한다. LogSystem, SpriteManager, LayoutManager가 들어간다.

내가 맡은 역할은 이중 통계/출력/스프라이트를 만든 것이였다. 처음에 이 부분에 대해서 프로젝트 규모에 맡추어 작게 만들려고 했다가 점점 커지면서 이렇게 하기엔 무리일 수도 있었다는 생각이 들었다. 구현한 순서대로 보노라면 다음과 같았다. 각각의 클래스 코드가 꽤나 길어졌기에 필요하다면

  1. LogSystem.h

    가장 먼저 구현한 코드이다. 기본 요구사항에도 로그 메세지와 통계의 출력을 말해놓았기에, 텍스트 기반 굳이 역할을 분리하면 비효율적이라 생각해 로그/통계가 하나로 합쳐진 LogSystem.h를 만들었다. 구현 상의 특징들은 다음과 같다:

    • 유틸리티 클래스: 통계치 데이터는 하나의 객체에 저장되어야 한다는 판단 하에 싱글톤 패턴으로 구현하고 모든 로그 출력 함수를 정적 함수로 선언, 클래스 이름을 통해 바로 접근할 수 있도록 간편함을 구현하였다.
    • 열거형을 이용한 통계치 정리: 정수기반 통계치를 각각 개별 변수로 선언하는게 아니라, 열거형으로 미리 이름을 정의해놓고 그 길이만큼 배열을 선언, 코드상에서도 인덱스가 아니라 열거형 이름으로 보이도록 하고, 출력에 있어서도 반복문을 통해 간편히 출력하도록 하였다.
    • ANSI코드를 이용한 텍스트 효과 부여: ANSI 이스케이프 코드를 이용한 색 부여 문자열을 미리 열거형으로 선언, 이를 이용해 몬스터나 아이템 이름, 데미지등 특정 문자열을 강조하여 밋밋하지 않게 보이도록 했다.

앞서 말한대로 간단한 로그 메세지 출력과 몇몇 통계치를 다루는 수준의 클래스라면 이미 충분히 좋은 방향성일 수 있겠으나, 빠르게 마무리한 후 다른 팀원의 작업을 도와주거나 추가 기능들을 구현하려던 계획 대신, 텍스트 기반 그래픽을 이용한 UI로 방향을 선회하면서 문제가 생기게 되었다.

  1. LayouyManger.h

    텍스트 기반한 3분할 창을 구현하고 이를 밀리거나 깨지는등 모양이 어그러지는 것을 막기위해 레이아웃 관리 기능을 모아놓은 클래스, LayoutManager.h를 만들게 되었다. *LogSystem.h와 마찬가지로, 싱글톤 패턴과 정적 함수들로 만들었다. 유틸리티 클래스의 성격을 띄기에 굳이 객체를 선언해 사용하도록 하는건 불필요하다.

    • Main,Side,Log로 이름한 3개의 레이아웃은 각각 독립적인 vector<string> 버퍼를 가진다.
    • 각 버퍼는 해당되는 함수를 통해 초기화하거나, 내용을 입력할 수 있다. 입력된 내용은 경계에 쓰여진 문자를 넘어갈 수없다. * ClearWindow()함수를 이용해 콘솔창의 내용을 모두 지울 수 있다. * PrintFrame()함수를 이용해 Main,Side,Log 버퍼의 내용을 취합하여 새로 출력할 프레임을 생성해 콘솔창에 띄운다. ClearWindow()와 함께 호출되어 존재하는 창을 새 내용으로 갱신하는데 사용될 수 있다.

의도와 구조 자체는 크게 문제가 없다. 처음부터 의도한 바는 아니지만 더블 버퍼링(Double Buffering)과 유사한(완벽하게 같다고 할 순 없다.)구조가 만들어지기도 했다. 다만 기존 LogSystem.h를 구현한 후에 구현했기에 아래 문제들이 따라오게 되었다:

  • Cout 출력에 대한 의존: 레이아웃 매니저는 출력 내용이나 메세지들을 모두 레이아웃에 맞는 형태로 출력하기 위해 만들어졌다. 텍스트 기반 UI로 레이아웃을 유지해줄 다른 클래스를 따로 만들게 되면서 cout을 이용하던 모든 출력을 교체해야 하는 상황이 되었다. 이는 LogSystem.h의 출력 함수 뿐 아니라, GameManager, BattleManager등 다른 클래스들의 cout 사용까지 포함하는 문제였다. LogSystem의 로그 함수들로 모든 메세지 종류를 감당할 수 없어서 최종적으로는 LogSystem에서 LogSystem 내 버퍼를 통해 LayoutManager로 바로 메세지를 전달해주는 함수를 추가하고서야 cout을 모두 대체할 수 있었는데, 이 정도로 출력을 담당하는 어댑터의 역할이 커질 것이였다면 로그 출력 함수도 코드 재사용을 통해 더 간결하게 만들 수 있지 않았을까 싶다.
    • LogSystem.h의 갓클래스화: 레이아웃 매니저를 이용해 메세지를 출력하도록 LogSystem 자체를 LayoutManager의 어뎁터 클래스처럼 바꾸는 과정에서, 기존의 간단한 로그/통계 클래스였던 목적성에서 벗어나 과도하게 기능이많은 클래스가 되어버렸다. 코드의 줄수로 단순히 판단할 수는 없지만 다른 파일들이 길어도 400줄 내외인 반면 LogSystem.h는 600줄 가까운 분량과 기능들을 가지게 되었다. 만약 이런 구조를 가져갈려 했다면, 통계 추적을 별도의 클래스로 분리해 단일 책임 원칙을 준수하는게 좋았을 것이다.
  • ANSI코드와 string 기본 함수의 충돌: 레이아웃 매니저는 string 기본제공 함수인 replace를 이용해 버퍼의 특정 위치 문자열을 교체하는 방식으로 작동한다. 근데 ANSI코드는 콘솔창에서 자리를 차지하지 않더라도 문자열의 길이로는 존재한다. 즉 ANSI코드와 겹치는 영역에서 replace를 작동시키면, 기존 ANSI 코드가 깨지면서 색의 설정이나 해제가 의도대로 일어나지 않거나, 문자열 길이에 영향을 줘서 레이아웃 배치를 망가뜨리는 영향을 주었다. 이는 독자적인 UI의 추가로 많은 재작업을 요구하게 만든 사항이라 할 수 있다. 만약 이런 레이아웃을 특정 모양에 맞추어 출력할 것이고, 그 클래스를 구현할 것임을 미리 상정했다면, 이에 따른 어뎁터를 사용하거나 간략히 클래스의 원형을 구현해둠으로서 재작업을 피할 수 있었을 것이다. 즉 되돌아 보노라면 이 LayoutManager가 제일 먼저 구현되었어야 했다.
    1. SpriteManager.h

      레이아웃 매니저의 기능들을 이용해, Main레이아웃의 원하는 위치에 <vector<string>인 2차원 문자 배열로 몬스터의 모양을 그리는 클래스이다.

  • 각 몬스터에 해당하는 몬스터 스프라이트를 하드코딩된 데이터로 가지고 있다.
  • 전투할 몬스터의 정보를 배열로 받아 위치를 자동으로 계산하여 스프라이트를 배치할 수 있다.
  • 각 몬스터 스프라이트의 상태를 관리하여 공격/피격 스프라이트를 배치하거나 재생할 수 있다.

위에서 언급한 ANSI 코드와 replace()의 충돌을 가장 많이 겪은 부분이다. 몬스터의 모습을 ANSI 이스케이프 코드로 감싼 문자열의 배치를 이용하기 때문인데, 때문에 기존에 이미 메인 레이아웃 버퍼에 다른 값이 존재할 때, replace()를 이용해 덮어씌우는 과정에서 줄밀림, 레이아웃 깨짐같은 많은 문제를 겪었다. 최종적으로는 replace()대신 ANSI 코드의 길이까지 고려하고 substr()을 이용하여 버퍼의 해당 줄을 재조합하는 방식으로 해결했다. 그래도 범용성 있는 코드로까지 구현하지 못했다는 아쉬움은 남는다.

또 한편으로는 애니메이션/스프라이트의 정보를 별도의 클래스로 분리해서 관리하지 못했다는 점은 아쉽다. 다만 구현해야할 범위가 정해져 있고 남은 시간이 많지 않은 상태에서 추가로 구현하기로 했던 점에서, 하나의 클래스로 모두 구현했기에 오히려 구현에 걸린 시간을 줄였을 수도 있겠다는 생각은 한다.

전투 매니저와의 결합도도 문제를 삼으려면 삼을 수도 있겠다. BattleManager에서 SpriteManager를 직접적으로 호출하는 방식으로 구현했는데, 이 과정에서 최적화되지 않은 연결과정으로 인해 많은 ClearWindow()호출로 이어졌고, 이는 전투 중에 심한 화면 깜빡임으로 이어졌다. 추후의 코드 최적화와 ClearWindow()개선으로 어느정도 해결했지만, 위에서 언급한대로 스프라이트를 별도의 클래스 객체로 분리하고 호출 과정을 정리했다면, 프레임률로 보전하면서도 상점 그림이나 아이템 모양을 띄운다거나, 추후의 확장성을 챙기는 방향으로 나아갔을 수도 있을 것이다.

팀으로 작업할 때의 문제: Git 충돌과 작업영역 설정

팀 단위로 깃허브를 본격적으로 사용해보는 것은 이번이 처음이였다. 그래서 개발 외적으로 문제가 발생한 부분도 있었는데 깃허브 파일 충돌과 팀원간의 작업 경계 설정이였다. 각자 자신이 작업할 브런치를 생성하고 매일 저녁때 스크럼을 진행하며 정보를 공유한 후 머지하는 방식으로 진행했는데, 처음에 파일을 각자 브런치에서 생성하다보니, 프로젝트의 파일 목록을 가지고 있는 vcxproj, vcxproj.filter 파일이 충돌이 나는 경우가 잦았다. 해당 파일들을 잘못 건드렸는지 나머지 프로젝트가 소스파일들을 인식 못하는 경우가 2-3번 발생했다. 충돌이 날 때 해결을 하는 방법에 대해서도 익숙해져야 하는 이유라 할 수 있겠다. 그리고 각자 구현할 것들은 대략적으로 정해놨지만, 구체적으로 어떤 기능들이 들어가고, 또 그 기능들이 어떻게 엮이고 호출되는지가 명확하지 않아서 초반에 역할분담을 확실히 하는 과정에 있어서 몇차례 조정이 있었다. 필요에 따라 유연하게 움직이는건 당연히 필요한 변화이겠으나, 방향성과 기틀을 어느정도 잡아놓고 시작하면 이런 충돌때문에 허비되는 시간과 노력을 보다 줄일 수 있을 것으로 생각된다. 작업 도중에는 게임 매니저와 로그 출력 호출 방식에 대해 협의할 일이 많았던 것 같다. 아무래도 게임 흐름을 제어하는 과정에서 사용자와 제일 많이 상호작용해야 할 것이니 자연스럽게 GameFlowManager에서 LogSystem을 호출하는 일이 많았다. 내가 만들어놓은 구조에 팀원이 맞추어서 사용하는 형태가 더 많았는데, 미리 어떤 로그를 출력할지 시나리오를 정리하고 갔던 이유이기도 하겠으나, 각 상황에 해당하는 로그 출력함수를 내쪽에서 분리하기보단, 로그메세지를 알아서 커스텀하고 어떤 통계치와 해당 로그를 엮을지 정하도록 하는 구조로 갔다면 팀원에게 더 편했을 것 같다는 생각도 하게된다.

이번의 경험: 플랜의 중요성

전반적으로 내가 생각한만큼 구현하지는 못했어도 주어진 시간 안에 해낼 수 있던 만큼은 해냈던 프로젝트라고 생각된다. 그래도 프로젝트에 필요한 기능과 범위를 보다 확실히 하고, 레이아웃, 로그, 스프라이트, 통계까지 어떤 순서로 구현할지 계획하고 진행했다면 더 빠르고 효율적으로 만들 수 있지 않을까 하는 아쉬움이 남는다.

This post is licensed under CC BY 4.0 by the author.