티스토리 뷰

Project

#011 : Unlight Copycat [Episode #09]

BaeMinCheon 2018. 8. 31. 14:27

Project Note #011


Unlight Copycat Episode #09

개요

환경

  • Visual Studio 2015 Professional
  • Windows 10 Home
  • pclaf (C/C++)

본 프로젝트를 오랜만에 재개함에 따라 DAY 에서 Episode 수식으로 변경했습니다. 새로운 마음가짐으로 프로젝트를 뜯어고치도록 하죠. 기존 코드를 참고하여 처음부터 다시 코딩을 하도록 하겠습니다. 우선 필터를 여러 개 두어 파일들을 분류합니다. Core필터에는 프로젝트에서 중심이 되는 파일들을, Utility 필터에는 조연이 되는 파일들을, Sequence 필터에는 시퀀스 파일들을 포함시킬 것입니다.
그리고 가독성을 위해 일부 파일들의 이름과 클래스 이름을 변경하겠습니다.
예) GameWindow -> Game

파일들이 이제 동일한 디렉터리 내에 존재하지않습니다. 따라서 프로젝트 설정으로 Include Directories 내에 Game 폴더를 추가해줍니다.
이제 각 파일들은 자신이 위치한 디렉터리 밖의 파일도 포함시킬 수 있게 되겠군요.

대부분의 파일들이 새로운 필터로 이동했기 때문에, 이에 맞추어 #include 이하 내용들을 수정해줍니다.
예) #include "Sequence.h" → #include <Sequence/Sequence.h>
그리고 프로그램의 시작점을 Entry.cpp 라는 파일로 분리하겠습니다. 기존의 Game.cpp 에 있던 시작점 mainLAF() 가 Entry.cpp로 옮겨진 것입니다.


기존에 사용했던 정적변수들을 수정하겠습니다. 대부분의 클래스들은 하나의 객체만을 가지기 때문에 굳이 정적변수로 선언할 필요가 없다고 판단했습니다.
그리고 접근자를 private 으로 바꾼 뒤 별도의 함수로 접근할 수 있도록 수정합니다. 더 이상 정적변수가 아니기 때문에, 이에 접근하기 위해 Game 객체를 가리키는 포인터를 각 클래스마다 유지하도록 합시다.
예) Game* mGame;

버튼이나 맵 등의 시퀀스보다 더 작은 요소들은 자신이 속하는 Sequence 객체를 가리키는 포인터를 유지하도록 합니다. 이로써 버튼 같은 작은 요소들도 Sequence 를 거쳐 Game 객체에 접근할 수 있게 됩니다.
예) mSeq->getGame()->getCurSeq()
그 외에도 변수명들을 고치고 깔끔하게 보이도록 정리해봅니다. 각 클래스들의 멤버변수들을 쉽게 구분하기 위해 m 접두어를 붙여주고 필요없는 함수들을 삭제합니다. 변수나 함수들이 정확한 의미를 가지도록 이름도 바꿔주고요.
예) setName() -> setText() 및 name -> mText

파일에 접근할 필요가 있는 클래스들은 파일명을 생성자로 받고 멤버변수로 유지하도록 합니다. 이로써 파일로 다시 출력할 경우에 재활용할 수 있게 되겠군요.
예) char* mFile
그리고 파일을 읽는 함수와 쓰는 함수를 만들어두어 생성자에서 수행하던 파일 읽기를 분리합니다. 우선 파일을 읽는 함수만 구현하고, 쓰는 함수는 나중에 구현하도록 하죠. 그냥 선언한 뒤 빈 함수로 방치합니다.
예) void readFile()


기존에 사용하던 std::shared_ptr 들을 모두 일반 포인터 형식들로 변경하겠습니다. 좀 더 우아하게 메모리를 할당하고 해제하기 위함입니다. 이로 인해 동적할당하는 코드뿐만 아니라 해제를 하는 코드도 별도로 작성해줘야겠군요.
예) std::unordered_map<std::string, std::shared_ptr<class Seqeunce>> mSeqMap → std::unordered_map<std::string, class Sequence*> mSeqMap

Game 클래스부터 수술을 시작합니다. 초기화를 위한 함수로 create() 를, 종료를 위한 함수로 quit() 을 선언해둡니다. create() 에서 할당을, quit() 에서 해제를 하도록 설계하겠습니다. 특히, Game 클래스는 윈도우를 파괴하는 임무 DestroyWindow(handle()) 까지 수행합니다. 생성자에서 create() 를 호출하고, 소멸자에서 quit() 을 호출하게 되는 구조입니다. 타 클래스들의 경우에도 이와 같은 구조를 가지도록 합니다.

