← SEARCHABLE ARCHIVE
C / C++ · 2026-07-12

C++ 기초부터 마스터까지 — 12화

게임 구조로 정리하는 C++

INTERACTIVE READING MODE →

본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.

지금까지 배운 문법을 실제 게임 구조에 연결해보자. 문법을 외우는 것보다 어떤 책임을 어느 클래스와 자료구조에 둘지 결정하는 일이 더 어렵다.

책임 나누기

Character는 체력과 공격을 책임지고, Monster는 몬스터 상태를 책임지게 할 수 있다. DungeonGame은 파티와 활성 몬스터, 스폰 대기열, 아이템 데이터베이스를 관리한다.

class DungeonGame
{
public:
    void AddCharacter(std::unique_ptr<Character> character)
    {
        if (character)
        {
            party.push_back(std::move(character));
        }
    }

    void QueueMonster(std::shared_ptr<Monster> monster)
    {
        if (monster)
        {
            spawnQueue.push(std::move(monster));
        }
    }

private:
    std::vector<std::unique_ptr<Character>> party;
    std::vector<std::shared_ptr<Monster>> activeMonsters;
    std::queue<std::shared_ptr<Monster>> spawnQueue;
    std::stack<int> killHistory;
    std::unordered_map<int, Item> itemDatabase;
    std::weak_ptr<Monster> currentTarget;
};

파티 캐릭터는 DungeonGame이 단독 소유하므로 uniqueptr을 사용했다. 몬스터는 대기열과 활성 목록 사이에서 공유될 수 있으므로 sharedptr을 사용했다. 현재 타깃은 소유하지 않으므로 weak_ptr이다.

타입만 봐도 누가 객체를 살려두는지 어느 정도 알 수 있다.

경계값 처리

게임 코드는 정상적인 입력만 들어온다고 가정하면 금방 망가진다.

bool AttackMonster(std::size_t characterIndex, int monsterId)
{
    if (characterIndex >= party.size())
    {
        return false;
    }

    auto iter = std::find_if(
        activeMonsters.begin(),
        activeMonsters.end(),
        [monsterId](const auto& monster)
        {
            return monster && monster->GetId() == monsterId;
        }
    );

    if (iter == activeMonsters.end())
    {
        return false;
    }

    (*iter)->TakeDamage(party[characterIndex]->Attack());
    return true;
}

잘못된 인덱스, 존재하지 않는 ID, nullptr 가능성을 먼저 검사하고 조기 반환한다. 빈 queue에서 front를 호출하거나 빈 stack에서 top을 호출해서도 안 된다.

체력은 0보다 작아지지 않게 하고, 이미 죽은 객체를 다시 처리하지 않게 만들어야 한다. 안전장치는 화려한 기능은 아니지만 디버깅 시간을 크게 줄여준다.

언리얼과 대응되는 개념

일반 C++의 자료구조와 언리얼 타입은 비슷한 역할을 가진 경우가 많다.

  • std::vector와 TArray
  • std::unordered_map과 TMap
  • std::unique_ptr과 TUniquePtr
  • std::shared_ptr과 TSharedPtr
  • std::weak_ptr과 TWeakPtr

그러나 이름이 비슷하다고 UObject에도 std::shared_ptr을 사용하면 안 된다. 일반 C++ 객체는 RAII와 스마트 포인터로 수명을 관리하지만 UObject는 언리얼의 GC가 관리한다.

AActor는 일반 객체처럼 직접 delete하지 않고 Destroy()를 호출하며, 종료 과정은 EndPlay()에서 처리한다. 일반 F 타입과 UObject 타입은 소유 방식이 다르다는 점을 구분해야 한다.

또한 TArray와 std::vector 모두 재할당이 일어나면 원소를 가리키던 포인터가 무효화될 수 있다.

결국 C++ 문법은 각각 따로 떨어진 기능이 아니다. 클래스가 책임을 묶고, 컨테이너가 상태를 저장하고, 스마트 포인터가 수명을 표현하고, 알고리즘이 동작을 수행한다.

이 구조가 눈에 들어오기 시작하면 문법을 외우는 것보다 코드가 왜 이렇게 생겼는지를 이해할 수 있게 된다.