Coding Feature.

SFML 게임 제작 공부 #6. Input, Event 처리 (Event Manager). 본문

Game Development/SFML 게임 제작 공부

SFML 게임 제작 공부 #6. Input, Event 처리 (Event Manager).

codingfeature 2023. 9. 10. 14:00

Huge Reference

 

SFML Game Development By Example | Packt

Create and develop exciting games from start to finish using SFML

www.packtpub.com

이번에는 Event Manager 클래스를 만들어서 사용자의 Input Event에 대한 처리를 해결해보고자 한다.

 

우선 SFML의 Event에 대해서 알아보자.

 

Event는 쉽게 말해서 키보드의 어떤 키를 누르거나, 마우스 버튼을 클릭하는 등의 사건을 의미한다. 

 

sf::Event는 C++의 Union이라는 자료구조로 이루어져 있다. Union은 Struct와 유사하지만 모든 멤버 변수가 하나의 메모리 공간을 공유한다는 특징이 있다. 따라서 실제로 멤버 변수를 사용할 때에는 하나만 사용할 수 있다.

 

아래 코드를 보겠다.

sf::Event event;

while(_window.pollEvent(event)){
	if(event.type == sf::Event::Closed){
    	_window.close();
    }
    
    if(event.type == sf::Event::KeyPressed){
    	if(event.key.code == sf::Keyboard::R){
        	std:: cout << "R Pressed!";
        }
    }
}

 

윈도우 개체의 pollEvent 함수에 의해 event에서 현재 활성화된 구조를 가져오게 된다. 그리고 그 event의 타입을 비교해서 그에 맞는 행동을 취하게 된다. (close() 함수 호출 등)

 

위의 코드에서 주의할 점이 있는데, 위 코드와 같이 Keyboard와 같은 input을 event에서 다루는 것은 sf::Keyboard::isKeyPressed(sf::Keyboard::R)과 같이 다루는 것과는 차이가 있다는 것이다.

 

event에서 input을 입력받는 것은 연속적이지 않다. 예를 들어 캐릭터를 움직이기 위해 화살표 키를 꾹 눌렀을 때 event에서 처리하는 경우 그 input을 연속적으로 처리하지 않는다는 얘기이다. 따라서 뚝뚝 끊기는 움직임을 확인할 수 있을 것이다. 반면에 IsKeyPressed로 직접 관리하는 경우에는 연속적이므로 처리하므로 자연스럽게 이동하는 것을 볼 수 있다. 위 두 가지 처리 방식을 꼭 구분지어서 사용해야겠다.

 

위 코드처럼 Input이나 Event를 처리하는 것은 큰 규모의 게임 프로그래밍에서는 적절하지 못하다. 여러 개의 Event를 동시에 처리해야 하는 경우에는 훨씬 복잡해지기 때문이다. 예를 들면 스페이스 바를 누르고 마우스 왼쪽 버튼을 누르면 어떤 함수를 실행시키는 등. 따라서 Event Manager를 만들어서 좀 더 체계적으로 관리해 보기로 한다.

 

Event Manager는 다음과 같은 일을 한다.

1. 여러 키, 버튼에 대한 이벤트들을 조합하고 그 기능을 특정 string으로 묶을 수 있다. (Binding)

2. Binding의 모든 조건이 만족하면 특정 함수를 호출할 수 있다. (Callback)

3. SFML 의 Event Polling에 대해서 그에 맞게 처리할 수 있다.

4. config 파일을 만들어서 Binding들을 저장, 불러오기 등을 할 수 있다.

 

EventManager 클래스의 헤더부터 작성해 본다.

우선 SFML의 Event 타입들에 대해서 enum class 형식으로 선언해 보았다.

enum class EventType {
	KeyDown = sf::Event::KeyPressed,
	KeyUp = sf::Event::KeyReleased,
	MButtonDown = sf::Event::MouseButtonPressed,
	MButtonUp = sf::Event::MouseButtonReleased,
	MouseWheel = sf::Event::MouseWheelMoved,
	WindowResized = sf::Event::Resized,
	GainedFocus = sf::Event::GainedFocus,
	LostFocus = sf::Event::LostFocus,
	MouseEntered = sf::Event::MouseEntered,
	MouseLeft = sf::Event::MouseLeft,
	Closed = sf::Event::Closed,
	TextEntered = sf::Event::TextEntered,
	Keyboard = sf::Event::Count + 1, Mouse, Joystick
};

