프로젝트 개요


1. 캐릭터 생성 및 관리

2. 레벨업 효과

3. 전투 시스템 // 방어력 , IsPoison , 턴제 전투 방식 

4. 몬스터 (보스,일반몬스터)  + 독속성 // 캐릭터 상태를 중독으로 만들고 Logic 

5. 아이템 및 골드 관리  // 랜덤값을 적용해 몬스터에게 아이템 드랍 적용
// 캐릭터 인벤토리 만들기 

6. 게임 로그 확인 => 후에 팀 회의를 통해 전투 기록 관리를 통해 대체


7. 게임 화면 UI + 스토리 출력 // 콘솔기반


8. 상점 시스템 


9. 랜덤값 생성 (GenarateRandom , IsCreateEvent =>Random.h)


10. UI중 일부인 PrintMonster , DrawBar 만들기

 

최종 프로젝트 클래스 다이어그램 :

몬스터 : 

 

캐릭터 , 아이템 매니저 : 

 

아이템 클래스 : 

 

 

Shop 클래스 : 

 

 

배틀매니저 ,  게임매니저 : 

 

 

이 중 제가 설계한 Monster,MonsterFactory,BattleManager 를 먼저 설명하고,

팀이 설계한 Character, GameManager,Shop,Item부분을 설명드리겠습니다. 

 

Monster클래스 : 

몬스터 클래스를 설계할 때, 보스몬스터의 공격력,체력은 일반몬스터의 공격력,체력에 1.5배로 

설정하고 싶은데  몬스터의 공격력을 어떻게 보스몬스터가 알게 해야하지?

라는 생각이 들었는데 , 해결책으로 Decorator패턴이 생각났습니다. 

기존몬스터의 1.5배라는건 몬스터(부모)를 Decorator로 래핑해서 생성한다면?

몬스터의 공격력을 물려받은 BossMonster도 동적으로 공격력을 확장 할 수 있게 됩니다.

 

위의 코드를 보시면 Shared_ptr을 사용하였는데, 그 이유는 다음과 같습니다.

제가 포인터가 약하였고, 그 부분을 해결 할 좋은 기회라고 생각했기 때문입니다.

 

//Monster -> MonsterDecorator (Interface) ->PoisonMonster

사실 저 move(m) 이라는것을 사용하기 위해 참 많은 트러블을 겪었습니다.

먼저 PoisonMonster가 생성할때 Monster(부모)를 인자로 받습니다.

하지만 MonsterDecorator(move(m))을 하는 순간 

monster에서 소유권이 Decorator로 넘어가게 되고, 우리는 더이상 m->getName()등 메서드를 사용할 수 없게 됩니다.

따라서 몬스터의 정보를 불러오기 위해서 PoisonMonster의 클래스에서 그 부모인 MonsterDecorator의 메서드를 가져오면됩니다. 부모는 자식을 대신할 수 있기 때문에 부모에 적절한 메서드가 있다면 이를 실행할 수 있습니다.

그래서 MonsterDecorator를 다음과 같이 작성하였습니다.

 

 

따라서 Decorator생성자가 실행이되면, protected로 monster가 들어가있기 때문에

Monster의 정보를 가져올 수 있게 됩니다.

Abstract or Interface라고 적어놓은 이유는 맨 아래에 적어놓을 거지만,

추상클래스와 인터페이스의 차이를 아직 잘 모르기 때문입니다.

 

MonsterFactory클래스

Decorator패턴으로 몬스터의 특징을 가진(1~1.5배의 랜덤공격력) 을 생성하였어도,

Monster를 캐릭터와 전투를 붙이기 위해서 그때그때 생성되는 몬스터의 로직을 클래스 내부에 사용한다면

가독성이 떨어질 것입니다. 그래서 Monster를 생성하는 MonsterFactory 클래스를 만들게 되었습니다.

이 중 IMonsterFactory와 IBossMonsterFactory의 2개의 인터페이스를 만든 이유는 

MonsterFactory의 경우 독속성 몬스터를 확률적으로 소환하게 됩니다. 

하지만 BossMonster의 경우 독속성을 가질 수 없게 설계하였습니다.

따라서, ISP 인터페이스 분리의 원칙에 의해 인터페이스를 2개로 나눠 설계하였습니다.

 

ISP 인터페이스 분리의 원칙이란? 

객체는 자신이 호출하지 않는 메소드에 의존하지 않아야한다는 원칙입니다.
구현할 객체에게 무의미한 메소드의 구현을 방지하기 위해
반드시 필요한 메소드만을 상속/구현하도록 설계하는 원칙인데요
지금 프로젝트에서 예시를 들면, 보스몬스터는 독속성을 가지지도 않는데
독속성 몬스터를 구현하는 IMonsterInterface를 상속받을 필요가 없다 생각합니다.
따라서 인터페이스를 분리하여 구현하였습니다.

 

 

