함수 형태의 이벤트 시스템을 데이터 주도 시스템으로 바꿀 때 생기는 문제 중 하나는 서로 다른 이벤트끼리 호환되지 않을 수 있다는 점이다.
예를 들어, EMP 총이 있다고 하자. 이 총을 사용하면 전자 장비 꺼지게 되고, 작은 동물들은 겁에 질려 도망가며, 주변에 있던 식물들은 휘청거린다.
동물들은 Scare 이벤트에 반응해 도망간다. 전자 장비들은 TurnOff 이벤트에 반응해 꺼진다.
식물들은 Wind 이벤트 핸들러를 가진다.
여기서 문제는, EMP 총이 이런 이벤트 핸들러와 호환되지 않는다는 점이다. 따라서 결국엔 새 이벤트 타입(ex: EMP Fire)을 만들고 모든 게임 객체가 이것에 반응하도록 짜야 하는 상황이 된다.
생각의 반전
이 문제는, 이벤트 타입이란 것을 아예 생각하지 말고, 게임 객체에서 다른 객체로 데이터 스트림을 보내는 개념으로 접근하면 해결할 수 있다.
이 시스템에서는 모든 게임 객체들이 데이터 스트림을 연결할 수 있는 입력 포트와 데이터를 보낼 출력 포트를 하나 이상 갖는다.
포트끼리 선으로 연결시켜주는 그래픽 유저 인터페이스만 있다면 어떤 복잡한 행동이라도 구현 가능하다.
앞에 나온 예를 이용해 계속 설명해보자면 EMP총은 출력에 Fire 이름이 붙은 포트가 있어서 boolean 신호를 보낼 것이다.
대부분의 시간 동안 이 포트는 false(0)값을 보내고 있겠지만, 총이 발사되면 아주 짧은 기간(ex: 한 프레임) 동안 값이 true(1)로 바뀐다.
다른 게임 객체들은 다양한 반응을 촉발하는 이진 입력 포트를 갖고 있다.
동물들은 Scare 입력, 전자 장비들은 TurnOn 입력, 식물들은 Sway 입력을 갖는다.
EMP 총의 Fire 출력 포트를 게임 객체들의 입력 포트에 연결하기만 하면, 총이 발사될 때 원하는 반응이 일어나게 할 수 있다.
프로그래머는 각 타입의 게임 객체들이 어떤 포트를 가질지 결정한다. 그러면 디자이너가 원하는 행동을 구현하기 위해 GUI를 이용해 이 포트들을 이리저리 연결한다. 이 외에 프로그래머는 그래프에서 사용할 여러 종류의 노드들을 만들어야 하는데, 입력을 뒤집는 노드, 사인 파(wave)를 출력하는 노드, 현재 게임 시간을 출력하는 노드 등이 그 예다.
데이터 통로를 통해 다양한 데이터 타입을 보낼 수 있다. 불리언 데이터를 보내거나 받는 포트도 있고, 단위 부동 소수 데이터를 보내거나 받는 포트도 있을 수 있다. 그 외에 3D 벡터나 컬러 정수 등을 보내게 만들 수도 있다.
이런 시스템에서 서로 호환되는 데이터 타입의 포트끼리 연결되게 하는 것이 중요한데, 그렇지 않으면 연관 없는 포트끼리 연결됐을 때 자동으로 데이터 타입을 변경해주는 기능을 구현해야 한다.
예를 들어, 단위 부동 소수 출력을 불리언 입력에 연결한 경우, 0.5보다 작은 값은 false, 0.5 이상인 값은 true로 바꾸는 식이다.
언리얼 엔진3의 키즈멧과 같은 GUI 기반 이벤트 시스템은 이와 같은 원리로 동작한다.
다음은 c++로 작성한 예제..
(문제가 있다면 지적 부탁해요)
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 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 | #include <iostream> #include <functional> #include <list> class input_node { public: input_node(std::function<void(const void*)> handler) : handler{ handler } { } void send(const void* data) { if (handler) { handler(data); } } public: // 신호를 받았을 때 호출할 함수 std::function<void(const void*)> handler; }; class output_node { public: void send(const void* data) { if (destination) { destination->send(data); } } void connect(input_node& destination) { this->destination = &destination; } public: // 목적지 input_node* destination; }; class hub { public: hub() : input{ [this](const void* data) { send(data); } } { } void add_input_node(input_node& destination) { destination_list.push_back(&destination); } void send(const void* data) { for (auto& destination : destination_list) { destination->send(data); } } public: input_node input; std::list<input_node*> destination_list; }; class invert { public: invert() : input{ [this](const void* data) {invert_data(data); } } { } void invert_data(const void* data) { float* power = (float*)data; bool onoff; // 파워 0.5보다 쌔면 off를 보낸다. if (0.5f < *power) { onoff = false; } else { onoff = true; } output.send(&onoff); } public: input_node input; output_node output; }; class obj { public: virtual const char* name() const = 0; }; class emp_gun : public obj { public: emp_gun() : output{}, power{0.6f} { } virtual const char* name() const override { return "emp_gun"; } void fire() { printf("%s: 모래반지 빵야빵야\n", name()); output.send(&power); } // 출력 노드 output_node output; float power; }; class radio : public obj { public: radio() : input{ [this](const void* data) { bool* onoff = (bool*)data; turn_on(*onoff); } } { } virtual const char* name() const override { return "radio"; } void turn_on(bool on) { printf("%s: ", name()); if (on) { printf("켜짐\n"); } else { printf("꺼짐\n"); } } input_node input; }; class flower : public obj { public: flower() : input{ [this](const void* data){ sway(); } } { } virtual const char* name() const override { return "flower"; } void sway() { printf("%s: 흔들흔들\n", name()); } // 입력 노드 input_node input; }; class animal : public obj { public: animal() : input{ [this](const void* data){ scare(); } } { } virtual const char* name() const override { return "animal"; } void scare() { printf("%s: 후다닥(도망)\n", name()); } // 입력 노드 input_node input; }; int main() { emp_gun g; hub h; flower f; radio r; invert i; animal a; // emp_gun과 hub를 연결 g.output.connect(h.input); // hub와 flower 연결 h.add_input_node(f.input); // hub와 invert 연결 h.add_input_node(i.input); // invert와 radio 연결 i.output.connect(r.input); // hub와 animal 연결 h.add_input_node(a.input); // emp_gun 발사!! 빵야빵야 g.fire(); return 0; } | cs |
출력 결과