KeyDown부터 TextEntered는 SFML에 발생할 수 있는 이벤트들이다. 마지막 줄에 sf::Event::Count + 1을 추가한 이유는 마지막 줄 이전까지 새로 입력한 event에 대해서는 모두 적절한 값임을 알리고 처리하기 위해서이다. 마지막 줄 이후의 이벤트들은 enumeration에 의해 sf::Event::EventType의 enum 값을 넘어서게 될 것이다.

 

그리고 이벤트가 발생했을 시 그 이벤트의 자세한 정보(눌려진 키보드의 버튼이 무엇인지 등)를 저장하기 위해 struct를 작성했다.

struct EventInfo {
	EventInfo() { _code = 0; }
	EventInfo(int event) { _code = event; }
	union {
		int _code;
	};
};

 

그리고 이벤트 타입과 이벤트 정보를 pair로 묶어서 벡터로 작성했다.

using Events = std::vector<std::pair<EventType, EventInfo>>;

예를 들어, 키보드의 스페이스 바를 누르는 이벤트에 대해서는 EventType으로 KeyDown이 들어오고, EventInfo에는 sf::Keyboard::Space가 들어올 것이다.

 

참고로 Keyboard와 Mouse 클래스에 대해서 각 버튼 또는 키에 대응되는 Interger 값 레퍼런스는 Keyboard, Mouse 클래스에 선언된 enum Key, enum Button을 참고하면 알 수 있다.

 

SFML 공식 페이지 - Keyboard 클래스

 

sf::Keyboard Class Reference (SFML / Learn / 2.6.0 Documentation)

enum  Key {   Unknown = -1 , A = 0 , B , C ,   D , E , F , G ,   H , I , J , K ,   L , M , N , O ,   P , Q , R , S ,   T , U , V , W ,   X , Y , Z , Num0 ,   Num1 , Num2 , Num3 , Num4 ,   Num5 , Num6 , Num7 , Num8 ,   Num9 , Escape , L

www.sfml-dev.org

SFML 공식 페이지 - Mouse 클래스

 

sf::Mouse Class Reference (SFML / Learn / 2.6.0 Documentation)

Give access to the real-time state of the mouse. More... #include static bool isButtonPressed (Button button)  Check if a mouse button is pressed.   static Vector2i getPosition ()  Get the current position of the mouse in desktop coordinates.   stati

www.sfml-dev.org

 

// #include <SFML/Window/Mouse.hpp>

enum  	Key {
  Unknown = -1 , A = 0 , B , C ,
  D , E , F , G ,
  H , I , J , K ,
  L , M , N , O ,
  P , Q , R , S ,
  T , U , V , W ,
  X , Y , Z , Num0 ,
  Num1 , Num2 , Num3 , Num4 ,
  Num5 , Num6 , Num7 , Num8 ,
  Num9 , Escape , LControl , LShift ,
  LAlt , LSystem , RControl , RShift ,
  RAlt , RSystem , Menu , LBracket ,
  RBracket , Semicolon , Comma , Period ,
  Apostrophe , Slash , Backslash , Grave ,
  Equal , Hyphen , Space , Enter ,
  Backspace , Tab , PageUp , PageDown ,
  End , Home , Insert , Delete ,
  Add , Subtract , Multiply , Divide ,
  Left , Right , Up , Down ,
  Numpad0 , Numpad1 , Numpad2 , Numpad3 ,
  Numpad4 , Numpad5 , Numpad6 , Numpad7 ,
  Numpad8 , Numpad9 , F1 , F2 ,
  F3 , F4 , F5 , F6 ,
  F7 , F8 , F9 , F10 ,
  F11 , F12 , F13 , F14 ,
  F15 , Pause , KeyCount , Tilde = Grave ,
  Dash = Hyphen , BackSpace = Backspace , BackSlash = Backslash , SemiColon = Semicolon ,
  Return = Enter , Quote = Apostrophe
}
// #include <SFML/Window/Mouse.hpp>

enum  	Button {
  Left , Right , Middle , XButton1 ,
  XButton2 , ButtonCount
}

 

그다음 이벤트 정보를 공유하기 위해 다음과 같은 struct를 만들었다.

struct EventDetails {
	EventDetails(const std::string& bindName) : _name(bindName) {
		Clear();
	}
	std::string _name;

	sf::Vector2i _size;
	sf::Uint32 _textEntered;
	sf::Vector2i _mouse;
	int _mouseWheelDelta;
	int _keyCode;

