窗口抽象与GLFW
前期的修改
我们将会使用一个修改过的GLFW的库:https://github.com/TheCherno/glfw
在cmd中直接:git submodule add https://github.com/TheCherno/glfw Hazel/vendor/GLFW
下载好该仓库。
随后我们编辑Hazel文件夹下的premake5.lua
......
outputdir = "%{cfg.buildcfg}-%{cfg.system}-%{cfg.architecture}"
//从这里开始修改
-- Include directories relative to root folder (solution directory)
IncludeDir = {}
IncludeDir["GLFW"] = "Hazel/vendor/GLFW/include"
include "Hazle/vendor/GLFW"
......
includedirs
{
"%{prj.name}/src",
"%{prj.name}/vendor/spdlog/include",
"%{IncludeDir.GLFW}"
}
links
{
"GLFW",
"opengl32.lib"
}
一个是负责添加GLFW的目录。另外下面这个include则是因为该目录下也有一个premake5.lua,这样就能两个一起都被使用了。
并且在includedirs加上这个目录,然后Hazel需要连接到GLFW中。
此时我们回到桌面,重新构建该项目(用bat)。

我们使用的premake版本太旧了,要换个新的。

换了个新版本果然好了(尝试将版本构建直接改成vs2022试试看?)

此时回到Hazel,会发现这里已经引用了GLFW。尝试Build一下GLFW,应该不会有报错。
构建窗口类等
先给Core.h加上这样一段代码:
Core.h
......
#ifdef HZ_ENABLE_ASSERTS
#define HZ_ASSERTS(x, ...) { if(!(x)) { HZ_ERROR("Assertion Failed: {0}", __VA_ARGS__); __debugbreak(); } }
#define HZ_CORE_ASSERT(x, ...) {if(!(x)) { HZ_CORE_ERROR("Assertion Failed: {0}", __VA_ARGS); __debugbreak(; )}}
#else
#define HZ_ASSERT(x, ...)
#define HZ_CORE_ASSERT(x, ...)
#endif
这段代码的用处是写了一个条件编译的断言宏定义,用于在调试Debug模式下捕获程序中的逻辑错误,而在发布Release模式则完全移除断言检查,避免性能开销。
WindowsWindow.h
#pragma once
#include "hzpch.h"
#include "Hazel/Core.h"
#include "Hazel/Events/Event.h"
namespace Hazel {
struct WindowProps
{
std::string Title;
unsigned int Width;
unsigned int Height;
WindowProps(const std::string& title = "Hazel Engine",
unsigned int width = 1280,
unsigned int height = 720)
: Title(title), Width(width), Height(height)
{
}
};
class HAZEL_API Window
{
public:
using EventCallbackFn = std::function<void(Event&)>;
virtual ~Window() {}
virtual void OnUpdate() = 0;
virtual unsigned int GetWidth() const = 0;
virtual unsigned int GetHeight() const = 0;
virtual void SetEventCallback(const EventCallbackFn& callback) = 0;
virtual void SetVSync(bool enabled) = 0;
virtual bool IsVSync() const = 0;
static Window* Create(const WindowProps& props = WindowProps());
};
}
这是一个为窗口准备的Windows.h,这段代码是Hazel引擎的窗口抽象基类。
结构体WindowProps中构造窗口用的参数包,标题,宽高。
Windows则是Hazel引擎的窗口抽象接口,定义了一套平台窗口必须实现的通用行为,本身并不能被实例化。
EventCallbackFn定义了事件回调函数的类型,当发生窗口事件的时候会用这个函数通知上层。//这里不是Event吗?如何确定是窗口事件呢?
纯虚函数用于确保基类指针删除派生类对象时不会内存泄漏。
OnUpdate()每帧调用一次,负责处理窗口消息,刷新缓冲区,由主循环反复调用。
GetWidth()和GetHeight()用于获取窗口的宽高,且不会修改窗口对象的状态。
SetEventCallback()设置事件回调函数,窗口收到事件会通过这个函数把事件传给上层。
SetVSync()和IsVSync()设置是否开启垂直同步
Window* Create()静态工厂函数,用于创建窗口实例。

