GLFW是个用于管理窗口的库,这是前提,用户在对这个窗口干的任何事都会被GLFW捕获,然后GLFW会调用用户指定的函数处理事件。
比如说窗口改变大小,用户按了个关闭按钮,用户点了下鼠标......
传统的方式通常是把所有的事件硬编码到一个lambda里然后传给GLFW,但这样子做就是放弃了未来的拓展。
所以对GLFW的事件处理做包装是非常非常必要的!
当这个累活落在我头上时,我选择先参考一些小型Game Engine的处理方式,以Hazel和Overload为例。
在 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对事件的处理事另一种奇妙的方式
在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