	void Clear() {
		_size = sf::Vector2i(0, 0);
		_textEntered = 0;
		_mouse = sf::Vector2i(0, 0);
		_mouseWheelDelta = 0;
		_keyCode = -1;
	}
};

위 구조체에서는 윈도우 사이즈, 마우스, 마우스 휠, 키보드의 키 코드 등의 디테일한 이벤트 정보들을 담을 수 있다.

 

그다음 Binding 구조체를 작성하였다.

struct Binding {
	Binding(const std::string& name) : _name(name), _details(name), c(0){}
	void BindEvent(EventType type, EventInfo info = EventInfo()) {
		_events.emplace_back(type, info);
	}

	Events _events;
	std::string _name;
	int c;				// 현재 일어나고 있는 이벤트 개수.

	EventDetails _details;
};

Binding 구조체에서는 EventType과 EventInfo가 저장된 Events 벡터, Binding의 이름, 그리고 현재 일어나고 있는 이벤트의 개수를 담는 변수 c, 그리고 EventDetails를 담고 있다.

 

c를 사용하는 이유는 만약 Binding에 등록된 이벤트가 두 개 이상일 경우 c, 즉 현재 발생한 이벤트 개수를 확인함으로써 이벤트가 모두 일어나고 있는 상황인지 조건을 확인할 수 있기 때문이다.

 

BindEvent라는 함수도 있는데, 이를 통해서 Events 벡터에 이벤트를 바인딩시킨다. 결국 Binding 구조체는 BindEvent 함수를 통해 여러 개의 Event들을 모두 바인딩시킬 수 있는 것이다.

 

다음 상황을 예로 들어보자.

어떤 캐릭터가 오른쪽으로 달리기를 하는 기능을 구현하기 위해 "Sprint_Right"라는 이름의 Binding을 만들고 싶다고 해보자. 그러면 아래와 같이 키보드의 왼쪽 쉬프트 키와 오른쪽 화살표 키를 누르는 이벤트를 BindEvent 함수를 통해 바인딩 함으로써 구현해 볼 수 있을 것이다.

Binding bind1("Sprint_Right");

bind1.BindEvent("KeyDown", sf::Keyboard::LShift);
bind1.BindEvent("KeyDown", sf::Keyboard::Right);

 

위 Binding 구조체를 저장하고 관리하기 위해 아래와 같이 unordered map 자료 구조를 사용했다.

using Bindings = std::unordered_map<std::string, Binding*>;

 

어떤 특정 이벤트들을 Binding 하는 것까지 완료했다. 이제 이 Binding의 조건이 모두 만족이 되면 어떤 함수를 호출하는 시스템을 구현해야 한다.

 

여기에서 Callback 개념이 사용된다.

 

콜백이란 어떤 특정 코드가 다른 코드의 인자로 넘어가서 적절한 시간에 수행되는 것을 의미한다. Event Manager에서 콜백을 활용할 수 있다.

 

만약 스페이스바를 누르면 Jump()라는 콜백함수를 호출해서 캐릭터를 점프하도록 하는 기능을 구현해 본다고 하자.

 

1. Jump라는 이름을 가진 Binding을 만들고,

2. 그 Binding에 이벤트 타입 KeyDown과 키코드 sf::Keyboard::Space를 넣은 뒤에,

3. Jump()를 이전 Binding과 엮어서 Event Manager에서 호출하도록 한다.

 

위와 같은 기능을 Callback 함수를 통해서 구현하기 위해 std::function을 이용한다. functional이라는 라이브러리에 선언되어 있다.

 

std::function은 c++11부터 추가된 기능으로 함수 포인터 기능을 대체할 수 있다.

간단한 예를 들어보자면,

void PrintA(int A)
{
	std::cout << A << "\n";
}

std::function<void(int)> func = PrintA;

PrintA(50);	// 50 출력
func(50);	// 50 출력

위와 같이 어떤 function의 원형(return 타입 void, 인자 타입 int)을 변수로 선언하고 함수를 입력받아 사용할 수 있게 된다.

 

std::bind 역시 살펴보면 좋은데 이는 함수의 인자를 미리 지정할 수 있는 것이다.

void PrintAddition(int a, int b)
{
	int sum = a + b;
	std::cout << sum << "\n";
}

auto func = std::bind(PrintAddition, 30, std::placeholders::_1);

PrintAddition(30, 70);	// 100 출력.
func(70);				// 100 출력.

bind에서 함수 이름을 먼저 받고 첫 번째 인자, 즉 a에는 30, 그리고 두번째 인자, 즉 b에는 func를 호출할 때 들어오는 첫번째 인자를 넣어준다는 의미의 placeholder 1을  받게 된다. 여기서 func는 auto로 선언해 준다.

 

using Callbacks = std::unordered_map<std::string, std::function<void(EventDetails*)>>;

Callbacks라는 unordered map에서 value 값으로 return 값이 void이고 인자로 EventDetails* 를 받는 콜백 함수를 저장할 수 있게 된다. 이렇게 콜백 함수를 지정하고 관리할 수 있게 되었다.

 

 

 

이제 Binding과 Callback 기능을 수행할 자료구조를 모두 만들었기 때문에 본격적으로 Event Manager 클래스를 작성해 보도록 한다.

 

class EventManager
{
	void LoadBinding();

