텍스트 RPG 게임 만들기 (C++)

2025. 1. 16. 21:26·C++
싱글턴 객체 / 전방 선언 / cin관련 함수 / 포인터 / const 에 관한 이야기

 

C++를 이용하여 텍스트 기반 RPG 게임을 만드는 과정에 있어서 생긴 이슈들을 중점으로 글을 정리해 볼 것이다.

1. 싱글턴 객체 생성

  • 싱글턴 객체의 생성은 어디에서 해야 할까?
    • 싱글턴 객체로 구현한 클래스를 정의한 cpp 파일에서 하면 된다.
//Character.h
private:
	static Character* instance;   

//Character.cpp
...
Character* Character::instance = nullptr;

Character* Character::GetInstance(string name)
{
    if (instance == nullptr)
    {
        instance = new Character(name);
    }
    return instance;
}
  • 그러나 싱글턴 객체 또한 new를 해서 delete를 해주어야 하므로 게임이 종료되는 순간 delete를 호출해야 한다.
  • 예를 들면 아래와 같다. 
    • 캐릭터가 죽을때
    • 게임이 클리어 되서 끝날때
    • 강제로 게임 종료를 할 때 
  • 그러나 이렇게 하면, 여러군데에서 delete를 call해야 하기에 이 객체를 unique_ptr로 만드는 것을 생각해 보았다.
    • 아래와 같이 구성해 보았다.
    • unique_ptr은 소유권이 하나이므로, move 연산자를 사용해 주어야 한다.
    • 또한, GetInstance의 리턴 타입을 레퍼런스로 해야한다.
      • 그 이유는 unique_ptr은 복사될 수 없기 때문이다.
      • unique_ptr의 사용에 대해서는 추가적인 글을 통해 자세히 설명할 예정이다.
//Character.h
...
private:
	static unique_ptr<Character> instance;
    
//Character.cpp
...
unique_ptr<Character> Character::instance = nullptr;

unique_ptr<Character>& Character::GetInstance(string name)
{
    if (!instance)
    {
        instance = unique_ptr<Character>(new Character(name));
    }
    return instance;
}

//main.cpp
...
unique_ptr<Character> playerCharacter = move(Character::GetInstance(name));
  • unique_ptr로 만들지 않고 delete를 하지 않은 상태로 메모리 누수 체크를 해보면 아래와 같이 메모리 누수가 발견되는 것을 볼 수 있다.
    • 18번째 줄이 바로 instance = new Character(name); 이 부분이다.

  • 게임 종료 시 delete를 호출하게 되면 해당 메모리 누수는 발생하지 않으며, unique_ptr로 만든 버전을 사용해도 메모리 누수가 발견되지 않는 것을 확인할 수 있었다.

2. 전방 선언

클래스 구조

  • Item클래스
    • 인터페이스 클래스
    • 함수중에 Character* 를 매개변수로 가지는 Use라는 함수가 존재 
    • 해당 클래스를 상속받은 여러 클래스 존재
  • Item 자식 클래스
    • Use 함수를 override해서 구현
  • Character 클래스
    • Item* 를 저장하는 vector 를 멤버변수로 가짐

발생하는 문제

  •  Item클래스가 있는 Item.h에서 #include "Character.h"를 하고
  • Character 클래스가 있는 Character.h에서 #include "Item.h"를 하면 순환 참조 문제가 생긴다.

  • 위 에러 메세지가나오게 되는데, 이는 서로가 서로의 헤더를 계속 포함시켜 컴파일 시 문제가 발생하는 것이다.

해결방법

  • 전방 선언
    • 헤더 파일에서는 include를 하지 않고, class Item; 이런식으로 전방선언을 통해 헤더를 구성하고
    • include는 .cpp 파일에 해서 구체적인 구현을 .cpp에서 하면 된다.
  • 그러나 이렇게 각자의 헤더가 다른 헤더를 참조하도록 구현된 것은 좋은 클래스 구조가 아닐 수도 있으니, 구현하면서 다시 한번 고민해 보는 것도 좋을 것이다.
  • 아래처럼 구성하면 컴파일이 된다.

Character 클래스
Item 클래스