WindowsWindow.h
#pragma once
#include "Hazel/Window.h"
#include "Hazel/Log.h"
#include <GLFW/glfw3.h>
namespace Hazel{
class WindowsWindow : public Window
{
public:
WindowsWindow(const WindowProps& props);
virtual ~WindowsWindow();
void OnUpdate() override;
inline unsigned int GetWidth() const override { return m_Data.Width; }
inline unsigned int GetHeight() const override { return m_Data.Height; }
inline void SetEventCallback(const EventCallbackFn& callback) override { m_Data.EventCallback = callback; }
void SetVSync(bool enabled) override;
bool IsVSync() const override;
private:
virtual void Init(const WindowProps& props);
virtual void Shutdown();
private:
GLFWwindow* m_Window;
struct WindowData
{
std::string Title;
unsigned int Width, Height;
bool VSync;
EventCallbackFn EventCallback;
};
WindowData m_Data;
};
}
WindowsWindow.h主要用于实现Window.h中的内容,这里多一个WindowData结构体,用来包含其中的一些状态,方便使用,即m_Data。这样我们就可以只传递结构体,不用传递完整的类对象。
这里的GLFWwindow* m_Window和句柄有关,m_Window是GLFW库内部malloc出来的一块C结构体,内部包含了真正的Win32内核对象,本身并不是。
源码:
// GLFW 源码(src/internal.h)示意
struct _GLFWwindow {
_GLFWplatform platform; // 各平台专用子结构
// 里面放着:
// Windows: HWND handle; <-- 真正的内核句柄
// X11: Window x11handle;
// Wayland: struct wl_surface* surface;
...
};
可以注意到,GLFWwindow* m_Window实际上是指向这一块内存的指针,里面拿到了操作系统内核中的handle,也就是HWND。
WindowsWindow.cpp
#include "hzpch.h"
#include "WindowsWindow.h"
namespace Hazel {
static bool s_GLFWInitialized = false;
Window* Window::Create(const WindowProps& props)
{
return new WindowsWindow(props);
}
WindowsWindow::WindowsWindow(const WindowProps& props)
{
Init(props);
}
WindowsWindow::~WindowsWindow()
{
Shutdown();
}
void WindowsWindow::Init(const WindowProps& props)
{
m_Data.Title = props.Title;
m_Data.Width = props.Width;
m_Data.Height = props.Height;
HZ_CORE_INFO("Creating window {0} ({1}, {2})", props.Title, props.Width, props.Height);
if (!s_GLFWInitialized)
{
int success = glfwInit();
HZ_CORE_ASSERT(success, "Could notintialize GLFW!");
s_GLFWInitialized = true;
m_Window = glfwCreateWindow((int)props.Width, (int)props.Height, m_Data.Title.c_str(), nullptr, nullptr);
glfwMakeContextCurrent(m_Window);
glfwSetWindowUserPointer(m_Window, &m_Data);
SetVSync(true);
}
}
void WindowsWindow::Shutdown()
{
glfwDestroyWindow(m_Window);
}
void WindowsWindow::OnUpdate()
{
glfwPollEvents();
glfwSwapBuffers(m_Window);
}
void WindowsWindow::SetVSync(bool enabled)
{
if (enabled)
glfwSwapInterval(1);
else
glfwSwapInterval(0);
m_Data.VSync = enabled;
}
bool WindowsWindow::IsVSync() const
{
return m_Data.VSync;
}
}
Init()里面涉及到if (!s_GLFWInitialized)这个语句,主要是作为一个静态标志,防止重复的初始化GLFW,只初始化一次。int success = glfwInit();用来真正的初始化GLFW动态库,加载DLL,建立内部数据结构等。m_Window = glfwCreateWindow((int)props.Width, (int)props.Height, m_Data.Title.c_str(), nullptr, nullptr);创建一个底层原生窗口句柄,GLFW虽然只能初始化一次,但是glfwCreateWindow()可以调用多次,因此支持多窗口。glfwMakeContextCurrent(m_Window)则是把刚刚创建的窗口的OpenGL上下文设为当前线程的上下文,这样后续所有的OpenGL调用都会作用到这个窗口)。glfwSetWindowUserPointer(m_Window, &m_Data),把Hazel自己定义的WindowData结构体指针存到GLFW的窗口句柄里,实现“反向绑定”。之后在 GLFW 的回调里可以通过glfwGetWindowUserPointer取回m_Data,从而拿到Hazel的WindowsWindow实例,做事件分发。
glfwPollEvents();把操作系统这次派发的鼠标、键盘、窗口尺寸等事件全部取出来,分发给前面注册的回调函数,驱动整个事件系统往前走。glfwSwapBuffers(m_Window);把刚才 OpenGL 渲染好的 back-buffer 与 front-buffer 交换,真正显示到屏幕上;同时按当前垂直同步设置阻塞或不阻塞,控制帧率。
void WindowsWindow::SetVSync(bool enabled):运行时动态开关垂直同步。
尝试使用
Application.h
//添加新的引入库
#include "Window.h"
//创建一个指针用来指
class HAZEL_API Application
{
public:
Application();
virtual ~Application();
void Run();
//创建一个指针用来指(在这里)
private:
std::unique_ptr<Window> m_Window;
};
在Application.cpp中加入东西
Application.cpp
//在构造函数中添加
Application::Application()
{
m_Window = std::unique_ptr<Window>(Window::Create());
}
......
//在Run函数中添加
void Application::Run()
{
while (m_Running)
{
m_Window->OnUpdate();
}
}
出现的神秘BUG
1.报了一大堆有关于符号解析的错,原因是使用的编译有问题,打开属性,C/C++,代码生成将运行库修改为多线程DLL即可,似乎是因为编译MinGW和MSVC有问题。