	Bindings _bindings;
	Callbacks _callbacks;
	bool _hasFocus;

public:
	EventManager();
	~EventManager();

	bool AddBinding(Binding* binding);		
	bool RemoveBinding(std::string name);	
	void SetFocus(const bool& focus);

	template<class T>
	bool AddCallback(const std::string& name, void(T::* func)(EventDetails*), T* instance) { // 헤더에 정의.
		auto temp = std::bind(func, instance, std::placeholders::_1);
		return _callbacks.emplace(name, temp).second;
	}

	void RemoveCallback(const std::string& name) { // 헤더에 정의.
		_callbacks.erase(name);
	}

	void HandleEvent(sf::Event& event);
	void Update();

	sf::Vector2i GetMousePos(sf::RenderWindow* wind = nullptr) {
		return (wind ? sf::Mouse::getPosition(*wind) : sf::Mouse::getPosition());
	}
};

위에서 AddCallback 메서드는 template 때문에 헤더에 정의해 주었다.

AddCallback은 인자로 콜백 함수의 이름과 콜백함수로 사용할 함수를 입력받고 callbacks map에 저장하게 된다.

 

먼저 생성자, 소멸자를 작성해 주었다.

EventManager::EventManager() : _hasFocus(true) { LoadBinding(); }

EventManager::~EventManager() {
	for (auto &itr : _bindings) {
		delete itr.second;
		itr.second = nullptr;
	}
}

 

생성자에서 실행되는 LoadBinding 메서드는 config 파일로부터 Binding 내용을 불러오는 일을 할 것이다.

소멸자는 Bindings에 저장된 모든 binding의 할당된 공간을 해제한다.

 

bool EventManager::AddBinding(Binding *binding) {
	if (_bindings.find(binding->_name) != _bindings.end())
		return false;

	return _bindings.emplace(binding->_name, binding).second;
}


bool EventManager::RemoveBinding(std::string name) {
	auto itr = _bindings.find(name);

	if (itr == _bindings.end()) { return false; }

	delete itr->second;
	_bindings.erase(itr);
	return true;
}

AddBinding 매서드는 binding 포인터를 인자로 받고 이미 bindings에 저장되어 있지 않다면 추가하도록 해준다.

RemoveBinding의 경우, 먼저 Binding의 이름을 인자로 받는다. 그리고 그 이름을 가진 binding이 없다면 false를 반환한다. 만약 있다면 delete를 통해 메모리 할당을 해제하고 true를 반환한다.

 

 

void EventManager::HandleEvent(sf::Event& event) {
	// SFML 이벤트 관리.

	//bindings의 모든 binding을 탐색.
	for (auto& b_itr : _bindings) {
		Binding* bind = b_itr.second;

		// 각 binding의 모든 event를 탐색.
		for (auto& e_itr : bind->_events) {
			EventType sfmlEvent = (EventType)event.type;

			if (e_itr.first != sfmlEvent) { continue; }

			// event의 타입(sfmlEvent)이 키보드 이벤트와 일치하는지 확인.
			if (sfmlEvent == EventType::KeyDown || sfmlEvent == EventType::KeyUp) {

				// 일치하는 경우 binding의 키코드와 event의 키코드와 일치하는지 확인.
				if (e_itr.second._code == event.key.code) {
					// keycode의 일치.
					// C count 증가.
					if (bind->_details._keyCode != -1) {
						bind->_details._keyCode = e_itr.second._code;
					}

					++(bind->c);

					break;
				}
			}
			// event의 타입(sfmlEvent)이 마우스 이벤트와 일치하는지 확인.
			else if (sfmlEvent == EventType::MButtonDown || sfmlEvent == EventType::MButtonUp) {
				// 일치하는 경우 binding의 키코드와 event의 키코드와 일치하는지 확인.
				if (e_itr.second._code == event.mouseButton.button) {
					// keycode의 일치.
					// C count 증가.
					bind->_details._mouse.x = event.mouseButton.x;
					bind->_details._mouse.y = event.mouseButton.y;
					if (bind->_details._keyCode != -1) {
						bind->_details._keyCode = e_itr.second._code;
					}
					++(bind->c);
					break;
				}
			}
			else {
				if (sfmlEvent == EventType::MouseWheel) {
					bind->_details._mouseWheelDelta = event.mouseWheel.delta;
				}
				else if (sfmlEvent == EventType::WindowResized) {
					bind->_details._size.x = event.size.width;
					bind->_details._size.y = event.size.height;
				}
				else if (sfmlEvent == EventType::TextEntered) {
					bind->_details._textEntered = event.text.unicode;
				}
				++(bind->c);
			}
		}
	}
}

HandleEvent 매서드는 window의 pollEvent를 통해 받은 event를 가지고 미리 지정된 binding을 탐색하게 된다.

 

iterator를 가지고 각 Binding을, 그리고 그 Binding 속에서 각 이벤트를 탐색하게 된다.

우선 pollEvent로 받은 event(여기서는 sfmlEvent라는 변수로 나타냄)의 타입을 각 이벤트의 타입과 비교해 본다. 타입이 같다면 그 타입에 대한 Keycode를 비교한다. Keycode 또한 같다면 바인드의 이벤트 detail 구조체의 멤버 변수를 그에 맞게 설정하고 c 또한 1을 증가시킨다.

 

즉 다음과 같이 탐색하게 된다.

각 Binding -> 각 Event -> 각 Event 타입 ->각 Event Keycode

위 탐색을 통해 마지막까지 pollEvent의 event가 일치한다면 그 내용을 그 바인딩의 EventDetail 구조체에 갱신하고 c를 1 증가시키게 되는 것이다.

 

 

 

위에서는 Event에 대해서 바인딩된 이벤트를 탐색했다면 이제 Update 매서드에서는 "isButtonPressed"와 같은 실시간 Input에 대해서 Binding을 탐색하게 될 것이다.

// 실시간 input 확인 및 Binding 업데이트.
void EventManager::Update() {
	if (!_hasFocus) { return; }

	for (auto& b_itr : _bindings) {
		Binding* bind = b_itr.second;

		// 실시간 마우스, 키보드, 조이스틱 input 확인.
		for (auto& e_itr : bind->_events) {
			switch (e_itr.first) {
			case(EventType::Keyboard):
				if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key(e_itr.second._code))) {
					if (bind->_details._keyCode != -1) {
						bind->_details._keyCode = e_itr.second._code;
					}
					++(bind->c);
				}
				break;
			case(EventType::Mouse):
				if (sf::Mouse::isButtonPressed(sf::Mouse::Button(e_itr.second._code))) {
					if (bind->_details._keyCode != -1) {
						bind->_details._keyCode = e_itr.second._code;
					}
					++(bind->c);
				}
				break;
			case(EventType::Joystick):
				// 추후에 추가.
				break;
			}
		}

