GLFW事件处理的包装

GLFW是个用于管理窗口的库,这是前提,用户在对这个窗口干的任何事都会被GLFW捕获,然后GLFW会调用用户指定的函数处理事件。
比如说窗口改变大小,用户按了个关闭按钮,用户点了下鼠标......

传统的方式通常是把所有的事件硬编码到一个lambda里然后传给GLFW,但这样子做就是放弃了未来的拓展。
所以对GLFW的事件处理做包装是非常非常必要的!
当这个累活落在我头上时,我选择先参考一些小型Game Engine的处理方式,以Hazel和Overload为例。

Hazel的事件处理

在 Hazel/src/Platform/Windows/WindowsWindow.h

用一个绑定了当前类实例的成员函数当作lambda传入callback中,对于GLFW每个不同的事件设置callback,在callback内部都会产生相应的Event类,然后传入这个成员函数里。

很绕口,但是往下看会有详细例子的...

这个成员函数通过EventDispatcher 把事件分发到当前层针对不同事件的callback中,再对其底下所有不同的Layer传递这个event,不同的Layer中再分别执行其OnEvent函数,然后再传给layer中不同的对象,十分聪明的做法。

事件派发的方法

在类EventDispatcher中,其构造函数接受一个Event对象的引用,这保证了它的..额..多态,这点很重要!
接着是分发的主要逻辑,其会把输入传给参数里的lambda,然后执行对Event的处理

template<typename T, typename F>
bool Dispatch(const F& func)
{
	if (m_Event.GetEventType() == T::GetStaticType())
	{
		m_Event.Handled |= func(static_cast<T&>(m_Event));
		return true;
	}
	return false;
}

显然编译器会推断出func的类型,这不重要,但编译器无法自动把父类Event对象转换到func需要的子类Event参数上,于是类型F得显示声明。
在进行转换之前,需要对比func需要的Event与参数Event类型是否相同,如果不同就会直接返回。

其实这里可以直接动态转换吧....或许动态转换开销比字符串比较还大..?还是说动态转换抛出异常的开销太大了....?我不知道哇..

目光移回顶层,最先得到Event的函数Application::OnEvent
当GLFW得到了一个事件,比如移动鼠标键盘按键,他首先会调用glfwxxxxcallback,这些callback函数接受一个lambda,lambda的第一个参数永远是GLFWwindow* window,而在GLFWwindow内部有一个可以供用户自定义的指针,用函数glfwSetWindowUserPointer(GLFWwindow*,void*)即可给指定window的指针赋值。这个指针指向你自己的数据啦...所谓的任意类型用户指针
在Hazel的WindowsWindow.h的类WindowsWindow里,事实上!有这么一个结构体

struct WindowData
{
	std::string Title;
	unsigned int Width, Height;
	bool VSync;
	EventCallbackFn EventCallback;
};

在WindowsWindow初始化的时候,这个结构体的地址就作为用户指针被传入了GLFWwindow中了....
而WindowData中的EventCallback成员则是一个被Bind了当前类指针的成员函数,也就是Application::onEvent(Event& e)
这步在Application的构造函数中完成

m_Window->SetEventCallback(HZ_BIND_EVENT_FN(Application::OnEvent));   
//HZ_BIND_EVENT_FN(x) std::bind(x,std::placeholders::_1)

还没完,在WindowsWindow的构造函数中,还为GLFW的每个事件回调函数进行了设置,这些回调函数的主要用途就是针对不同的GLFW事件生成不同的Event,然后传入WindowData::EventCallback,事实上也就是传入了Application::onEvent(Event& e)
例如

glfwSetWindowSizeCallback(m_Window, [](GLFWwindow* window, int width, int height)
{
	WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window);
	data.Width = width;
	data.Height = height;
	WindowResizeEvent event(width, height);
	data.EventCallback(event);
    //这个data.EventCallback就是Application::OnEvent啦
});

这些Event会被当前Application的不同事件处理函数处理,接着Application::OnEvent派发到其子Layer,子Layer又会把事件派发到更下层的对象,在派发的过程中,又被处理...
具体的来说