3. 숫자 입력 처리

  • cin.fail()
    • int 타입을 입력받을 때 우리가 string을 입력하는 경우 등 잘못된 입력을 하면, true를 반환
    • 오류 발생 시 failbit 플래그가 설정된다.
  • cin.clear()
    • 입력 스트림의 오류 플래그(위에서 언급한 failbit 등)를 초기화
    • 다시 입력 스트림을 정상 상태로 복구한다.
  • cin.ignore()
    • 입력 스트림에 남아있는 불필요한 입력 데이터를 무시
    • ignore(count, delimiter) 
      • count는 무시할 최대 문자의 갯수 (기본값 1)
      • delimiter는 종료할 문자 (기본값 EOF)
  • 활용 예제
    • 입력 값이 int이고, 0이상 3 이하의 숫자만 입력받고, 아니면 다시 입력을 받도록 한다.

 

 

4. 포인터 타입 및 delete

※ 포인터 타입 vector

vector를 통해 Item을 저장하는 상황에서 우리는 왜 Item* 를 vector의 타입으로 사용해야 할까? 

  • 그 이유는 다형성 때문이다. 
  • 우리가 단순히 Item만 저장할 것이 아니라, Item을 상속받아 만든 자식 클래스들의 객체 또한 포함되기 때문이다.
  • 아래의 코드를 보면 이해할 수 있다.
    • 다형성 특성을 잘 이용하기 위해 상속을 사용해야 하고, 
    • 이를 문제없이 수행하기 위해서는 item* 타입으로 vector에 저장해야 한다.
vector<Item*> items;
...

Item* item1 = new Sword();
Item* item2 = new Armor();

items.push_back(item1);
items.push_back(item2);

...
for(Item* item : items)
	item.use();
  • 우선 문제가 생기는 버전의 코드를 한번 살펴보자
    • 만약 Item타입으로 선언 (vector<Item>items) 한다면, use() 함수를 쓸 때 우리가 원하는 동작을 하지 않을 것이다.
    • C++는 기본적으로 정적 바인딩이기 때문에, 아래처럼 구현한 후, use함수를 쓰면 부모인 Item 클래스의 use 함수가 호출될 것이다.
      • 정적 바인딩은 실제로 무엇을 가리키는지가 아니라 타입을 따라간다는 의미이다.
      • vector안에 있는 것들은 모두 Item 타입이기 때문에 Item의 use만 call된다.
//Item.h
class Item
{
public:
	Item(string name)
		:mName(name)	
	{}

	virtual void use()
	{
		cout << "use" << mName << "\n";
	}	

private:
	string mName;	
};

//GoodItem.h
class GoodItem : public Item
{
public:
	GoodItem(string name)
		:mName(name)
		, Item(name)
	{

	}

	void use() override
	{
		cout << "Good use" << mName << "\n";
	}
private:
	string mName;
};
//Character.cpp
...
void Character::Show()
{
    for (Item item : mItems2)
    {
        item.use();
    }
}

//main.cpp

...
Character* playerCharacter = Character::GetInstance("player");

Item item("item ");
GoodItem item2("good item ");

playerCharacter->Push(item);
playerCharacter->Push(item2);

playerCharacter->Show();

위 코드의 실행 결과

 

개선된 버전은 아래와 같다.

  • vector<Item*> items로 바꾼 후
  • 각 함수들만 포인터로 동작하도록 수정하면 된다.
//main.cpp
...
Item* item = new Item("item");
Item* item2 = new GoodItem("good item");

playerCharacter->PushItem(item);
playerCharacter->PushItem(item2);

playerCharacter->ShowItems();

원하는 실행 결과

※ new 하면 delete 하기 

  • 메모리 누수 발생하면 코드 break 해주는 함수 
    • _CrtSetBreakAlloc(long NewValue);
    • 위의 함수의 매개변수는 우리가 메모리 릭 발생한 코드 블럭의 숫자를 넣어주면, 해당 위치에서 멈추게 되고
    • 추가적으로 call stack을 보면서 디버깅을 해보자
  • delete 해야하는 곳
    • Character
      • 싱글턴 객체로 생성된 character를 게임이 종료될 때 해제하기
    • Item
      • inventory 타입: vector<Item*> mItems;
      • 캐릭터가 아이템을 사용할 때, inventory에서 item을 하나씩 빼주고,  해당 포인터를 해제하기
      • 캐릭터가 가지고있는 inventory를 돌면서, 동적 할당 된 아이템들을 해제하기
    • Monster
      • 랜덤하게 생성된 monster 포인터를 monster 처치 후 delete 해주기
  • 구현 방식
    • Destroy 함수를 Character 클래스에 구현하여, 게임이 종료될 때 Destroy 함수를 통해 inventory와 Character 자신을 delete 하도록 구현
    • 아이템을 사용하는 곳에서 pop_back과 delete를 통해 해제
    • monster는 몬스터가 처치된 곳에서 delete를 통해 해제
  • 관련 코드
    • UseItem 함수는 아이템을 사용할 때 해제해주는 함수
    • Destroy함수는 게임 종료시 호출되는 함수