		// Binding의 이벤트 개수와 실제로 Binding에서 발생한 이벤트 개수가 같은지 확인. (즉, Binding된 모든 이벤트가 발생하였는지 확인)
		if (bind->_events.size() == bind->c) {
			auto callItr = _callbacks.find(bind->_name);
			if (callItr != _callbacks.end())
			{
				// 콜백 함수 실행.
				callItr->second(&bind->_details);
			} 
		}
		bind->c = 0;
		bind->_details.Clear();
	}
}

이번에도 HandleEvent와 비슷한 방식으로 iterator를 통해 각 Binding을, 그리고 각 Event를 탐색하게 한다.

switch 문을 사용해 현재 탐색 중인 Event에 대해서 먼저 Type을 확인한다. 그리고 키보드, 마우스, 또는 조이스틱 이벤트 타입에 대해서 실시간으로 input을 확인하게 된다. Keyboard, Mouse, Joystick은 앞서 헤더파일의 EventType에서 SFML과 무관하게 임의로 지정한 타입들이다. 따라서 실시간 Input에 대한 바인딩을 하고 싶다면 Keyboard, Mouse, Joystick과 같은 키워드를 사용하면 될 것이다.

 

그리고 나머지는 HandleEvent와 같은 방식으로 Event의 detail을 갱신하고 c를 +1 한다.

 

마지막으로 각 Binding에 대해서 바인딩에 할당된 c 값을 바인딩된 이벤트 개수와 비교를 하는데, 이 두 수가 같다는 것은 Binding 된 이벤트가 모두 발생했다는 뜻이므로 그에 해당하는 Callback 함수를 호출하게 된다.

 