그리고 Button 클래스를 고져봅니다. Sequence 포인터를 멤버변수로 가지도록 하고, 버튼의 상태를 저장하도록 BState 열거형을 정의해줍니다. 버튼도 4가지 상황에 대한 각각의 기능을 저장할 수 있도록 함수 객체를 배열로 멤버변수에 저장합니다.
예) std::function<void()> mFunc → std::function<void()> mFunc[4]
버튼의 상태를 저장하는 이유는 버튼 위에 마우스가 위치했는지 또는 클릭했는지 등을 구별하기 위함입니다. 이 상태값에 따라 버튼을 흰색 / 회색 / 흑색으로 출력할 것입니다. 따라서, 기존에 색상값을 저장하던 mColor 변수 및 관련된 코드들을 제거합니다.

Button 클래스 자체는 추상클래스이기 때문에, 자식클래스에서 기능들을 구현합니다. 사각형 버튼을 만들기 위해 BRect 라는 자식클래스를 작성합니다. 사각형을 표현하기 위해 별도의 mWidth 와 mHeight 멤버변수를 가집니다. Button 클래스의 가상함수를 모두 override 해줍니다.
mouseDown() 을 비롯한 클릭 관련 함수들에서 버튼의 상태값을 변경하고 해당 클릭에 대한 버튼의 기능을 수행합니다. 그리고 timer() 에서는 마우스 커서의 위치를 받아 현재 버튼 위에 위치해 있는지 검사하여 적절하게 처리합니다. 그리고 Game::timer()에서 현재 시퀀스의 timer() 를 호출하도록 합니다.

pclaf 에서 타이머 사용하기

  • void startTimer(int interval)
    • 매개변수로 타이머 호출간격을 전달하고, 타이머를 시작합니다
  • void stopTimer()
    • 작동 중이던 타이머를 종료시킵니다
  • void timer()
    • 타이머가 작동할 때마다 호출되는 함수이며, 이곳에서 타이머가 불릴 때마다 해야할 일을 작성합니다

기존에 제작한 대화상자는 Button 클래스를 응용했었지만, 한계점이 많아 이 구조를 포기하고 새로운 클래스로 제작해야겠다고 생각했습니다. 대화상자는 항상 시퀀스의 내용보다 맨 앞에 등장해야하는데, 이를 처리하기에는 기존의 Button 클래스로는 복잡해졌기 때문입니다.
Dialog 라는 새로운 클래스를 만들고, Game 클래스에서 직접 관리하도록 합시다. 그러려면 역시 Dialog 에도 Sequence 포인터를 가지고 있어야겠죠( 간접적으로 접근하기 위해 ). 또한 Game 클래스는 대화상자를 보관할 컨테이너가 있어야합니다. 게임 진행에 대화상자가 여러 개 필요할지도 몰라 우선은 std::vector 로 저장합니다. 이 또한 Game::quit() 에서 해제하는 코드를 작성해줍니다.
예) std::vector<class Dialog*> mDlgVector

Dialog 클래스도 우선은 추상클래스로 사용할 것이며, Button 클래스와 유사한 구조를 가지겠습니다. Button 과 상당 부분 겹치는 점이 많은데, 이 둘을 묶는 부모클래스를 나중에 별도로 제작하는 것이 좋겠네요. 지금은 얼마나 바뀔지 알 수 없으니 이대로 가겠습니다.
출력할 텍스트의 크기에 따라 대화상자의 크기를 변경하기 위해 생성자에서는 위치값읋 받지않겠습니다. Dialog::setText() 에서 대화상자의 위치와 크기를 설정합니다. 계산과정은 경험적으로 잘 나오는 값을 사용했습니다.

대화상자는 자체적으로 클릭을 검출할 필요가 없으므로 isInside() 함수는 제거합니다. 멤버변수로 Button 클래스를 가지면 되기 때문입니다. 또한 대화상자에서는 좌클릭만 사용할 것이므로 mouseDown() 을 제외한 함수들도 제거해줍니다.
Dialog 의 자식클래스로 버튼을 하나만 가지는 대화상자란 의미에서 DOne 클래스를 만들고, 멤버변수로 BRect 포인터를 작성해줍니다. DOne::create() 에서 버튼을 만들고 DOne::quit() 에서 해제합니다. 버튼의 기능은 DOne 의 함수를 호출하는 것입니다.
예) class BRect* mBOne


선택지가 한 개인 대화상자 DOne 을 만들었으니, 이번에는 선택지가 두 개인 DTwo 를 만들어봅시다. DOne 과 같이 Dialog 클래스를 상속받습니다. DOne 과 유사한 구조를 가지지만, BRect 포인터 멤버변수를 두 개 가진다는 차이점이 있습니다.
따라서 두 개의 버튼을 적절히 초기화하고 사용하는 코드들을 추가해줄 필요가 있습니다. 한쪽 버튼을 누르면 해당 대화상자의 기능이 실행되고, 반대쪽 버튼을 누르면 그냥 닫히는 방식으로 구현을 해봅시다. 한쪽 버튼에만 mFunc() 을 작성해주면 됩니다.