2.Sandbox报错,原因是因为没有加预编译头,解决方式是自己模仿改一个预编译器……,能用就行(不过不知道为什么报了个冲突,直接勾选一个允许Hazel.dll得了,好吧不行,这个后续也必须要解决)
premake.lua
project "Sandbox"
location "Sandbox"
kind "ConsoleApp"
language "C++"
targetdir ("bin/".. outputdir .. "/%{prj.name}")
objdir ("bin-int/".. outputdir .. "/%{prj.name}")
pchheader "sandpch.h"
pchsource "Sandbox/src/sandpch.cpp"
files
{
"%{prj.name}/src/**.h",
"%{prj.name}/src/**.cpp"
}
includedirs
{
"Hazel/vendor/spdlog/include",
"Hazel/src"
}
links
{
"Hazel"
}
filter "system:windows"
cppdialect "c++17"
staticruntime "On"
systemversion "latest"
defines
{
"HZ_PLATFORM_WINDOWS"
}
filter "configurations:Debug"
defines "HZ_DEBUG"
symbols "On"
filter "configurations:Release"
defines "HZ_RELEASE"
symbols "On"
filter "configurations:Dist"
defines "HZ_DIST"
symbols "On"

3.为什么会报ostream的错,以及一些神秘的错误(来自Events事件文件夹的头文件们),答案是这些文件在自嗨,完全没有cpp使用他们,还没编译呢(

4.神秘的报错!以及解决,答案是Hazel和Sandbox都需要配置一样的运行时库,当时Hazel.dll配置为了/MD,而Sandbox.exe被配置为了其他的,应该都修改为/MD。

(思索,所以是不是可以不要Sandbox的预编译,直接都用/MD就可以了?)
(好吧不行)
最后效果如下:成功启动了引擎!

可以继续修改看一些其他的情况,最终版本:
Application.cpp
#include "hzpch.h"
#include "Application.h"
#include "Hazel/Events/ApplicationEvent.h"
#include "Hazel/Log.h"
#include<GLFW/glfw3.h>
namespace Hazel
{
Application::Application()
{
m_Window = std::unique_ptr<Window>(Window::Create());
}
Application::~Application()
{
}
void Application::Run()
{
while (m_Running)
{
glClearColor(1, 0, 1, 1);
glClear(GL_COLOR_BUFFER_BIT);
m_Window->OnUpdate();
}
}
}