그리고 c를 다시 0으로 초기화시키고 그 바인드의 Event Detail를 초기화시킨다.

 

 

그다음 바인딩에 대한 설정 파일을 읽어서 불러오는 LoadBinding 메서드를 작성해 보았다.

설정 파일 Keys.cfg에 대한 포맷은 다음과 같다.

Window_close 0:0
Fullscreen_toggle 5:89
Move 9:0 24:38

각 줄마다 Binding 하나를 의미한다. 먼저 첫 단어는 Binding의 이름이다. 그 뒤로부터는 "(이벤트 타입 enum):(Keycode)" 형식으로 입력이 된다.

 

예를 들어 Fullscreen_toggle이라는 Binding은 5라는 이벤트 타입 enum과 89라는 Keycode를 받게 된다. 5는 sf::Event::KeyPressed, 또는 Keydown 타입이며 89는 'F5'의 Keycode이다. 즉 Fullscreen_toggle이라는 바인딩을 활성화하려면 Keydown 이벤트 타입이면서 Keycode는 'F5'이어야 한다는 의미이다.

 

void EventManager::LoadBinding() {
	std::string delimiter = ":";

	std::ifstream bindings;
	bindings.open("keys.cfg");
	if (!bindings.is_open()) {
		std::cout << "CANNOT OPEN keys.cfg." << '\n';
		return;
	}

	std::string line;

	while (std::getline(bindings, line)) {
		std::stringstream keystream(line);
		std::string callbackName;
		keystream >> callbackName;
		Binding* bind = new Binding(callbackName);

		while (!keystream.eof()) {
			std::string keyval;
			keystream >> keyval;
			int start = 0;
			int end = keyval.find(delimiter);
			if (end == std::string::npos) {
				delete bind;
				bind = nullptr;
				break;
			}

			EventType type = EventType(stoi(keyval.substr(start, end - start)));

			int code = stoi(keyval.substr(end + delimiter.length(), keyval.find(delimiter, end + delimiter.length())));
			EventInfo eventInfo;
			eventInfo._code = code;

			bind->BindEvent(type, eventInfo);
		}

		if (!AddBinding(bind)) {delete bind; }
		bind = nullptr;
	}
	bindings.close();
}

위 코드를 통해 cfg 파일에 대한 내용을 바인딩 형식에 맞게 저장하도록 한다.

 

마지막으로 SetFocus 메서드를 다음과 같이 정의해 주었다.

void EventManager::SetFocus(const bool& focus) {
	_hasFocus = focus;
}

 

이로써 EventManager 클래스를 모두 작성했다.

이제 이를 Window 클래스 내에서 직접 다루어보도록 한다.

 

이전에 작성했던 Window 클래스의 헤더에 다음과 같은 내용을 추가했다.

// Window.hpp
#pragma once
#include <SFML/Graphics.hpp>
#include "EventManager.hpp"

class Window {
	sf::RenderWindow _window;
	sf::Vector2u _windowSize;
	std::string _windowTitle;

	EventManager _eventManager;	// 추가

	bool _isDone;
	bool _isFullScreen;
	bool _isFocused;			// 추가

	void Setup(const std::string& l_title, const sf::Vector2u& l_size);
	void Destroy();
	void Create();

public:
	Window();
	Window(const std::string& l_title, const sf::Vector2u& l_size);
	~Window();

	void BeginDraw();						
	void EndDraw();

	void Update();		

	bool IsDone();
	bool IsFullScreen();
	sf::Vector2u GetWindowSize();

	void ToggleFullScreen(EventDetails* details);	// 기존 매서드 인자에 이벤트 디테일 추가.

	void Draw(sf::Drawable& l_drawable);

	sf::RenderWindow& GetWindow();

	bool IsFocused();								// 추가.
	EventManager* GetEventManager();				// 추가.
	void Close(EventDetails* details = nullptr);	// 추가.
	
};

 

먼저 새롭게 추가한 매써드 Close는 단순히 윈도우를 끌 때 사용하는 것이므로 _isDone을 true로 설정하는 것만 해주었다.

void Window::Close(EventDetails* details) { _isDone = true; }

 

그리고 Window 클래스의 매서드인 Update를 다음과 같이 수정해 주었다.

void Window::Update() {
	sf::Event event;
	while (_window.pollEvent(event)) {
		if (event.type == sf::Event::LostFocus) {
			_isFocused = false;
			_eventManager.SetFocus(false);
		}
		else if (event.type == sf::Event::GainedFocus) {
			_isFocused = true;
			_eventManager.SetFocus(true);
		}
		_eventManager.HandleEvent(event);
	}
	_eventManager.Update();
}

