SFML 라이브러리에서 제공하는 기본적인 시스템 루프는 다음과 같습니다.
1. 대기 중인 Event Queue에서 처리할 이벤트를 하나 가져와서 처리한다.
처리할 이벤트가 없다면 2번으로 넘어간다.
2. 그래픽 렌더링을 처리한다.
3. 윈도우가 열려 있는 동안, 위 과정을 반복한다.
여기서 이벤트를 갖고 오기 위해 사용하는 메서드가 pollEvent입니다.
waitEvent는 현재 처리할 이벤트가 없는 경우, 이벤트가 발생할 때까지 대기하는 반면,
pollEvent는 처리할 이벤트가 없다면 기다리지 않고 넘어갑니다.
문제가 발생하는 지점도 비슷합니다.
이벤트를 빠르게 처리했거나 처리할 이벤트가 없다면, 바로 2번으로 넘어가 렌더링을 수행할 수 있습니다.
다만 처리할 이벤트가 너무 많은 경우에는 렌더링으로 넘어가지 못하고 이벤트만 처리하고 있게 됩니다.
대부분의 이벤트는 이러한 bottleneck을 일으킬 정도로 오래 걸리진 않지만,
윈도우를 드래그하는 경우엔 다릅니다.
Windows에서는 (다른 OS를 확인해보진 못했지만) 윈도우 창을 드래그해서 움직이는 동안 block 처리가 되어
1번에서 2번으로 넘어가질 못하게 됩니다. 따라서 다음과 같은 현상이 발생하게 됩니다.
이 문제를 해결하기 위해선 결국 렌더링과 이벤트 처리를 분리해줄 필요가 있습니다.
다시 말해 Multi thread를 사용해야 합니다.
둘 다 각각 thread를 할당해주기엔, main thread에서 할 일이 없으니
하나는 main thread에서, 하나는 child thread를 만들어서 할당해줍시다.
그렇다면 문제의 해결 방법이 자세하게는 두 가지로 나뉘겠네요.
1. 이벤트 처리는 main에서, 렌더링은 new thread에서 한다.
2. 렌더링을 main에서, 이벤트 처리를 new thread에서 한다.
Multi thread 자체가 까다롭고 머리 아픈 작업이긴 하지만, 두 가지 중에선 1번이 좀 더 편한 것 같습니다.
렌더링은 단순히 draw call만 하는 것이기도 하고, 무엇보다도 현재 SFML 2.5.1이 사용하는 그래픽 라이브러리가 openGL인 이유가 큽니다. openGL은 single thread로 돌아가더군요.
Vulkan이 다음 버전에서 지원될 가능성이 높긴 한데, 뭐 언제 출시될 지 모르는 일이니까요.
다만... 1번은 제 설계 상에 문제가 하나 있었습니다.
이벤트 처리는 윈도우가 켜진 후로부터 꺼질 때까지 계속 루프를 수행합니다.
다만 렌더링은 경우에 따라 사용자가 렌더링을 하지 않도록 요청할 수도, 추가적인 갱신을 요청할 수도 있습니다.
결국 내가 원할 때 렌더링을 켜고 끌 수가 없다는 얘기죠.
그래서 2번으로 선회해야 했습니다.
2번의 문제는 openGL로 돌리기엔 별로 좋지 않은 환경이란 점입니다.
앞에서 말했듯이, openGL은 single thread로 작동합니다.
main thread에서 윈도우 객체를 생성했다면, 이 윈도우는 main thread에서만 다룰 수 있습니다.
따라서 thread를 새로 생성하고, 그 thread 내에서 window 객체 생성부터 이벤트 처리까지 모두 담당해야 합니다.
이 과정까지는 나름대로 할만합니다. 람다식에 다 때려넣으면 돌아가니까요.
다음 문제는 렌더링입니다. new thread에서 생성된 윈도우이니, main thread에서 호출하는 render call도 제대로 작동하지 않습니다. 그래도 이건 SFML 라이브러리에서 관련 함수를 제공하는데, setActive 함수를 사용하면 됩니다.
윈도우가 한 thread에서만 렌더링 활성화될 수 있기 때문에, 활성화 상태를 켜고 끌 수 있게 해줍니다.
따라서 new thread에서는 윈도우 객체를 생성하고, setActive(false)를 실행한 다음, 이벤트 루프를 처리합니다.
setActive까지 안전하게 실행한 후에야, main thread에서 render call을 해도 안전합니다.
그러니 mutex lock 같은 것을 적용해줄 필요가 있습니다. 그러지 않으면 실행할 때마다 에러 가챠를 돌리는 골치 아픈 일이 발생합니다.
그리고 여기서도 끝나지 않습니다. 방금까지의 처리는 윈도우를 생성할 때에 대비한 것이라면, 윈도우를 종료할 때 또한 문제가 발생할 수 있겠죠.
render call은 어느 시점에서나 발생할 수 있기 때문에, close call이 나온 이후에는 render call을 추가로 받지 않아야 하며, 아직 처리되지 않은 render call도 모두 처리해야 합니다. 이렇게 정리된 후에 close를 실행할 수 있습니다.
근데 rende call이 얼마나 남아있는지 알 수 있는 방법이 없는 관계로, 일단은 condition value와 sleep으로 처리하고 있습니다. rendering 도중인 경우에는 sleep으로 기다리고, rendering 측에서도 종료 중이므로 추가적인 call을 방지했습니다. sleep을 쓴다는 점이 좀 마음에 걸리네요.
전체적인 pseudo code를 정리하면 다음과 같습니다.
createWindow()
{
thread windowThread(signal, this);
mutex.wait();
windowThread.detach();
}
signal()
{
window.create(...);
window.setActive(false);
mutex.notify();
//event loop
while (window.isOpen())
{
... //handle event
if (closing) //close called
{
while (isRendering) sleep(); //at least sleep for 5 ms, I think
window.close();
closing = false; //you can open the window again later
}
}
}
render()
{
if (isRendering || !window.isOpen() || closing) return;
isRendering = true;
... //render here
isRendering = false;
window.setActive(false); //turn off rendering mode
}
윈도우가 여러 개인 경우에도 잘 돌아가게끔... 하긴 했는데 테스트해보진 않았습니다.
나중에 렌더링할만한 시스템이 좀 갖추어지면 해봐야겠습니다.
'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 |
[게임 프레임워크 개발 일지] #3 EventManager 설계 (0) | 2021.12.31 |
[게임 프레임워크 개발 일지] #1 Roadmap (0) | 2021.12.16 |