대화상자는 버튼을 클릭하자마자 삭제가 되어야합니다. 따라서 버튼이 클릭되었는지를 저장하기 위해 mIsDead 라는 멤버변수를 선언합니다. 물론 초기값은 false 로 대입하고요. 버튼에 넘기는 람다 함수에서 이 변수를 true 로 바꾸도록 하고, 대화상자의 timer() 에서 해당 변수값을 검사합니다. 대화상자가 삭제되어야한다면( 이 변수값이 true 라면 ) Game 클래스의 setDlg() 를 호출하고 nullptr 을 넘겨 메모리 해제와 Game::mDlgVec 컨테이너의 초기화를 수행합시다.
여기까지 설계하고 보니 Game 클래스에서 대화상자를 std::vector 로 보관할 필요가 없음을 알게 되었네요. 그냥 단일 포인터 변수로 바꿔줍니다.

사각형 버튼도 있으니 이제 원형 버튼을 만들 차례인 것 같군요. BCirc 이라는 이름으로 클래스를 만들어봅시다. 본 클래스도 BRect 와 유사한 구조를 가질 것입니다. 다만 원형이기 때문에 너비 및 높이가 아닌 반지름을 멤버변수로 가집니다. 이에 따라 생성자의 모양도 달라집니다.
예) float mRadius
버튼이 클릭되는지 안 되는지 판별하는 isInside() 함수의 내용도 BRect 와는 다를 것입니다. 이 부분은 이전에 작성했던 코드를 약간만 수정해서 재활용했습니다. sqrt() 대신 제곱된 값으로 바로 비교를 하는 것이 차이점입니다.


사용자의 정보를 저장하고 이를 출력하기 위한 User 클래스를 만들겠습니다. 기존에 사용하던 User 클래스와 유사한 점이 많을 것입니다. 다만 Game 클래스에서 사용자 정보를 관리하고, 반대로 User 클래스는 게임에 접근하기 위해 생성자의 매개변수가 추가됩니다. 각 시퀀스에서는 mGame 멤버변수를 통해 Game 클래스가 관리하는 사용자 정보에 접근할 수 있도록 할 것입니다.

사용자 정보로 이름 경험치 등뿐만 아니라 보유중인 덱의 정보도 저장하겠습니다. 이를 위해 DeckInfo 구조체를 정의해줍니다. 그리고 User 의 멤버변수로 해당 구조체의 배열을 작성해줍니다. 덱 관련 정보는 동적으로 할당할 필요가 없으므로 모두 정적할당으로 처리하겠습니다.

이번에는 사용자 정보와 덱 정보를 분리하겠습니다. 사용자 정보는 USER.txt 에 덱 정보는 DECK.txt 에 작성합니다. 이에 따라 User 의 생성자에서 파일주소를 두 개 받습니다. 그리고 User::create() 에서 두 파일을 모두 읽도록 합니다.
사용자 이름을 읽는 과정에서 std::string 에서부터 TCHAR 로의 변환이 필요합니다. 하지만 이는 앞서 Button 과 Dialog 클래스에서 사용한 코드가 있긴 합니다. 하지만 여러 번 등장하게 되었으니 이를 별도의 함수로 만들고 재활용하도록 하겠습니다. Utility.h 에 getTCHAR() 라는 함수로 작성해둡니다.

이름 외의 사용자 정보는 std::stoi() 로 추출할 수 있으니 간단합니다. 덱정보의 경우 덱이름을 추출하는 것은 사용자 정보와 동일하지만, 덱을 구성하는 카드들을 매핑하는 것은 좀 더 거쳐야할 단계가 있습니다. 카드 이름을 키로 받고 ID를 값으로 하는 맵을 만들 필요가 있겠네요.
당장은 User 클래스 내에 정적멤버변수로 선언해둡니다. 추후 필요한 경우 별도의 클래스로 빼도록 하죠. gCardMap 을 만들고 당장 필요한 요소들만 작성해둡니다. 처음부터 파일에 ID로 작성하면 될 일이기도 하지만 가독성을 높이기 위해 매핑을 추가한 것입니다.
예) static std::unordered_map<std::string, int> gCardMap;


이전처럼 BMP 파일들을 프로젝트에 추가하겠습니다. Game 클래스에 BMP를 보관할 컨테이너 mBMPVec 을 만들고 getter 와 setter 함수를 만들어줍니다. Game::create() 에서 이 벡터를 초기화하고, Quest 시퀀스에서 캐릭터 카드를 출력하는 데에 사용합니다.

많이 길어져 여기에서 끊겠습니다. 다음 번에는 Quest 시퀀스를 마저 작성하겠습니다. 시퀀스 요약은 게임이 완성되고 나서 해도 늦지않겠다고 판단하고 지웠습니다.

'Project' 카테고리의 다른 글

#010 : Speedrun Gunner  (0) 2018.06.22
#009 : Unlight Copycat [DAY #18]  (0) 2018.03.11
#008 : Unlight Copycat [DAY #09]  (0) 2018.03.02
#007 : Unlight Copycat [DAY #08]  (0) 2018.03.01
#006 : Unlight Copycat [DAY #05]  (0) 2018.02.26
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함