우선 pollEvent를 통해 event를 받으면 이벤트 타입이 LostFocus 또는 GainedFocus인가에 따라서 Event Manager에 그에 맞게 private 변수인 _hasFocus를 설정하도록 해주었다. 그리고 Event에 대한 처리로 HandleEvent 메서드를, 그리고 실시간 Input에 대한 처리로 Update 매서드를 추가해 주었다.

 

그다음 Window 클래스의 매서드인 ToggleFullScreen와 Close를 콜백함수로 사용하기 위해 다음과 같이 작업해 보았다.

1. keys.cfg에 바인딩을 설정한다.

2. 콜백함수로 등록한다.

 

1. keys.cfg에 바인딩을 설정한다.

Window_close 0:0
Fullscreen_toggle 5:89

다음과 같이 keys.cfg 파일에 작성한다. Window_close는 앞서 얘기한 것처럼 Binding의 이름이고 뒤의 0:0은 각각 sf::Event::Closed라는 이벤트 타입, 그리고 윈도우 종료 시 Keycode를 따로 넣지 않으므로 0으로 설정했다.

Fullscreen_toggle 역시 Binding의 이름이고 5:89는 각각 Keydown 타입, 'F5' 키코드를 의미한다.

 

2. 콜백함수로 등록한다.

void Window::Setup(const std::string& l_title, const sf::Vector2u& l_size) {
	_windowTitle = l_title;
	_windowSize = l_size;
	_isFullScreen = false;
	_isDone = false;
	Create();

	_isFocused = true;
	
	_eventManager.AddCallback("Fullscreen_toggle", &Window::ToggleFullScreen, this);
	_eventManager.AddCallback("Window_close", &Window::Close, this);
}

콜백 함수로 등록하기 위해서 Window 매서드인 Setup 내에서 AddCallback 매서드를 사용하였다. AddCallback 함수의 첫 번째 인자로는 keys.cfg에 정해놓은 Binding 이름이 들어가고 그 위에는 클래스 매서드의 이름과 현재 인스턴스(여기에서는 window)인 this를 입력한다. 이로써 콜백함수 등록도 끝이 났다.

 

실제로 F5를 누르거나 윈도우 창 끄기 버튼을 누르면 잘 작동하는 것을 확인할 수 있었다.

 

 

전체적으로 Event Manager와 Binding, 콜백 함수 등이 동작하는 것이 복잡한 것 같아서 전체화면 토글 기능에 대해서 집중하면서 Flow를 이해해 보기로 했다.

 

전체화면 토글 기능(F5 버튼)의 Flow.

1. 

void Window::Setup(const std::string& l_title, const sf::Vector2u& l_size) {
    ...
    
	_eventManager.AddCallback("Fullscreen_toggle", &Window::ToggleFullScreen, this);
}

Window 클래스의 Setup 매서드에서 "Fullscreen_toggle" Binding 이름과 콜백 함수로 사용될 ToggleFullScreen 함수가 AddCallback의 인자로 들어간다.

 

2. 

class EventManager
{
...
public:
...

	template<class T>
	bool AddCallback(const std::string& name, void(T::* func)(EventDetails*), T* instance) {
		auto temp = std::bind(func, instance, std::placeholders::_1);
		return _callbacks.emplace(name, temp).second;
	}
...
};

EventManager의 헤더에 정의된 AddCallback 매서드에서 bind 함수를 통해 _callbacks라는 unordered map에 Key: binding 이름, Value: 함수 가 저장된다.

using Callbacks = std::unordered_map<std::string, std::function<void(EventDetails*)>>;

이로써 "Fullscreen_toggle" Binding 이름과 그에 대응되는 함수 ToggleFullScreen이 콜백 함수로 등록된다.

 

3. 

