ECS를 정석대로 구현하면, Component는 Entity의 tag 역할만 수행하고, 실제 상호작용은 System에서 이루어집니다.
다만 이러한 ECS가 나를 위한 해답이 될지는, 경우에 따라 다릅니다.
저같은 경우에도, Component에 기능을 조금 추가할 필요가 있었습니다.
처음 설계에 Renderable을 Component에 추가했기 때문에, (이게 좋지 않은 방향이어서 그랬는지는 몰라도)
그래픽 포인터를 안전하게 할당하기 위한 함수를 정의했습니다.
이번 포스팅에서는 Entity와 Component 관련 파트를 만들면서 사용한 방법을 정리하겠습니다.
처음 윈도우 창 (이하 Window)을 정의할 때는 Component로 정의했습니다.
시스템 관련으로 Component를 더 정의할 가능성이 있다고 생각했고, Entity를 시스템 관련과 인게임 관련으로 분리할 생각이었습니다.
지금은, Entity 구분을 딱히 하고 있지 않고, System Component도 Window 이외에 아직 뭔가 추가하지를 않았기 때문에
Window를 Entity로 전환할까 고민하고 있습니다. 키보드 시스템이나 사운드 시스템을 개발한 뒤에 결정해야겠습니다.
Window가 Component 치고는 너무 많은 기능이 정의되어 있다는 점도 한몫했습니다.
아무튼, Window를 Component로 추가하는 과정에서 문제가 발생했습니다.
entt 라이브러리에서 Component를 사용하기 위해서는, Component의 copy가 가능하거나, move가 가능해야 합니다.
근데 SFML에서 사용하는 Window object는 move가 불가능합니다. (당연히, copy도 불가능합니다.)
따라서 Window Component에서 SFML의 Window object를 생성해서 사용할 수 없게 되었습니다.
그러니 Window object를 우회해서 접근할 필요가 있었습니다.
결국 object로 중개해줄, 포인터를 사용해야 했습니다.
포인터를 사용하게 될 경우에 해결해야 될 문제는 다음과 같았습니다.
1. 어디서 object를 생성할 것인가?
2. dangling pointer 문제가 발생하지는 않는가?
3. user가 SFML Window object에 access하려고 하는 경우에 어떻게 안전하게 제공할 것인가?
(사실, 이걸 해결하면서 Window는 Entity로 전환시키자 라는 결론이 나오긴 했습니다만,
그래도 뭔가 얻어간 게 있으니까.... 그냥 한 번 얘기해보죠.)
1번의 경우, Window 내에서는 생성하지 못합니다. 처음 construct할 때 생성해서, destruct할 때 없어져야 되는데
그동안 Component를 건드리면서 중복 생성될 가능성이 있습니다.
그럼 뭐... 외부에서 만들고 가져와야죠. WindowPool을 정의했습니다.
WindowPool에서 Window object의 생성과 소멸을 관리합니다. 최대 갯수도 여기서 체크하면 되겠네요.
2번의 경우는... 어떤 Window가 소멸되었을 때, 그 object를 가리키던 모든 Component에서 발생할 수 있습니다.
그러니 Component가 Window를 가리키는 횟수가 0이 되면 소멸시키도록 해야겠죠.
WindowPool에 tombstone 기법을 썼습니다. shared_ptr가 생각나죠.
3번은 모호합니다. 그래서 복잡하죠.
코드를 누군가가 쓸 수 있게끔 작성하는 경우에는, 그 누군가가 정말 아무나 될 수 있다는 점을 명심해야 합니다.
제공하는 기능만으로 잘 쓸 수 있는 경우가 있는가하면, '답답해서 내가 직접 하겠다!' 하고 직접 다루는 경우도 있습니다. 후자의 경우엔 이게 심각하게 꼬이지 않도록 조심할 필요가 있습니다.
포인터를 생으로 그냥 냅다 제공해버리면, 사용자가 해제해버려서 dangling pointer가 발생할 가능성이 존재했습니다.
적어도 이러한 사태를 막아버리고 싶었기 때문에, 포인터를 안전하게 제공할 필요가 있었습니다.
(알아서 하셈 ㅋㅋ 하고 그냥 줘버릴 수도 있겠지만)
뭐... 그래서 wrapper를 정의해야 했습니다. 알고 있는 wrapper는 reference_wrapper뿐이었는데
reference는 써먹기가 어려운 상황이더군요.
그래서 포인터용 간단한 Wrapper를 정의했습니다. stackoverflow에는 뭐든지 있죠.
nullptr 여부를 판단할 수 있고, 연산자를 통한 접근만 제공하고, 포인터 자체에는 접근하지 못하게 하면 됩니다.
template <typename T>
class Wrapper
{
private:
T* ptr;
public:
Wrapper(T* ptr = nullptr) : ptr(ptr) {}
Wrapper(const Wrapper<T>& other)
{
ptr = other.ptr;
}
T* operator->() const
{
return ptr;
}
operator bool() const
{
return ptr != nullptr;
}
};
다른 파트쪽도 뭔가 적어보려고 했는데, 완성된 것도 아니고 맥락 없이 설명만 하는 것 같아서 적지 않겠습니다.
좀 더 만들어본 뒤에 정리해야 할 것 같습니다.
여기까지 진행하는 데에 시행착오가 많은 것 같습니다.
다음으로 넘어가기 전에 전체적으로 리뷰해봐야겠습니다.
'C++ > Game' 카테고리의 다른 글
[Physics] 2D 충돌 감지 구현하기 (2D Collision Detection) - Part 1 (1) | 2022.04.09 |
---|---|
[게임 프레임워크 개발 일지] #5 Multi Style이 적용된 Text (0) | 2022.02.18 |
[게임 프레임워크 개발 일지] #3 EventManager 설계 (0) | 2021.12.31 |
[게임 프레임워크 개발 일지] #2 SFML의 렌더링 정체와 Multi Thread (0) | 2021.12.17 |
[게임 프레임워크 개발 일지] #1 Roadmap (0) | 2021.12.16 |