2024년 12월 말에 SFML 3.0이 나왔습니다.
그 말인즉, 이전의 2.0에서 많은 것이 바뀌었기 때문에 migration이 필요하다는 이야기겠죠
간단하게 요약하면
1. SFML에서 사용하는 C++이 C++17으로 업데이트되었습니다.
2. 사운드 모듈로 사용하던 openAL이 miniaudio로 교체되었습니다.
이제 openal32를 링킹하지 않아도 되는군요.
3. sf::Vector2<T>를 받을 때 scalar parameters를 사용하지 못하게 되었습니다.
무슨 말이냐면, 다음과 같은 함수 호출이 불가능해졌다는 말입니다.
//cannot use in SFML 3
sf::Videmode videoMode(1920, 1080);
sprite.setPosition(100, 100);
sf::FloatRect rect(0, 0, 300, 300);
대신 sf::Vector2<T> 객체로 wrapping해서 호출해야 합니다.
sf::Videmode videoMode(sf::Vector2u(1920, 1080));
sprite.setPosition(sf::Vector2f(100, 100));
sf::FloatRect rect(sf::Vector2f(0, 0), sf::Vector2f(300, 300));
//or you can use as
sf::Videmode videoMode({1920, 1080});
sprite.setPosition({100, 100});
sf::FloatRect rect({0, 0}, {300, 300});
{}로 wrapping 하면 쉽게 migration할 수 있습니다.
다만 이 방식은 함수에서 사용 가능한 타입을 추론하는 방식이기 때문에,
타입 추론이 명확하지 않으면 사용할 수 없습니다.
4. Event 호출 및 사용 방법이 바뀌었습니다.
포스트에서 다룰 내용은 이겁니다.
SFML 3에서 사용하는 event loop는 다음과 같은 방식입니다.
while (window.isOpen())
{
while (const std::optional event = window.pollEvent())
{
if (event->is<sf::Event::Closed>())
{
window.close();
}
else if (const auto* keyPressed = event->getIf<sf::Event::KeyPressed>())
{
if (keyPressed->scancode == sf::Keyboard::Scancode::Escape)
window.close();
}
}
// Remainder of main loop
}
pollEvent 혹은 waitEvent의 반환 타입이 std::optional<sf::Event>로 바뀌었습니다.
그리고 Event 내부 설계가 바뀌어서 이제 mouseMove 등의 객체에 직접 접근할 수가 없습니다.
//SFML 2
sf::Vector2f move(e.mouseMove.x, e.mouseMove.y);
//SFML 3
sf::Vector2f move;
if (const auto mouseMove = e->getIf<sf::Event::MouseMoved>())
move = mouseMove->position;
그런고로, SFML 3에 맞춰서 코드를 전체적으로 수정하기로 했습니다.
겸사 잘못되었거나 미구현된 부분도 다시 쓰기로 했구요.
while (const auto e = window.pollEvent())
{
sf::Event event(e.value());
if (e->is<sf::Event::TextEntered>())
{
auto text = *e->getIf<sf::Event::TextEntered>();
if (text.unicode == '\r')
text.unicode = '\n';
event = text;
}
//loop with event
}
기존에 Event 객체의 data 값을 수정하는 코드가 있었습니다만
pollEvent로 가져오는 값이 const이고, sf::Event의 default constructor가 삭제되었기 때문에
임의로 sf::Event 객체를 복사 생성하고, EventSubtype 객체를 따로 생성해서 할당하는 방식으로 처리했습니다.
방식이 영 깔끔하진 않지만, 이것 이외에 괜찮은 방법을 떠올리지 못했습니다.
이것보다 더 복잡한 문제는 기존에 구현했던 Event System이었습니다.
sf::Event::EventType으로 이벤트 타입을 구분하고 실행했었는데, 이것이 삭제되었기 때문입니다.
SFML 3에서 sf:Event의 type은 e->is<TEventSubtype>() 으로 체크하기 때문에
별도로 type을 알 수 있는 enum이 없어서 std::type_index로 대체했습니다.
template <typename EventType>
void disconnect(unsigned int indicator)
{
disconnect(typeid(EventType), indicator);
}
void disconnect(std::type_index eventType, unsigned int indicator)
{
systemEvents.at(eventType).erase(indicator);
}
//
listener.disconnect<sf::Event::MouseButtonPressed>(indicator);
event call을 할 때 event의 type을 각 EventSubtype을 일일이 is로 체크하고 실행하는 것은 번거롭다고 생각해서
github의 소스 코드를 보니 std::variant로 구현이 되어있더군요.
window에서 event를 처리하는 메서드가 하나 추가되었는데 이 variant에 visit 메서드를 호출하는 방식으로 구현되어 있었습니다.
const auto onClose = [&window](const sf::Event::Closed&)
{
window.close();
};
const auto onKeyPressed = [&window](const sf::Event::KeyPressed& keyPressed)
{
if (keyPressed.scancode == sf::Keyboard::Scancode::Escape)
window.close();
};
while (window.isOpen())
{
window.handleEvents(onClose, onKeyPressed);
// Rest of the main loop
}
std::visit를 이용하면 std::variant에 저장된 데이터를 매칭되는 타입을 argument로 받는 caller에 매칭하여 실행시킬 수 있습니다.
단, 어떻게든 visit을 통해 실행하는 함수가 존재해야 합니다. 예를 들면
std::variant<int, float> a;
std::visit([](auto& x) { std::cout << x * 2;}, a);
이 코드에서는 int와 float 모두 caller 함수에 매칭이 되어 문제가 없지만
std::variant<int, float, std::string> a;
std::visit([](auto& x) { std::cout << x * 2;}, a);
이 경우에는 std::string이 매칭될 수 있는 함수가 존재하지 않기 때문에 에러가 발생합니다.
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
std::variant<int, float, std::string> a;
std::visit(overloaded{[](auto& x) { std::cout << x * 2;},
[](auto& x) { std::cout << x;}}, a);
이런 경우에서는 std::string이 두 번째 함수로 매칭되므로 에러가 발생하지 않습니다.
SFML에서는 아예 lambda 함수 팩에 나머지 모든 타입과 매칭될 수 있는 더미 함수를 추가해서 해결했습니다.
[](const priv::DelayOverloadResolution&) { /* ignore */ }
sf::Event에 visit 함수가 구현되어 있으므로, 이걸 활용했습니다.
void call(sf::Event e)
{
if (!beforeSystemCall(e)) return;
auto caller = [this](auto& subEvent) {
std::type_index eventType = typeid(subEvent);
for (auto& p : events.at(eventType))
p.second->call(sf::Event(subEvent));
};
e.visit(caller);
}
이참에 이쪽에서 구현한 Event도 다시 쓰기로 했습니다.
function의 argument가 무엇이든 void기만 하면 다 받아들이도록 설계했었는데
여러모로 지저분했기 때문에 C++17 혹은 C++20에 맞춰서 다시 쓰기로 했습니다.
C++20에서 concept나 map에서의 contains 함수가 추가되었거든요.
C++23은... 아직 프리뷰이기도 하고 C++17/C++20도 다 알지 못하기 때문에 보류
이번에는 connect를 호출할 때 Event 객체가 아니라 lambda이더라도 암시적 변환이 가능하게끔 metaprogramming하여 설계할 겁니다.
//source : https://stackoverflow.com/questions/13358672/how-to-convert-a-lambda-to-an-stdfunction-using-templates
template <typename T>
struct function_traits
{
using return_type = void;
using type = void;
};
template <typename Ret, typename F, typename... Args>
struct function_traits<Ret(F::*)(Args...) const>
{
using return_type = Ret;
using type = std::function<Ret(Args...)>;
};
lambda를 std::function으로 변환하는 template traits 코드입니다.
지난 번에 Event 코드를 작성하는 포스트에서도 나왔었는데,
이번에는 function의 void 버전을 구하는 게 아니라 return type만을 가져오게 했습니다.
template <typename T>
using to_function = function_traits<decltype(&T::operator())>;
//C++20
class Event;
template <typename T>
concept isConvertibleToFunction = !std::is_base_of_v<Event, std::decay_t<T>> &&
std::convertible_to<T, typename to_function<std::decay_t<T>>::type> &&
std::is_same_v<typename to_function<std::decay_t<T>>::return_type, void>;
...
template <typename T> requires isConvertibleToFunction<T>
Event(T&& lambda)
{
setFunction(std::function{ lambda });
}
C++20에서는 concept를 사용해서 template 제약 조건을 쉽게 작성할 수 있습니다.
1. Event에서 받아들이는 argument의 type T (정확히는 decay로 얻어낸 reference 등이 제거된 타입)가 Event의 파생 클래스가 아닐 것 (복사 생성자와 구분하기 위해서)
2. T가 해당 T를 std::function으로 변환할 수 있을 것
3. 이러한 std::function 버전의 T의 반환 타입이 void일 것
C++17 부터는 CTAD (Class Template Argument Deduction)이 적용되므로
std::function{ lambda } 로 brace initialization을 해도 자동으로 타입 추론이 됩니다.
다만 concept가 C++20 부터 추가되었기 때문에, C++17에서는 using으로 enable_if를 정의하고 사용했습니다.
class Event
{
template <typename T>
using isConvertibleToFunction = std::enable_if_t<!std::is_same_v<Event, std::decay_t<T>> &&
std::is_convertible_v<T, typename to_function<std::decay_t<T>>::type> &&
std::is_same_v<typename to_function<std::decay_t<T>>::return_type, void>>;
...
template <typename T, typename = isConvertibleToFunction<T>>
Event(T&& lambda)
{
setFunction(std::function{ lambda });
}
...
}
보통은 constexpr를 사용합니다만, 여기서는 컴파일 상수 값의 평가 시점의 문제인지
파생 클래스 등에서 복사 생성자를 호출할 때 에러가 발생해서,
온몸 비틀기해서 이렇게 코드를 작성했습니다.
이러고 빌드해서 실행하려고 하니 call 단계에서 문제가 발생했습니다.
SFML 3.0에서 Event 구조가 바뀌면서 Event의 생성자 중에
Event (const TEventSubType& subEvent); 가 생겼는데, 이 때문에
call에서 event type으로 sf::Event나 sf::String이 아닌 객체가 들어오면, 실행 우선순위 때문에
call(sf::Event)가 호출되는 문제가 발생하는 것이었습니다.
현재 구현된 call 함수들을 보면 다음과 같습니다.
void call(sf::Event e);
template <typename... Args>
void call(sf::String e, Args&&... args);
여기서 만약 call("abc");를 호출하게 되면, 여기서 "abc"는 const char*이지만
sf::String으로 명시적으로 변환되지 않기 때문에, 두 함수 중 비 template 함수인 (그래서 우선 순위가 더 높은) call(sf::Event)를 호출하게 됩니다.
그리고 Event의 생성자 중에 TEventSubType을 받는 생성자가 있기 때문에, 이 생성자로 시도합니다.
당연히, const char*은 허용된 TEventSubType이 아니므로 static assert에 걸려서 컴파일 에러가 발생합니다.
따라서 이를 걸러줄 수 있는 (sf::String으로 유도할 수 있는) 템플릿 함수를 정의할 필요가 있습니다.
template <typename T, typename... Args> requires std::convertible_to<T, sf::String>
void call(T&& e, Args&&... args)
{
call(sf::String(e), std::forward<Args>(args)...);
}
C++17이라면 다음처럼 하면 됩니다.
template <typename T, typename... Args>
void call(T&& e, Args&&... args)
{
static_assert(std::is_convertible_v<T, sf::String>, "T should be convertible to sf::String");
if constexpr (std::is_convertible_v<T, sf::String>)
call(sf::String(e), std::forward<Args>(args)...);
}
아니면 방금 전 Event(T&&)에서 처럼 enable_if를 사용해도 상관없습니다.
'C++ > Game' 카테고리의 다른 글
[게임 프레임워크 개발 일지] #18 Clipping Mask (1) | 2024.02.19 |
---|---|
[게임 프레임워크 개발 일지] #17 Window 이것저것 수정하기 (0) | 2024.02.18 |
[게임 프레임워크 개발 일지] #16 일단 마무리 (0) | 2023.08.09 |
[게임 프레임워크 개발 일지] #15 지옥의 Multithread와 메모리 관리 (0) | 2023.07.02 |
[게임 프레임워크 개발 일지] #14 Sound Panning에 관한 삽질 (0) | 2023.06.11 |