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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | #include<iostream> #include<string> #define Log(...) printf("\n"); printf(__VA_ARGS__); namespace ConvertTo { char LowerCase(char ch) { return ch < 90 ? ch += 32 : ch; } int Index(char ch) { return LowerCase(ch) - 97; } } namespace Farmer { class Farmer_t { private: std::string name; int age; int money; bool is_death; public: Farmer_t() :money(0), age(0), is_death(false), name("기본이름") { } void Earn(int money) { this->money += money; } void Death() { this->is_death = true; } void Win() { this->age = 100; } }; namespace Command { class type { public: virtual void execute(Farmer_t&, int) = 0; }; class Earn : public type { virtual void execute(Farmer_t& farmer, int info) { farmer.Earn(info); Log("농부가 %d만큼 벌었습니다", info); } }; class Death : public type { virtual void execute(Farmer_t& farmer, int info) { farmer.Death(); Log("농부는 죽음을 맞이했습니다"); } }; class Win : public type { virtual void execute(Farmer_t& farmer, int info) { Log("농부는 인생의 승리자입니다"); } }; } class Button_t { Command::type* arr[36]; public: Button_t() { for (int i = 0; i < 36; i++) { arr[i] = nullptr; } } Button_t& setCommand(char _ch, Command::type&& command) { int idxGotten = ConvertTo::Index(_ch); arr[idxGotten] = &command; return *this; } Command::type* getCommand(char _ch) { return arr[ConvertTo::Index(_ch)]; } }; } int main() { Farmer::Farmer_t Farmer; Farmer::Button_t Farmer_Button; Farmer_Button .setCommand('a', Farmer::Command::Death()) .setCommand('b', Farmer::Command::Earn()) .setCommand('c', Farmer::Command::Win()) ; Farmer_Button .getCommand('a')->execute(Farmer, -1); Farmer_Button .getCommand('b')->execute(Farmer, -1); Farmer_Button .getCommand('c')->execute(Farmer, -1); } | cs |
굉장히 깔끔하게 잘됐다.
중간에 링크에러가 떠서 뭔가 하고 한참 확인했는데
Farmer의 메서드를 정의하지 않은 것이었다..
그리고 어제 배운 싱글톤은 생각해보니 그렇게 복잡하게 구현할 필요가 없다.
모든 클래스가 init을 무조건 하도록 하면 된다.
인스턴스화된 카운트 재고 싶은 클래스는 안에다 private으로 count 변수 선언해놓고
count가 1 이상이라면 디버그모드에서만 assert 일으키게 하면 된다.
성능 걱정과 쓸데없는 복사생성자 걱정 없이 둘 이상의 인스턴스 생성을 가장 확실하게 잡을 수 있는 방법이다.
음 이 init을 무조건 하도록 하게 하는 것이 난관인 것 같긴 하지만 말이다.
이것은 언리얼이나 유니티가 어떻게 구현되어있는지 참고해봐야겠다.
아니면 간단한 구글링으로도 나올 수 있고..
오전 5:34 확장
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | #include<iostream> #include<string> #include<stdlib.h> #include<windows.h> #include<conio.h> #define Log(...) printf("\n"); printf(__VA_ARGS__); namespace Button { template <class ReturnType, class ... Arguments> class type { public: virtual ReturnType operator()(Arguments ... arguments) = 0; }; } namespace ConvertTo { char LowerCase(char ch) { return ch < 90 ? ch += 32 : ch; } int Index(char ch) { return LowerCase(ch) - 97; } } namespace Farmer { class Farmer_t { private: std::string name; int age; int money; bool is_death; public: Farmer_t() :money(0), age(0), is_death(false), name("기본이름") { } void Earn(int money) { this->money += money; } void Death() { this->is_death = true; } void Win() { this->age = 100; } }; namespace Command { class type { public: virtual void execute(Farmer_t&, int) = 0; }; class Earn : public type { virtual void execute(Farmer_t& farmer, int info) { farmer.Earn(info); Log("농부가 %d만큼 벌었습니다", info); } }; class Death : public type { virtual void execute(Farmer_t& farmer, int info) { farmer.Death(); Log("농부는 죽음을 맞이했습니다"); } }; class Win : public type { virtual void execute(Farmer_t& farmer, int info) { Log("농부는 인생의 승리자입니다"); } }; } class Button_t { Command::type* arr[36]; public: Button_t() { for (int i = 0; i < 36; i++) { arr[i] = nullptr; } } Button_t& setCommand(char _ch, Command::type&& command) { int idxGotten = ConvertTo::Index(_ch); arr[idxGotten] = &command; return *this; } Command::type* getCommand(char _ch) { return arr[ConvertTo::Index(_ch)]; } }; } class Control { #define Define_InputProcess(X) class X##_InputProcess :public InputProcess_t { virtual int operator()(char _ch, int info) { Control::##X##_Button.getCommand(_ch)->execute(Control::##X, info); }} class InputProcess_t; static InputProcess_t* InputProcess; static Farmer::Farmer_t Farmer; static Farmer::Button_t Farmer_Button; friend class FarmerInputProcess_t; class InputProcess_t { public: virtual int operator()(char _ch, int info) = 0; }; Define_InputProcess(Farmer); /* class FarmerInputProcess_t :public InputProcess_t { virtual int operator()(char _ch, int info) { Control::Farmer_Button.getCommand(_ch) ->execute(Control::Farmer, info); } }; */ public: void run() { this->InputProcess = new Farmer_InputProcess(); while (1) { this->InputProcess->operator()(_getch(), -1); Sleep(1000); } } }; // Class control static variables definition Control::InputProcess_t* Control::InputProcess = nullptr; Farmer::Farmer_t Control::Farmer; Farmer::Button_t Control::Farmer_Button; int main() { Farmer::Farmer_t Farmer; Farmer::Button_t Farmer_Button; Farmer_Button .setCommand('a', Farmer::Command::Death()) .setCommand('b', Farmer::Command::Earn()) .setCommand('c', Farmer::Command::Win()) ; Farmer_Button .getCommand('a')->execute(Farmer, -1); Farmer_Button .getCommand('b')->execute(Farmer, -1); Farmer_Button .getCommand('c')->execute(Farmer, -1); } | cs |
Farmer_Button은 다형성의 적용이 불가능하다.
부모자식 클래스끼리 execute의 첫번째 파라미터가 다를 수밖에 없기 때문에 확장이 안된다.
그래서 나는 심각한 고민을 했다.
입력하고 실행시키는 루프는 하나여야한다.
나의 이상한 신조일지도 모르지만 그래서 나는 위와 같이 만들었다.
다만 위의 코드 또한 수정이 필요해보이기도 한다.
왜냐하면 입력을 받는 클래스를 고정된 숫자로밖에 받기 불가능하기 떄문.
그래서 입력을 받는 클래스 포인터의 배열들을 std::vector로 선언할 것이다.
이럼 매우 유동적으로 확장 가능하다.
다음은 재사용될 것 같은 코드들을 모아놓은 것이다.
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | namespace Object { class Object_t { private: public: Object_t() { } }; namespace Command { class type { public: virtual void execute(Object_t&, int) = 0; }; } class Button_t { Command::type* arr[36]; public: Button_t() { for (int i = 0; i < 36; i++) { arr[i] = nullptr; } } Button_t& setCommand(char _ch, Command::type&& command) { int idxGotten = ConvertTo::Index(_ch); arr[idxGotten] = &command; return *this; } Command::type* getCommand(char _ch) { return arr[ConvertTo::Index(_ch)]; } }; } | cs |
음 복사하면서 코드를 분석해보니 굳이 네임스페이스 안에 둬야하나 라는 생각도 든다.
그냥 클래스로 묶어도 될 것 같은데..
그래서 아까 작성한 테스트코드를 봤다.
Command와 Farmer가 분리된 형태였다.
Farmer는 최소한으로 정의된 동작만 하고 Command는 Farmer의 메서드들을 호출해서 복합적인 동작을 시킨다.
근데 굳이 커맨드를 나눌 필요가 있을까 라고 한다면 있는 것 같긴 하다.
근데 언제 나눌까..?
다른 객체들끼리 상호작용하는 매개체..? 가 되면 좋을 것 같다.
그렇다면 Command는 꼭 한 객체와 묶이지 않아도 될 것 같다.
InputProcess 역시 마찬가지다.
그러니까 정리하자면
- InputProcess는 조상 Command가 하나 생길 때마다 작성한다.
- 어떤 오브젝트에 키 포커스를 해야될 상황이 오면 해당 오브젝트의 InputProcess 인스턴스를 실행중인 std::vector 타입 배열에 밀어넣는다.
인데
여기에서 생각해보니 키를 받아 객체를 엮어 실행하는 것은 Command이다.
그렇기 때문에 위에 내가 한 말에서 오브젝트를 Command로 말해도 괜찮을 것 같다.
사용자가 입력하면 반응하는 것은 결국 Command이기 때문이다.
또한 키에 맞는 커맨드 세팅을 한 곳에서 해야한다. (항상 DefaultKeySetting(아직 미구현)메서드에서 한다)
따라서 커맨드 클래스의 이름은 항상 Command로 하는 것으로 한다.
키에 맞는 커맨드 세팅을 한 곳에서 해야하는데 Command의 이름이 제각기 다르면 안되기 때문이다.
Command는 다형성이 불가하기 때문에(execute 메서드의 첫번째 인자가 맞지 않음) 상황에 맞는 InputProcess 오브젝트를 일일히 작성해야 한다.
근데 이는 다행히도 매우 비슷한 작업이므로 매크로로 해놓았다.
DefineInputProcess(Farmer)이라고 하면 Farmer_InputProcess 클래스 코드가 생성된다.
Farmer_InputProcess 뒤에 _t를 붙이지 않은 이유는 어차피 스택에서 직접 쓸 일이 없기 때문이다.
그러니까 클래스 뒤에 굳이 _t를 붙이는 이유는
예를 들어 Farmer_t라고 하면 Farmer_t Farmer; 이런 식으로 변수에 직관적인 이름을 쓸 수 있기 떄문인데
Farmer_InputProcess_t Farmer_InputProcess; 이렇게 변수를 선언할 일이 없기 때문에 변수 이름을 붙이지 않았다.
오히려 이 클래스들은 그 타입으로 선언된 변수 이름보다는 클래스 이름을 다룰 일이 많은데
new Farmer_InputProcess_t()라고 하면
new Farmer_InputProcess()보다 헷갈릴 것이다.
위에 friend class Command를 해놓은 이유는 Command 클래스에서 Control 클래스 안의 오브젝트에 직접 접근이 가능해야 하기 때문.
그러니까 Command 클래스에서 필요한 오브젝트를 골라쓸 수 있어야 한다.
오브젝트를 Control 클래스 밖에서는 접근이 불가능하게 하고 Command 클래스 안에서만 접근이 가능하게 하고 싶었는데 이 방법밖에 없었다.
만약 Control 클래스에서 감추고 싶은 민감한 정보가 있다면 private 변수를 보관하는 클래스를 하나 작성해서
그 클래스에서 friend class Command; 하면 될 것 같다.
이러면 Command 클래스만 해당 클래스의 private 영역에 접근 가능해진다.
음 오브젝트(커맨드)가 자신의 키포커스를 제외시키는 방법이 있어야 하는데
예외를 던지면 될 것 같다.
stack rewinding(스택 풀기)라고 해서 성능의 병목을 발생시키는 경우가 있을 수도 있겠지만
뭐 어떤가. 어차피 함수 호출은 간단해서 풀어야 될 스택공간도 얼마 없을 것이다.
아니라면 나중에 방법을 생각하면 될 것 같다.
근데 예외를 그냥 던지는 건 안될 것 같다.
왜냐하면 똑같은 try ~ catch 문으로 하면 무슨 목적의 예외인지 헷갈리기 때문.
c++에서 전처리기가 쓸모있는 점이 이런 것이다.
#define Throw_KeyFocusInterrupt(keyfocusInterruptEnum code)
#define Catch_KeyFocusInterrupt(keyfocusInterruptEnum code)
이런 식으로 정의해놓으면 알아보기 편할 것 같다.
예외가 던져지면 루프를 돌리는 클래스는 현재 루프 내에 있는 모든 키포커스가 된 오브젝트들에게 해당 code를 날린다.
그리고 반환값이 참인 오브젝트를 루프 돌리는 배열에서 제외시킨다.
게임의 기본적인 루프는
while(1)
{
입력
업데이트
출력
}
이거인데
내가 지금 구현하고 있는 부분은 업데이트 부분인 것 같다.
그렇다면 초점을 돌려서, Control 내에 게임에서의 개념인 Scene 클래스를 구현하고 계층구조화시킬 수 있지 않을까?
내일 생각해야겠다.
오늘 코딩공부하다가 의도치않은 좋은 아이디어를 얻었다.
역시 뭐든간에 실전으로 해보는 게 좋다.
'진행중인 프로젝트들' 카테고리의 다른 글
구상 - 01 (0) | 2023.01.19 |
---|---|
명령패턴-undo 기능 추가해보기 (0) | 2023.01.18 |
게임프로그래밍패턴-01 서론 (0) | 2023.01.15 |
Bitmap (0) | 2023.01.15 |
Singleton, Functor (2) | 2023.01.15 |