CreateMonsterFunc은 몬스터를 생성하는 로직입니다. 몬스터와 보스몬스터 둘 다 

몬스터를 생성하는 로직은 같기 때문에 함수로 작성하였습니다.

 

BattleManager클래스 :

BattleManager는 캐릭터와 몬스터가 전투를 하는 방식입니다.

HandleBattle은 보스몬스터와 몬스터의 대결구도가 같은 것을 묶은 멤버함수입니다.

HandlePlayerAttack과 HandleMonsterAttack은 몬스터와 플레이어가 공격할 때를 묶은 멤버함수입니다.

addRecord는 플레이어의 전적을 확인할 수 있습니다.

 

 

저는 원래 GameManager와 BattleManager를 분리시키려했습니다.

다시 말해 GameManager와 BattleManager는 서로를 모르고 외부에서 리턴값으로 교환하는 형식으로 설계하였습니다.

하지만 팀원분들과 상의를 통해 GameManager안에 BattleManager가 있는 것이 더 좋다는 생각이 들었습니다.

그 이유는 showRecord의 기능 때문입니다.팀 회의를 통해 한분이 기록저장을 해놓으면 좋겠다라는 제안이 들어왔고,

해당 기능을 구현하면서 이건 BattleManager보다 GameManager에 들어가 있는게 좋지 않을까? 라는 생각이 들었고

이는 GameManager안에 BattleManager가 인자든, private이든 들어가 있어야 한다는 소리였습니다.

 


지금부터는 팀들이 짠 코드를 해석 및 의도를 파악해보겠습니다.

 

 

Character 클래스 :

Character의 기능은 대부분 상태를 가져오고 설정하는 Getter,Setter 메소드들입니다.

하지만 Item을 관리하는 기능을 Character가 가지게 된다면, 이는 단일책임의 원칙을 위배하게 되고,

따라서 팀원분은 Character에서 Item을 관리하는 ItemManager 클래스를 만들었습니다.

이부분을 생각못하고 저는 혼자 Character안에 구현하였는데, 팀원분의 설계를 보고

기능을 나눠서 구현하는 것을 배웠습니다.

또, Character의 경우 싱글톤 패턴을 사용하여 구현하였습니다.

플레이하는 사람의 수가 1명이라고 가정을 했기 때문에 싱글톤으로 구현하였는데

실제로 캐릭터클래스를 작성해보면서 다음과 같은 생각이 많이 들었습니다.

만일 플레이하는 사람이 여러명 MultiPlayer라면 어떻게 처리를 할 것인가?라는 생각이 들었습니다.

마지막에 정리하겠습니다.

 

 

ItemManager클래스 : 캐릭터의 아이템을 관리하는 매니저로

ADD,DELETE 등 아이템의 CRUD를 수행합니다.

원래 우리조에서 회의한것은 Map 과 Vector를 써서 Map으로 수량관리와 탐색을하고 

Vector에서 인덱스기반으로 강화시스템을 만들어보자 ! 라는 의견이 나와서 

Map과 Vector를 둘 다 사용하였습니다.

 

 

GameManager클래스

전체적인 게임을 관리하는 매니저로 

 

Shop 클래스 : GameManager에서 Shop을 호출할 수 있는 방법도 있었으나,

Character 내부에 VisitShop 메서드를 만들어 캐릭터를 통해 Shop을 방문할 수 있게 합니다.

이유는 설계를 할 때 게임매니저가 상점을 방문시키는것보다 

유저가 상점을 방문하는것이 더 확장성이 높아보였기 때문입니다.

예를들어 , 유저가 상점을 방문할 때 추가적인 디스플레이를 보여주려고 한다면 캐릭터의 정보가 필요할 수 있습니다.

따라서 Shop은 캐릭터 내부메서드로 구현하였습니다.

 

Item 클래스 :  아이템은 Item 으로부터 상속받아 다형성을 구현하였습니다.

구현한 아이템으로는 HP포션, 공격력 증가 포션, 해독제 가 있습니다.

 

 

 

겪었던 문제와 해결방법  : 

사실 이번 프로젝트는 팀적으로 구현도 해봄과 동시에 혼자서도 진행을 한 프로젝트입니다.

왜냐하면 2가지 이유가 있었는데

첫번째 이유는 협업경험이 얼마 없기 때문에, 좀 더 효율적으로 협업을 배우기 위해

내가 그 클래스들을 직접짜봐야 팀원과의 의사소통에 좀 더 도움이 될거라고 생각했고

최종적으로 다음 팀프로젝트에서는 더 나은 팀프로젝트를 수행하기 위함입니다.

 

두번째 이유는 스스로의 연습이 필요했기 때문입니다.

 

 

 

 

 

1. 헤더 순환참조가  많이 일어났다. => 의존성의 중요성

 