void Character::UseItem()
{
    if (!mItems.empty())
    {
        Item* item = mItems.back();
        cout << item->GetName() << "사용\n";
        mItems.pop_back();    

        delete item;        
    }
}

void Character::Destroy()
{
    for (Item* item : mItems)
    {
        delete item;
    }
    mItems.clear();

    delete instance;
}
  • 그러나 아직 원인을 찾을 수 없는 메모리 누수가 계속 발생했고, 해당 문제를 해결하기 위해서 스마트 포인터를 사용하여, 개선할 예정이다.

원인을 찾을 수 없었던 메모리 누수

    • 해당 메모리 누수의 문제를 찾았다.
      • 문제점은 바로 Item 클래스(인터페이스 클래스) 의 소멸자를 virtual로 만들어주지 않아서 생긴 문제이다.
      • 아래와 같이 Item 클래스에서 소멸자를 가상함수로 선언해주니, 해당 메모리 누수를 막을 수 있었다.
virtual ~Item() = default;
    • unique_ptr로 바꿔서 메모리 누수가 없는 버전을 완성시켜 깃허브에 업로드 할 예정이다. (모든 메모리 누수를 해결)
    • https://github.com/GbLeem/Cpp_Issues/tree/main/TEXT_RPG

5. Const 쓰기

https://www.youtube.com/watch?v=4fJBrditnJU

  • const를 써서 코드의 안정성을 높이자는 말을 많이 했는데, 코드를 짜다가 컴파일러가 알려주는 오류를 통해 다시 한번 느끼게 되었다.
  • 일단 리마인드를 해보면,
    • 함수 뒤에 const가 붙는다면, 해당 함수 내부에서 값을 바꾸지 않는다는 의미로 볼 수 있다.
    • 즉 getter 함수에서 이런식으로 구현하는 것이 좋다. (read only)
  • 아래 코드를 보면
    • for range문에서 const를 써서 우리는 값을 바꾸지 않고, 출력만 할 것이다 라는 의미로 썼던 것이다.
    • 그러나 Item.h 파일에서 GetName()를 상수 멤버함수로 만들지 않아서, item-> 이 부분에 "호환되지 않은 형식 지정자" 오류가 발생했다.
    • 주석 처리한 것 처럼 함수 뒤에 const를 붙여서 read only 함수로 만들어주는 것이 코드의 안정성을 높여주게 된다.
//Character.cpp
void Character::ShowItems()
{
    for (const Item* item : mItems)
    {
        cout << item->GetName() << " ";
    }
}

//Item.h
string GetName() { return mName; }  //error!
//string GetName() const { return mName; } //good

 

'C++' 카테고리의 다른 글

static Keyword  (0) 2025.03.27
unique_ptr 써보기  (2) 2025.01.17
C++ 면접 대비 정리  (0) 2025.01.09
C++ 디자인 패턴  (0) 2025.01.06
C++ TIL day 13  (1) 2025.01.03
'C++' 카테고리의 다른 글
  • static Keyword
  • unique_ptr 써보기
  • C++ 면접 대비 정리
  • C++ 디자인 패턴
gbleem
gbleem
gbleem 님의 블로그 입니다.
  • gbleem
    gbleem 님의 블로그
    gbleem
  • 전체
    오늘
    어제
    • 분류 전체보기 (184)
      • Unreal Engine (73)
      • C++ (19)
      • 알고리즘(코딩테스트) (27)
      • TIL (60)
      • CS (4)
      • 툴 (1)
  • 블로그 메뉴

    • 홈
    • 카테고리
  • 링크

    • 과제용 깃허브
    • 깃허브
    • velog
  • 공지사항

  • 인기 글

  • 태그

    map을 vector로 복사
    상속
    character animation
    blend pose
    매크로 지정자
    gamestate
    DP
    const
    actor 클래스
    Vector
    motion matching
    enhanced input system
    C++
    addonscreendebugmessage
    싱글턴
    applydamage
    BFS
    cin함수
    템플릿
    additive animation
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
gbleem
텍스트 RPG 게임 만들기 (C++)
상단으로

티스토리툴바