void EventManager::LoadBinding() {
	...
    
	while (std::getline(bindings, line)) {
		...
        
		Binding* bind = new Binding(callbackName);
        // 새로운 binding 생성. "Fullscreen_toggle"

		while (!keystream.eof()) {
		...
        
			bind->BindEvent(type, eventInfo);
            // 새롭게 생성된 binding에 이벤트 타입(5)과 keycode(89)를 추가.
		}
        	if (!AddBinding(bind)) {delete bind; }
		...
}

keys.cfg를 EventManager 클래스의 LoadBinding 매서드를 통해 읽는다. (LoadBinding은 EventManager 클래스의 생성자에 있다. ) Binding의 이름과 이벤트 타입, keycode를 읽어서 새로운 binding을 생성하고 AddBinding 매서드를 호출한다.

 

struct Binding {
	Binding(const std::string& name) : _name(name), _details(name), c(0){}
	void BindEvent(EventType type, EventInfo info = EventInfo()) {
		_events.emplace_back(type, info);
	}

	Events _events;		// (5:89) 이벤트 타입과 키코드 등록됨.
	std::string _name;	//"Fullscreen_toggle"
	int c;			// 0으로 설정.

	EventDetails _details;
};

 

4. 

bool EventManager::AddBinding(Binding *binding) {
	if (_bindings.find(binding->_name) != _bindings.end())
		return false;

	return _bindings.emplace(binding->_name, binding).second;
}

AddBinding 매서드 내에서  _bindings에  <Key: Binding 이름, Value: Binding 포인터> unordered map 형식으로 저장된다.

using Bindings = std::unordered_map<std::string, Binding*>;

이로써 Binding에 대한 등록이 완료된다.

 

5.

void Game::Update() {
	_window.Update(); 
	// 게임 내 요소 업데이트 및 이벤트 관리.
}

Game 클래스의 Update 매서드에 의해 윈도우 객체의 Update 매서드가 호출된다.

 

6.

void Window::Update() {
	sf::Event event;
	while (_window.pollEvent(event)) {
		if (event.type == sf::Event::LostFocus) {
			_isFocused = false;
			_eventManager.SetFocus(false);
		}
		else if (event.type == sf::Event::GainedFocus) {
			_isFocused = true;
			_eventManager.SetFocus(true);
		}
		_eventManager.HandleEvent(event);
	}
	_eventManager.Update();
}

Update 매서드에서 window 객체의 pollEvent 매서드로 event가 생성되고 이 event는 HandleEvent의 인자로 들어가게 된다.

 

7. 

void EventManager::HandleEvent(sf::Event& event) {
    
	for (auto& b_itr : _bindings) {
		Binding* bind = b_itr.second;
        // _bindings에서 "Fullscreen_toggle"에 대한 Binding 탐색.

		for (auto& e_itr : bind->_events) {
        // "Fullscreen_toggle" Binding에서 (5:89) 이벤트 탐색.
			EventType sfmlEvent = (EventType)event.type;

			// pollEvent에서 가져온 event가 KeyDown 인지 확인.
			if (sfmlEvent == EventType::KeyDown || sfmlEvent == EventType::KeyUp) {
				// pollEvent에서 가져온 event가 'F5'인지 확인.
				if (e_itr.second._code == event.key.code) {
                	// 'F5' 이므로 "Fullscreen_toggle" Binding의 detail 키코드 갱신.
					if (bind->_details._keyCode != -1) {
						bind->_details._keyCode = e_itr.second._code;
					}
					
                    //"Fullscreen_toggle" Binding의 c에 +1. 
					++(bind->c); // 현재 c는 1

					break;

...

}

HandleEvent 매서드에서 Fullscreen_toggle Binding의 이벤트와 pollEvent의 이벤트와 비교. 같으므로 그 Binding의 detail에 Keycode 내용을 갱신하고 c에 +1.

 

8.

EventManager 클래스의 Update 매서드가 호출됨.

void EventManager::Update() {

	for (auto& b_itr : _bindings) {
		Binding* bind = b_itr.second;
        // "Windowscreen_toggle" Binding 탐색.

...

		// "Windowscreen_toggle" Binding의 이벤트 개수와 c 값을 비교. 둘다 1.
		if (bind->_events.size() == bind->c) {
			auto callItr = _callbacks.find(bind->_name);
            // "Windowscreen_toggle" 이름으로 등록된 콜백 함수를 가져옴.
			if (callItr != _callbacks.end())
			{
				// "Windowscreen_toggle" 이름으로 등록된 콜백 함수 ToggleFullScreen 호출.
				callItr->second(&bind->_details);
			} 
		}
		bind->c = 0;
		bind->_details.Clear();
	}
}

Update 매서드의 마지막 부분에서 "Windowscreen_toggle" Binding에 저장된 이벤트 개수와 실제로 발생한 이벤트 개수(c)를 비교한다. 둘다 1이므로 "Windowscreen_toggle"이라는 이름으로 등록된 callback 함수를 찾아서 호출한다.

최종적으로 ToggleFullScreen 매서드가 호출이 된다.

 

 

다음에는 게임의 여러 상태(게임 메뉴, 게임 중, 일시정지, 게임 종료 등)를 다루는 State에 대해 공부해보기로 한다.