헤더 전방선언과 의존성의 중요성을 깨닫게 되었는데, 

클래스간의 의존성이 높고, 헤더파일을 제대로 작성하지 않는다면 

순환참조가 일어나게 됩니다.

순환참조란 ) A헤더파일에서 B를 가르키고,

B헤더파일에서 A를 가르키게 된다면 헤더끼리 서로 순환하는 형식이 되어,

헤더파일내의 메소드를 인식하지 못하는 상황이 생깁니다.

 

초반 해결 방법)

이를 해결하기 위한 방법중 헤더 전방선언을 사용해보았습니다.

헤더 전방선언 : 헤더내에서 컴파일러에게 클래스가 있다는것을 미리 알려, 해당 클래스의 포인터와 참조값은 인자로 받을 수 있게 설정합니다.

헤더를 선언하지 않고, 클래스만 있다는 것을 알려 순환참조를 방지합니다.

주의해야할 것은 전방선언으로 선언된 클래스의 메서드는 사용할 수 없습니다.

 

후반 해결 방법)

특히 BattleManager의 전적기록보관 과 Observer를 구현하는 단계에서 순환참조로 인해 구현을 하지 못하였습니다.

Observer의 경우 마감시간이 얼마 남지 않아 , 구현을 포기하였고

BattleManager의 전적기록보관의 경우 Vector를 선언하여야하는데, 순환참조가 일어나

컴파일러가 Vector메서드를 인식하지 못하였습니다.  시간이 얼마남지 않았기 때문에 전적기록을 담을 공간인

Vector를 동적배열로 만들어서 구현하였습니다.

 

 

2. Observer 패턴을 활용해 캐릭터의 Status를 화면에 띄우기 => 실패

 

Observer에 캐릭터 Status를 구조체 형식으로 만들어서 notify를 만들려하였지만,

비효율적이라고 튜터님에게 피드백을 받았습니다.

그리고 이 또한 순환참조가 일어나서 구현을 하지 못하였습니다.

 

 

3. GameManager와 BattleManager의 관계 

 

원래 서로를 모르게 설계하려 했으나, GameManager안에 BattleManager가 들어있는게 맞습니다.

그 이유는 BattleManager안의 메서드를 추가적으로 구현하게 된다면

그 메서드를 GameManager에서 사용하는게 좋은 그림이 될 수 있기 때문입니다.

 

 

4. Git Conflict 

 

깃을 협업으로 처음사용을 해봐서, 여러 Conflict가 발생하였고, 협업 초반에는

서로의 리턴값과 메서드들의 정의가 불분명하였습니다.

이로 인해 Conflict가 7개이상의 소스파일,헤더파일에서 발생하였고,

이 Conflict를 처음 접하게 되었는데 이 부분을 해결하는데 오래걸렸습니다.

해결방법으로는

1. GitHub자체에서 Conflict를 수정하였습니다

2. VisualStudio의 병합기를 사용하였습니다.

구체적인 해결방법은 다음과 같습니다.

브랜치와 메인의 차이점을 살펴보고 올바른 코드를 남기고 나머지는 지웠습니다.

 

Conflict를 겪으면서 

협업과정에서 팀원들간의 리턴값을 확실하게 하고 , 메서드를 팀원들이 쉽게 알아볼 수 있게

네이밍하는 연습이 필요할 것 같다는 생각이 들었습니다.

 

5. Git Version이 안맞음

최종 제출을 남기고 얼마 남지 않은 시점에서 팀원들간의 Git Version이 맞지 않게 되었고

기능을 우선시할것인지, GameManager를 우선시할것인지에 대한 기로에 놓이게 되었습니다.

우리조가 선택한 방식은 GameManager를 우선시하며 내 로컬에 있는 기능을 마지막까지 

옮기는 방식이였습니다.

내 로컬에 있는 기능들은 혼자 이것저것 연습해보기 위함과 구현에 성공한다면 TeamGit에 올리는 방식으로 

사용하고 있었습니다. 그래서 팀프로젝트가 있는 Git과는 GameManager부분이 많이 다르기 때문에

둘중에 하나를 양자택일 하여야했습니다.

 

왜 Version이 안맞는지는 모르겠지만 , 중간과정에서 파일이 날라간것은 확실합니다.

따라서 팀끼리 Git Version이 맞지않는것을 보고 주기적으로 깃에서 PULL을 하는 습관을 들이는것이 좋아보입니다.

 

 

6. Character Class에서 ItemManager설계

제가 혼자 설계하였을 때는  캐릭터 안에 아이템을 관리하는 기능을 넣으려했으나,

팀원이 짠 코드를 보고 아 이렇게 기능을 분리해서

아이템매니저를 설계하는게 더 좋은 설계겠구나 라는 생각이 들었습니다.

 

7. Character Class에서 Map 함수 

