제가 생각한 EventManager의 구조는 이렇습니다.
1] Event Type은 sf::Event와 sf::String 두 가지이다.
sf::Event는 기본적인 시스템 이벤트에, sf::String은 커스텀 이벤트에 해당합니다.
두 가지 클래스를 한 컨테이너에 넣어두기는 힘들기 때문에, 각각에 대한 컨테이너를 만들어야 했습니다.
2] Event Type에 따라 Event는 여러 개 사용할 수 있다.
하나씩만 사용했다간 분명 문제가 있을 것 같아서 컨테이너를 더 끼워넣었습니다.
각 이벤트 함수에 대한 ID는 심플하게 unsigned int로 했습니다.
그냥 int를 사용해도 됐는데, 음... 왜 이랬는지는 기억 안남.
함수 객체에 대한 ID를 생성해서 Event 객체만 제시하면 알아서 추가/삭제가 가능하게 하고 싶었는데,
그건 불가능한 것 같았습니다. 고유한 property가 있어야 뭐 해시를 하든 뭘 하지,
갖고 있는 게 메모리 주소 뿐인데 이것만으로는 같은 객체인지 판별할 수가 없었습니다.
3] Event의 함수는 무엇이든 될 수 있어야 한다.
당연한 내용이지만, 생각보다 복잡했습니다. 함수 자체는 람다식을 저장하고 쓰면 됐지만, 파라미터가 문제였습니다.
그리고 가장 기본적인 클래스 (Event)를 두고, 이걸 상속하면 뭐든 알아서 정의해서 쓸 수 있게끔 설계하려고 했는데,
이것도 좀... 문제가 있었습니다.
1, 2번은 문제될 게 없었는데 3번에서 많이 막혔습니다.
일단 함수 파라미터는 void(int), void(), void(string ,int) 등 여러 가지가 될 수 있었습니다.
해결은 결국 제약을 여러 개 거는 것이었는데, return type은 void로 강제하는 게 편할 것 같았고 call by reference는 필요하다면 pointer로 처리하게끔 할 수 있었습니다.
파라미터 타입과 개수는 고민해보다가 그냥 std::vector<std::any>로 받아와서 변환하도록 하는게 좋을 것 같았습니다.
이쪽 입장에서는 뭐가 나올지 모르고, 어떻게 처리해줄 수가 없기 때문에 사용자가 직접 각 파라미터를 캐스팅하고 검사한 뒤에 사용해야 합니다. 그쯤은 '감수해줘'가 된 거죠.
다음 문제는, Event를 상속받은 클래스 객체라면, 그 클래스도 뭔지 모른다는 것입니다.
알고 있는 건 Event를 상속한 클래스라는 점이기 때문에, 당연히 Event*로 upcasting을 해서 저장하는 게 정석적인 방법입니다.
여기서 발생하는 문제는, 포인터라는 것입니다. 저는 사용자 입장에서는 어지간하면 정적 할당된 객체를 사용하게 하고 싶었습니다. 객체를 받아와서 포인터를 저장한다는 것인데, 그렇게 되면 함수 내에서 생성된 객체 등은 lifetime이 끝나게 되면 dangling pointer로 변하게 된다는 점입니다. 객체였다면 복사를 했을테니 상관 없이 살아 있었겠지만 말입니다.
그럼 사용자가 동적 할당된 Event*를 넘겨줘야 되나?
하고 보니 이렇게 되면 메모리 관리가 영 찝찝해지게 됩니다. lifetime이 어떻게 되는지, 이 객체 관리를 사용자가 직접 해야될지 EventManager 차원에서 알아서 해줘야될지가 모호하더라구요.
사용자가 불쑥 delete해버리면 EventManager에, EventManager가 하면 결국엔 사용자에게 dangling pointer가 발생할 수 있었습니다.
그렇게 해서 생각한 방안이 객체를 받아오되, EventManager 내에서 새로 동적 할당하고 복사를 하자! 였습니다.
참 멀리도 돌아가고, 많이도 꼬인 방법입니다. 이게... 맞나?
아무튼, 이렇게 할 경우 Event를 상속한 객체를 template class T로 받아와서, T가 Event의 Derived class인지만 검사하고
T* 객체를 새로 생성해서 복사를 하면 됐습니다. 이 클래스가 복사가 가능한지는... 그건 뭐 정의한 사람이 알아서 해 줄 일이긴 하니까요.
이렇게 하면 EventManager 내에서 알아서 메모리 할당하고 해제하고 하든, 바깥에서는 신경쓰지 않아도 됩니다.
저는 이것도 귀찮아서 unique_ptr를 사용했습니다.
std::map<EventType, std::unique_ptr<Event>> events;
template <typename Function>
void add(EventType type, Function func)
{
if (Event* event = dynamic_cast<Event*>(&func)) //derived check
{
std::unique_ptr<Function> ptr(new Function);
*ptr = func;
events.emplace(type, std::move(ptr));
}
}
대충 이런 방식인데, 다행스럽게도 돌아갔습니다.
실제 코드는 여기서 다른 예외 처리 같은 게 더 들어갑니다.
일단 이벤트가 하나만 추가되진 않습니다.
EventType에 대응되는 map이 필요하고, 이 map에서 Event를 찾을 id도 필요합니다.
Event 객체 자체만으로는 각각을 구별할 방법이 없기 때문에, id를 배정해줄 겁니다.
std::unordered_map<sf::Event::EventType, std::unordered_map<unsigned int, std::unique_ptr<Event>>> events;
unsigned int getNextID(sf::Event::EventType type)
{
unsigned int indicator = 0;
while (events[type].find(indicator) != events[type].end()) indicator++;
return indicator;
}
template <typename Function>
unsigned int connect(sf::Event::EventType e, Function func)
{
unsigned int ret = getNextID(e);
if (std::is_base_of<Event, Function>::value)
{
std::unique_ptr<Function> ptr(new Function);
*ptr = func;
events[e].emplace(ret, std::move(ptr));
}
else std::cerr << "Event function should be derived from class Event." << std::endl;
return ret;
}
void call(sf::Event e)
{
auto& evs = events[e.type];
for (auto func = evs.cbegin(), nxt = func; func != evs.cend(); func = nxt)
{
nxt++;
func->second->call(e);
}
}
void disconnect(sf::Event::EventType e, unsigned int indicator)
{
if (events[e].find(indicator) == events[e].end()) return;
events[e].erase(indicator);
}
custom event는 sf::Event 대신 다른 string 등을 id로 사용하면 됩니다.
이제 실제 Entity를 추가하고 렌더가 잘 되는지 이벤트 핸들링이 되는지 테스트를 해봐야 됩니다.
지금까지는 따로 각각 테스트했다면 이제 한번에 돌아가는지 봐야되는 거죠.
포스팅 작성 시점에는... 좀 갈아 엎을 게 있습니다. 또 며칠 열심히 생각해봐야겠습니다.
'C++ > Game' 카테고리의 다른 글
[Physics] 2D 충돌 감지 구현하기 (2D Collision Detection) - Part 1 (1) | 2022.04.09 |
---|---|
[게임 프레임워크 개발 일지] #5 Multi Style이 적용된 Text (0) | 2022.02.18 |
[게임 프레임워크 개발 일지] #4 Entity와 Component (0) | 2022.02.06 |
[게임 프레임워크 개발 일지] #2 SFML의 렌더링 정체와 Multi Thread (0) | 2021.12.17 |
[게임 프레임워크 개발 일지] #1 Roadmap (0) | 2021.12.16 |