void Application::OnEvent(Event& e)
{
	HZ_PROFILE_FUNCTION();

	EventDispatcher dispatcher(e);
	//给当前的对象分发事件
	dispatcher.Dispatch<WindowCloseEvent>(HZ_BIND_EVENT_FN(Application::OnWindowClose));
	dispatcher.Dispatch<WindowResizeEvent>(HZ_BIND_EVENT_FN(Application::OnWindowResize));

	//给下层的对象分发事件
	//注意事从后往前,因为渲染的顺序事从前往后,那么最后面的层一般事UI层   
	//我们一般希望UI会先捕获事件    
	for (auto it = m_LayerStack.rbegin(); it != m_LayerStack.rend(); ++it)
	{
		if (e.Handled) 
			break;
		(*it)->OnEvent(e);
	}
}   

我好像把一件事讲了两次,不过不要紧,回头再来8
It's not a loop, it's a sprial

这种方式保证了窗口不知道也不用关心上层应用,而上层应用有能力知道并且处理窗口中的事件ヾ( ̄▽ ̄)

Overload对事件的处理

Overload对事件的处理事另一种奇妙的方式

在OvWindowing中可以找到相关代码....emm
首先,其对GLFWwindow的包装类,Window中维护了一个静态的字段static std::unordered_map<GLFWwindow*, Window*> __WINDOWS_MAP;,每当一个窗口被创建,就会往里面填入被创建的这个窗口类..
还有个静态函数static Window* FindInstance(GLFWwindow* p_glfwWindow);,只要给出了GLFWwindow*,就能在这个map中拿到对应的GLFWwindow包装类。

重点来了!
在前文提到过GLFW的事件回调函数中的第一个参数永远是GLFWwindow*,Overload定义的回调函数则干了这么件事,首先调用FindInstance找到这个指针对应的窗口,然后把事件分发给这个窗口。

例如在绑定回调函数中:

void OvWindowing::Window::BindKeyCallback() const
{
	auto keyCallback = [](GLFWwindow* p_window, int p_key, int p_scancode, int p_action, int p_mods)
	{
		Window* windowInstance = FindInstance(p_window);

		if (windowInstance)
		{
			if (p_action == GLFW_PRESS)
				windowInstance->KeyPressedEvent.Invoke(p_key);

			if (p_action == GLFW_RELEASE)
				windowInstance->KeyReleasedEvent.Invoke(p_key);
		}
	};

	glfwSetKeyCallback(m_glfwWindow, keyCallback);
}

那么这个KeyPressedEvent是什么?在OvTools/Eventing /Event.h中我们能找到答案...

KeyPressedEvent是

template<class... ArgTypes> class Event;

的实例啦,那么模板中的可变参的目的事?

答案事回调函数的参数,在其内部有如下定义:
using Callback = std::function<void(ArgTypes...)>;
而为这个事件添加回调函数的方法事
ListenerID AddListener(Callback p_callback);
放入其中的函数会被插入到其内部的成员
std::unordered_map<ListenerID, Callback> m_callbacks;中,而函数的返回值ListenerID是一个从0开始自增的uint64,作为对应回调函数的id,如果还想移除该函数就保存,反之随它去吧。
如果要执行所有附着在该事件上的回调函数,则调用void Invoke(ArgTypes... p_args);,其具体代码也就是遍历上面的map,然后传入参数p_args.....这个函数一般在事件被触发时调用
在class Window中,定义了数个Event变量,任何拿到窗口的对象都可以往里面加点回调事件。
不过最好还是再Wrap一层,比如OvWindowing::Inputs::InputManager就是对输入再包装。

总结

Overload的做法很简单,而且可拓展性明显更高,技巧性也不是那么强...但缺点是它的应用上层应用恐怕得和窗口绑定的深一点..也没法做到分层分顺序分发事件..这也是Hazel的优点...

当然Hazel的按层传播最大的优点(最最大!!!)如果一个事件在某层被解决了,可以防止它继续向下传播,试想一个FPS游戏,你按ESC打开菜单,然后点了下设置....彭,枪响了...尴尬,但如果事件在UI层被解决了,那么就不会往Gameplay层传播,也就没这事啦
我要把两者结合一下下...emmm

♪(´▽`)



滚石!
这专的封面真的神奇owo

END