Map 자료구조는 KEY와 Value로 이루어진 자료구조이며 레드블랙트리로 구성되어 있습니다.

Map은 자료를 입력하면 그 순간 정렬을 해주는 특징이 있습니다.

 

 

따라서 초기의 함수는 다음과 같이 사용을 하였지만,

같은 아이템일때 같다라고 인식이 되지 않았습니다.

다시말해 같은 아이템일때는 int(수량)을 늘려야하는데 map이 동작하면서 

int가 늘어나질 않았습니다.

 

그 이유는 다음과 같았습니다.

Map은 내부적으로 compare함수를 가지고 있는데, 

이 compare함수는 Shared_ptr<Item> 의 주소를 가지고 비교를 하게 됩니다.

우리의 눈에 같은 아이템이라도 Item의 주소는 다르기 때문에 이를 다르게 인식하게 됩니다.

따라서 

다음과 같이 ItemCompare를 비교함수로 주고 이름값으로 비교를 한다면 false를 반환해

두개의 아이템이 같다라는것을 인식해 수량을 증가시킵니다.

만일 이름이 다를 경우 오름차순으로 비교합니다.

 

여기에서 operator()를 사용하는 이유는 다음과 같습니다.

  • 클래스 내부에서 operator()를 정의하면 객체를 함수처럼 호출할 수 있습니다.
  • 즉, ItemCompare 클래스의 객체가 함수처럼 동작하여 두 개의 std::shared_ptr<Item>을 비교합니다.
  • STL 컨테이너에 사용자 정의 비교 함수를 전달하려면 함수 객체(또는 함수 포인터)를 사용해야 합니다

따라서 ItemCompare Struct의 객체를 함수처럼 호출하기 위해 사용하였습니다.

 

 

8. SABC 뽑기 로직 

예를들어 S등급이 1%, A 등급이 10%, B등급이 30% 나머지가 C,D라고 할 때 ,

제가 처음 생각한 코드는 if(Iscreate(1)) else {추가적인 A,B,C,D}

의 로직을 생각하게 되었습니다. 하지만 이는 명백히 잘못된 것으로

99%의 내부에서 10%,30%...을 구현하기 때문에 확률상으로 잘못되었습니다.

 

따라서 해당 뽑기로직을 구현하기위해 CDF(누적분포함수)를 적용하였습니다.

CDF란?

  • CDF는 확률 분포의 누적 확률을 나타내는 함수입니다.
  • 어떤 확률 변수 X가 특정 값 x 이하일 확률을 나타냅니다.

수학적으로는 다음과 같이 정의됩니다:

FX(x)=P(X≤x)F_X(x) = P(X \leq x)

여기서:

  • FX(x)F_X(x): 확률 변수 XX의 누적 분포 함수.
  • P(X≤x)P(X \leq x): 확률 변수 XXxx 이하일 확률.

쉽게 말해 P(X<=30) 이라면 30%이하일 확률입니다.

 

 

따라서 처음에 확률이 1%이하라면 S를 반환하고 함수가 종료됩니다.

만일 1~10의 확률이라면 A를 반환하고 함수가 종료됩니다.

 

 

마지막으로 의문이 남았던 점에 대하여 작성하겠습니다.

 

의문이 남았던점  : 

의문이 남았던 점들은 모아서 찾아보고 공부한 후 TIL로 올릴 것입니다.

우선 궁금했던것들을 먼저 찾아보고 지금의 TIL을 작성한 것이고,

추가적인 것들은 찾아보고 먼저 이해를 한 후 TIL을 작성하도록 하겠습니다.

 

1. 추상클래스와 인터페이스

 

프로젝트를 발표할 때 튜터분께서 피드백을 주신 내용중에 추상클래스를 사용하면 이점이 뭐죠?

라는 말이 기억이 남았습니다. 저는 추상클래스를 활용하게 되면 다형성을 구현 할 수 있게 됩니다.

라고 말씀드렸습니다. 하지만 정답은 의존성문제에 관한 내용이였습니다.

 

그러면 여기서 스스로 생각이 드는게, 아이템에서의 다형성에서 루트는 뭐라고 부르는 것일까?라는 생각이 들었습니다.

아직 찾아보고 있지만 둘 중 하나로 생각이 듭니다. 인터페이스와 추상클래스

이 점에서 내가 추상클래스와 인터페이스를 구별못한다는 것을 인지하였고,

이 부분을 좀 더 공부해보고  TIL로 작성하겠습니다. 또, 구체적으로 코드를 통해 의존성을

어떻게 줄여주는지를 확인해보고 싶습니다.

 

2.  만일 캐릭터가 싱글이 아닌, 멀티플레이어라면?

 

3. 위에 있는 몬스터.h의 파일에서 헤더파일을 분리하는게 더 좋을지? 아니면 합치는게 더 좋을지?

+ Recent posts