Hazel引擎学习(三)

一. Layer

设计完Window和Event之后,需要创建Layer,Layer这个概念比较抽象,具体在游戏里,比如游戏画面可能是离摄像机最远的Layer,然后依次可能会有UI Layer和Debug Layer,游戏里的Layer应该具备最基本的两个功能:可以在该Layer上渲染一些东西接受外部的Event,所以Layer类需要有以下内容:

  • OnUpdate:用于处理渲染的loop
  • OnEvent:用于处理事件
  • Init函数:负责Layer的初始化
  • Exit函数: 负责Layer的结束操作

Layer接口设计如下

class HAZEL_API Layer
{
public:
	Layer (const std::string& name = "Layer");
	virtual ~Layer ();
	virtual void OnAttach() = 0; //当layer添加到layer stack的时候会调用此函数,相当于Init函数
	virtual void OnDettach() = 0; //当layer从layer stack移除的时候会调用此函数,相当于Shutdown函数
	virtual void OnEvent() = 0;
	virtual void OnUpdate() = 0;

protected:
	bool isEnabled;	// 值为fasle时,该Layer会被禁用,不会绘制画面,也不会接收事件
	std::string m_DebugName;
};

在游戏里经常会有多个Layer,当多个Layer存在时,往往需要对上面一层的Layer(离摄像机最近的)进行处理,比如我们在点击UI时并不想让角色随之进行动作,所以这里设计了一个LayerStack,用于按照到摄像机距离从远到近的存放Layer,值得注意的是,处理渲染时,应该先画最远的Layer,再画最近的Layer,而处理事件时正好相反,因为最上面一层的Layer才应该是接受event的对象,二者的顺序正好是相反的。


二. LayerStack

由于游戏里面有很多个Layer,所以要设计一个数据结构存储这些Layer,这里用一个vector模拟了一个Stack,作为容器,vector[0]作为栈顶,栈顶的Layer是屏幕上离我们最近的Layer,该Layer最先接受事件,最后被渲染,类的设计思路如下:

class HAZEL_API LayerStack
{
public:
	LayerStack();
	~LayerStack();
	void PushLayer(Layer*); // lay代表普通的layer, Overlay代表离屏幕最远的layer
	void PushOverlay(Layer*);
	Layer* PopLayer();

private:
	std::vector<Layer*>stack;
	std::vector<Layer*>::iterator curStackItr;
};

每一个Application都需要有对应的LayerStack,可以存放多种Layer:

class HAZEL_API Application
{
public:
	Application();
	virtual ~Application();

	void OnEvent(Event& e);
	void Run();
	bool OnWindowClose(WindowCloseEvent& e);
	void PushLayer(Layer* layer);
	Layer* PopLayer();
protected:
	LayerStack m_LayerStack;
	std::unique_ptr<Window>m_Window;
	bool m_Running = true;
};

Application* CreateApplication();

实际应用时,需要Sanbox创建自己的Layer和LayerStack,然后在Application.Run函数里面调用即可,部分代码如下所示:

=============main函数========
int main()
{
	Hazel::Log::Init();
	auto app = Hazel::CreateApplication();
	app->Run();
	delete app;
}

=============创建自己的Layer=========
Hazel::Application* Hazel::CreateApplication() 
{
	Sandbox *s = new Sandbox();
	return s;
}

class Sandbox : public Hazel::Application
{
public:
	Sandbox()
	{
		m_LayerStack.PushLayer(new ExampleLayer());
	};
	~Sandbox(){};
private:
};


三. 添加图形库

可以使用glew,也可以使用glad库,二者的在效率上好像没啥区别,不过glad的库要更新一些,所以这里用glad库,具体步骤有:

  • 上网站https://glad.dav1d.de/上下载对应版本的header和src文件,放在vendor文件夹下
  • 网站上下载的glad库没有premake5文件,所以按照glfw库的方式为其写一个,与glfw库相同,这里的glad库也是作为lib文件使用
  • 把glad库的premake5文件相关内容整合到整个工程的premake5文件里

四. 添加ImGUI库

关于ImGui
ImGui的项目地址在Github上,全称为Dear ImGui,主要的作者是ocornut,Dear ImGui是一个C++语言写的图形接口库(bloat-free graphical user interface library for C++),It outputs optimized vertex buffers that you can render anytime in your 3D-pipeline enabled application. It is fast, portable, renderer agnostic and self-contained (no external dependencies).

这里的ImGui的主要用于程序员的图形Debug工具(类似于Unity的ImGUI),举个例子,如果没有ImGUI,要调整好一个参数,要反复在代码里面修改数值,然后编译运行项目查看效果,这样很麻烦,而通过ImGui,就可以实现直接在图形界面调试参数的功能

添加ImGUI使用文件
ImGui可以帮助程序员进行Debug,基于上述的Layer系统,把ImGui也作为一个Layer,为其创建对应的ImGuiLayer.cpp和相关头文件。
对应的ImGuiLayer类,如下所示:

namespace Hazel 
{
	HAZEL_API class ImGuiLayer : public Layer
	{
		ImGuiLayer(const std::string& name = "Layer");
		~ImGuiLayer();
		void OnAttach() override; //当layer添加到layer stack的时候会调用此函数,相当于Init函数
		void OnDettach() override; //当layer从layer stack移除的时候会调用此函数,相当于Shutdown函数
		void OnEvent(Event&) override;
		void OnUpdate() override;
	};
}

加入ImGui Layer具体的代码
需要仿照ImGui使用OpenGL的代码,具体代码需要参考两个imgui给出的cpp文件,由于我们用的是glfw库加上OpenGL3的版本,所以需要参加的两个cpp文件为:imgui_impl_opengl3.cppimgui_impl_glfw.cpp

以下是imgui_impl_glfw.cpp的部分代码,可以看到基本是ImGui把glfw库的很多函数和功能封装到了ImGui自己的类中:

static GLFWwindow*          g_Window = NULL;    // Main window
static bool ImGui_ImplGlfw_Init(GLFWwindow* window, bool install_callbacks, GlfwClientApi client_api)
{
    g_Window = window;
    g_Time = 0.0;

    // Setup backend capabilities flags
    ImGuiIO& io = ImGui::GetIO();
    io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors;         // We can honor GetMouseCursor() values (optional)
    io.BackendFlags |= ImGuiBackendFlags_HasSetMousePos;          // We can honor io.WantSetMousePos requests (optional, rarely used)
    io.BackendPlatformName = "imgui_impl_glfw";

    // Keyboard mapping. ImGui will use those indices to peek into the io.KeysDown[] array.
    io.KeyMap[ImGuiKey_Tab] = GLFW_KEY_TAB;
    io.KeyMap[ImGuiKey_LeftArrow] = GLFW_KEY_LEFT;
    io.KeyMap[ImGuiKey_RightArrow] = GLFW_KEY_RIGHT;
    io.KeyMap[ImGuiKey_UpArrow] = GLFW_KEY_UP;
    io.KeyMap[ImGuiKey_DownArrow] = GLFW_KEY_DOWN;
    io.KeyMap[ImGuiKey_PageUp] = GLFW_KEY_PAGE_UP;
    io.KeyMap[ImGuiKey_PageDown] = GLFW_KEY_PAGE_DOWN;
    io.KeyMap[ImGuiKey_Home] = GLFW_KEY_HOME;
    io.KeyMap[ImGuiKey_End] = GLFW_KEY_END;
    io.KeyMap[ImGuiKey_Insert] = GLFW_KEY_INSERT;
    io.KeyMap[ImGuiKey_Delete] = GLFW_KEY_DELETE;
    io.KeyMap[ImGuiKey_Backspace] = GLFW_KEY_BACKSPACE;
    io.KeyMap[ImGuiKey_Space] = GLFW_KEY_SPACE;
    io.KeyMap[ImGuiKey_Enter] = GLFW_KEY_ENTER;
    io.KeyMap[ImGuiKey_Escape] = GLFW_KEY_ESCAPE;
    io.KeyMap[ImGuiKey_KeyPadEnter] = GLFW_KEY_KP_ENTER;
    io.KeyMap[ImGuiKey_A] = GLFW_KEY_A;
    io.KeyMap[ImGuiKey_C] = GLFW_KEY_C;
    io.KeyMap[ImGuiKey_V] = GLFW_KEY_V;
    io.KeyMap[ImGuiKey_X] = GLFW_KEY_X;
    io.KeyMap[ImGuiKey_Y] = GLFW_KEY_Y;
    io.KeyMap[ImGuiKey_Z] = GLFW_KEY_Z;

    io.SetClipboardTextFn = ImGui_ImplGlfw_SetClipboardText;
    io.GetClipboardTextFn = ImGui_ImplGlfw_GetClipboardText;
    io.ClipboardUserData = g_Window;
#if defined(_WIN32)
    io.ImeWindowHandle = (void*)glfwGetWin32Window(g_Window);
#endif

    // Create mouse cursors
    // (By design, on X11 cursors are user configurable and some cursors may be missing. When a cursor doesn't exist,
    // GLFW will emit an error which will often be printed by the app, so we temporarily disable error reporting.
    // Missing cursors will return NULL and our _UpdateMouseCursor() function will use the Arrow cursor instead.)
    GLFWerrorfun prev_error_callback = glfwSetErrorCallback(NULL);
    g_MouseCursors[ImGuiMouseCursor_Arrow] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR);
    g_MouseCursors[ImGuiMouseCursor_TextInput] = glfwCreateStandardCursor(GLFW_IBEAM_CURSOR);
    g_MouseCursors[ImGuiMouseCursor_ResizeNS] = glfwCreateStandardCursor(GLFW_VRESIZE_CURSOR);
    g_MouseCursors[ImGuiMouseCursor_ResizeEW] = glfwCreateStandardCursor(GLFW_HRESIZE_CURSOR);
    g_MouseCursors[ImGuiMouseCursor_Hand] = glfwCreateStandardCursor(GLFW_HAND_CURSOR);
#if GLFW_HAS_NEW_CURSORS
    g_MouseCursors[ImGuiMouseCursor_ResizeAll] = glfwCreateStandardCursor(GLFW_RESIZE_ALL_CURSOR);
    g_MouseCursors[ImGuiMouseCursor_ResizeNESW] = glfwCreateStandardCursor(GLFW_RESIZE_NESW_CURSOR);
    g_MouseCursors[ImGuiMouseCursor_ResizeNWSE] = glfwCreateStandardCursor(GLFW_RESIZE_NWSE_CURSOR);
    g_MouseCursors[ImGuiMouseCursor_NotAllowed] = glfwCreateStandardCursor(GLFW_NOT_ALLOWED_CURSOR);
#else
    g_MouseCursors[ImGuiMouseCursor_ResizeAll] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR);
    g_MouseCursors[ImGuiMouseCursor_ResizeNESW] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR);
    g_MouseCursors[ImGuiMouseCursor_ResizeNWSE] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR);
    g_MouseCursors[ImGuiMouseCursor_NotAllowed] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR);
#endif
    glfwSetErrorCallback(prev_error_callback);

    // Chain GLFW callbacks: our callbacks will call the user's previously installed callbacks, if any.
    g_PrevUserCallbackMousebutton = NULL;
    g_PrevUserCallbackScroll = NULL;
    g_PrevUserCallbackKey = NULL;
    g_PrevUserCallbackChar = NULL;
    if (install_callbacks)
    {
        g_InstalledCallbacks = true;
        g_PrevUserCallbackMousebutton = glfwSetMouseButtonCallback(window, ImGui_ImplGlfw_MouseButtonCallback);
        g_PrevUserCallbackScroll = glfwSetScrollCallback(window, ImGui_ImplGlfw_ScrollCallback);
        g_PrevUserCallbackKey = glfwSetKeyCallback(window, ImGui_ImplGlfw_KeyCallback);
        g_PrevUserCallbackChar = glfwSetCharCallback(window, ImGui_ImplGlfw_CharCallback);
    }

    g_ClientApi = client_api;
    return true;
}

接下来在Platform文件夹下,创建OpenGL文件夹:

  • imgui_impl_opengl3的头文件和源文件放进去,更名为ImGuiOpenGLRenderer,用来存放ImGui调用OpenGL的代码。
  • 而原本用到的imgui_impl_glfw相关内容,就直接Copy和Paste到ImGuiLayer里。

五. 为ImGui添加Event

由于之前已经构建好了Event和EventDispatcher类,在Application.cpp中还使用了WindowCloseEvent类,核心代码如下:

void Application::OnEvent(Event& e)
{
	//CORE_LOG("{0}", e);
	CORE_LOG(e.ToString());
	EventDispatcher dispatcher(e);
	dispatcher.Dispatch<WindowCloseEvent>(std::bind(&Application::OnWindowClose, this, std::placeholders::_1));
	for (Layer* layer : m_LayerStack)
	{
		layer->OnEvent(e);
	}
}

bool Application::OnWindowClose(WindowCloseEvent &e)
{
	m_Running = false;
	return true;
}

类比这么写就可以了:

void Hazel::ImGuiLayer::OnEvent(Event &e)
{
	EventDispatcher dispatcher(e);
	dispatcher.Dispatch<MouseButtonPressedEvent>(std::bind(&ImGuiLayer::OnMouseButtonPressed, this, std::placeholders::_1));
	dispatcher.Dispatch<MouseButtonReleasedEvent>(std::bind(&ImGuiLayer::OnMouseButtonReleased, this, std::placeholders::_1));
	dispatcher.Dispatch<MouseMovedEvent>(std::bind(&ImGuiLayer::OnMouseCursorMoved, this, std::placeholders::_1));
}

bool Hazel::ImGuiLayer::OnMouseCursorMoved(MouseMovedEvent &e)
{
	ImGuiIO& io = ImGui::GetIO();
	io.MousePos = ImVec2((float)e.GetXPos(), (float)e.GetYPos());
	return true;
}

bool Hazel::ImGuiLayer::OnMouseButtonPressed(MouseButtonPressedEvent& e)
{
	ImGuiIO& io = ImGui::GetIO();
	io.MouseDown[e.GetMouseButton()] = 1;
	return true;
}

bool Hazel::ImGuiLayer::OnMouseButtonReleased(MouseButtonReleasedEvent &e)
{
	ImGuiIO& io = ImGui::GetIO();
	io.MouseDown[e.GetMouseButton()] = 0;
	return true;
}

KeyTypeEvent
这里需要添加一种额外的Event,作为打字时调用的Event,对应glfw的回调函数为glfwSetCharCallback,用于在窗口上打字。


六. Input Polling

Input接口类设计
现在要为引擎添加新的功能,我们的应用需要能够知道键盘的输入状态,比如Unity里按住W和鼠标右键就可以实现摄像机的推进,所以引擎需要能够知道键盘的W键是否被按下,为了解决这个问题,首先想到的就是,对W键设置一个Pressed变量,当产生对W的OnKeyPressedEvent时,将其置为true,产生对W的OnKeyReleasedEvent时,将其置为false,然而这样做非常繁琐,因为这代表着,每一个键,包括鼠标按键,都需要添加一个记录其状态的变量。

思路是创建一个Input接口类,这个类根据不同平台生成对应的的Input子类,比如Windows平台下有class WindowsInput : public Input,Input类的接口需要判断某个键的状态、鼠标点击状态等,由于一个系统不会存在两个同样的键,也不会有两个鼠标,所以把这些函数都设计为Static函数,最开始我思考的接口设计是这样:

namespace Hazel
{
	class HAZEL_API Input
	{
	public:
		Input();
		virtual ~Input();
		virtual static bool IsKeyPressed(int keycode) = 0;//得知keycode键的按键状态
		virtual static bool GetMouseX(int keycode) = 0;
		virtual static bool GetMouseY(int keycode) = 0;
	}
}

然而这里出现了问题,问题在于我用了static的虚函数,这是不对的,因为虚函数是对象的多态,属于类的实例化对象的方法,而static函数属于类,而不属于对象,所以It's not meaningful,况且虚函数所用的虚指针vptr不是static对象,所以static函数不可以使用它,更多的可以参考C++ static virtual members? - Stack Overflow

为了解决上述问题,保证一个系统只有一个Input实例,可以用单例模式,单例暴露的接口是static函数,而实现的具体方法是单例的虚函数,代码设计如下:

namespace Hazel
{
	class HAZEL_API Input
	{
	public:
		Input();
		virtual ~Input();
		inline static bool IsKeyPressed(int keycode) { return s_Instance -> IsKeyPressedImp(keycode); }
		static bool GetMouseX() { return s_Instance -> GetMouseXImp(); }
		static bool GetMouseY() { return s_Instance -> GetMouseYImp(); }
	protected:
		virtual bool IsKeyPressedImp(int keycode) = 0;
		virtual bool GetMouseXImp() = 0;
		virtual bool GetMouseYImp() = 0;
	private:
		static Input* s_Instance;
	}
}

设计完Input接口类之后,就可以创建对应的子类了,如下所示是WindowsInput.cpp的内容:

// todo

Window类补充
这是之前的Window接口类的状态,由于Input的子类需要获取具体的Window得到相关的按键信息,比如glfwGetKey函数,所以需要为Window接口类添加一个接口函数,用来返回具体window的指针,比如返回WindowsWindow类内的私有函数GLFWWindow,如下所示:

class HAZEL_API Window
{
	...
	virtual void* GetNativeWindow() = 0;
}

七. Math

游戏引擎里自然少不了Vector3、Matrix以及相关的计算,如果自己写Math库,也可以允许,但是运行效率会不尽如人意,因为好的Math库能够尽可能快的完成数学运算(比如通过一次CPU指令完成矩阵的运算),这(好像)也叫做smid,如下图所示:
在这里插入图片描述

为了保证效率和跨平台的能力,这里使用glm库作为引擎的数学库,glm不只是OpenGL的数学库,也可以单独抽出来使用。

八. ImGui Docking and Viewports

游戏引擎,比如Unity、UE4里的窗口都是可以拖拽(Docking)的,这是编辑器最基本的功能,为了不采用WPF、QT这些技术来完成拖拽功能,可以直接用ImGui来完成,而ImGui在其Dock分支正在开发这一功能,还没合并到master上,这意味着这个相关功能可能会随时更新,还记得之前的ImGui的相关代码怎么做的吗,我是把相关代码抽离出来,放到了自己的ImGuiLayer.cpp里,然而这么做的缺点是,当ImGui改动代码的时候,我必须去看它改动了什么,然后再去手动Merge改动,这也不符合我使用git submodule的初衷,之前使用submodule的时候,都是引用该Project,然后把该submodule的源文件放进来,然而这里由于imgui里的源文件很多是我们不需要的,所以这里把其中的重要文件放到了ImGuiBuild.cpp里,直接当作头文件include进来,源码就不加在project的source列表里了,如下图所示,这里没有我们需要的cpp文件:
在这里插入图片描述

所以需要做以下事情:

  • 切换ImGui分支:cd到Imgui的submodule,切换到docking分支,添加premake5.lua文件然后上传
  • 清除之前在ImGuiLayer.cpp里粘贴的ImGuiOpengl3Renderer和ImGuiGlfw3的相关内容,然后建立一个ImGuiBuild.cpp,把相关文件include进来(类似UnityBuild的做法),如下图所示:
    在这里插入图片描述

设计思路
之前的ImGuiLayer是在SandboxApp.cpp里加入的,而实际上ImGui应该是游戏引擎自带的东西,不应该是由用户定义添加到LayerStack里,所以需要为Application提供固有的ImGuiLayer成员,可以用宏括起来,Release游戏的时候,就不用这个东西,设计思路如下:

	class HAZEL_API Application
	{
	public:
		Application();
		virtual ~Application();
		inline static Application& Get() { return *s_Instance;  }

		void OnEvent(Event& e);
		void Run();
		bool OnWindowClose(WindowCloseEvent& e);
		void PushLayer(Layer* layer);
		Layer* PopLayer();
		Window& GetWindow()const { return *m_Window; }
	private:
		static Application* s_Instance;

	protected:
		std::unique_ptr<Window>m_Window;
		ImGuiLayer* m_ImGuiLayer;// 添加ImGUILayer
		LayerStack m_LayerStack;
		bool m_Running = true;
	};

为了让每一个Layer都有一个ImGuiLayer,让每一个Layer都继承一个接口,用于绘制ImGui的内容,同时让ImGuiLayer成为Hazel内在(intrinsic)的部分,需要在Application里面加上对应的LayerStack,与其内部的Layer一一对应,设计思路如下:

class HAZEL_API Layer
{
public:
	Layer (const std::string& name = "Layer");
	virtual ~Layer ();
	virtual void OnAttach() {}; //当layer添加到layer stack的时候会调用此函数,相当于Init函数
	virtual void OnDettach() {}; //当layer从layer stack移除的时候会调用此函数,相当于Shutdown函数
	virtual void OnEvent(Event&) {};
	virtual void OnUpdate() {};
	virtual void OnImGuiRender() {};
private:
	...
}

然后在实际Run的Loop里,先调用Layer的正常函数,再调用其ImGuiRender函数,如下所示:

while (m_Running)
{
	// 每帧开始Clear
	glClearColor(1, 0, 1, 1);
	glClear(GL_COLOR_BUFFER_BIT);
	// Application并不应该知道调用的是哪个平台的window,Window的init操作放在Window::Create里面
	// 所以创建完window后,可以直接调用其loop开始渲染
	for (Layer* layer : m_LayerStack)
	{
		layer->OnUpdate();
	}

	m_ImGuiLayer->Begin();//统一调用,调用了NewFrame
	for (Layer* layer : m_LayerStack)
	{
		// 每一个Layer都在调用ImGuiRender函数
		// 目前有两个Layer, Sandbox定义的ExampleLayer和构造函数添加的ImGuiLayer
		layer->OnImGuiRender();
	}
	m_ImGuiLayer->End();//统一结束调用,调用了EndFrame
	
	// 每帧结束调用glSwapBuffer
	m_Window->OnUpdate();
}


Cherno提出的作业

在完成上述功能后,就可以把ImGui对应的窗口任意拖拽了,但为了在SandboxApp展示的窗口,也就是原始的Windows的粉色窗口上绘制对应的内容,需要在ExampleLayer里的OnImGuiRender里进行绘制,代码如下所示:

include "imgui.h"//为了使用下面的函数,需要include头文件
	void OnImGuiRender() override
	{
		ImGui::Begin("Test");
		ImGui::Text("Hello World");
		ImGui::End();
	}

然而运行后,会报错,如下所示:

SandboxApp.obj : error LNK2019: unresolved external symbol 

大概意思就是,Linker找不到Begin、Text和End函数的定义,这是为什么呢?

这是因为原本Hazel引擎做成了dll,从外部可以调用的类和函数都是用HAZEL_API定义的,然而ImGui用的是IMGUI_API,而ImGUI的内容是作为lib文件链接到Hazel.dll里的,IMGUI_API默认是没有任何定义的,所以ImGUI的相关API并没有声明为dllexport,当然是不能运行的。

所以需要对IM_GUI进行处理,在IMGUI的工程里是dllexport,在Sandbox工程里是dllimport,具体做法如下:

  • 由于Hazel是以ImGui的project作为reference,所以在ImGui的项目上,其项目应该定义IMGUI_API这个宏为__declspec(dllexport),负责导出API
  • 然后在运行的SandboxApp的项目中,定义IMGUI_API这个宏为__declspec(dllimport),就可以了。

实际上定义宏的具体方式也有多种,我一开始想到了两种:

  • 第一种是直接改Core.h文件,内容如下所示:
#ifdef HZ_PLATFORM_WINDOWS
	#ifdef HZ_BUILD_DLL
	#define HAZEL_API _declspec (dllexport)
	#define IMGUI_API _declspec (dllexport) // 添加对IMGUI_API的定义,导出api
	#else 
	#define HAZEL_API _declspec (dllimport)
	#define IMGUI_API _declspec (dllimport) // 添加对IMGUI_API的定义,导入api
	#endif // HZ_BUILD_DLL
#endif

-然而这个方法有个缺点,就是我得在ImGui.h前面加上#Include "Core.h",否则在ImGui.cpp里根本就不知道IMGUI_API这个宏已经定义过了,这样做会更改git submodule里的cpp内容,从父模块改变子模块的内容,这样并不好。

第二种方法是直接修改submodule里对应的premake5.lua文件:

  • 因为了解到premake里面可以直接设定宏,所以考虑export部分放在premake文件里,而import部分还是不变
    所以Core.h变为:
#ifdef HZ_PLATFORM_WINDOWS
	#ifdef HZ_BUILD_DLL
	#define HAZEL_API _declspec (dllexport)
	//#define IMGUI_API _declspec (dllexport) // 添加导出这一行不要了
	#else 
	#define HAZEL_API _declspec (dllimport)
	#define IMGUI_API _declspec (dllimport) // 添加导入
	#endif // HZ_BUILD_DLL
#endif

然后在ImGui的Premake5.lua文件里进行修改,(由于这个lua文件是自己添加的,应该不会与作者对submodule的更改起冲突),宏定义如下所示:

		defines 
		{
            "IMGUI_API=__declspec(dllexport)"
		}

然后就可以看到自己窗口的Window展示了,如下图所示,美滋滋:
在这里插入图片描述

使用Module Definition File
除了上面说的去定义宏的方法,还可以使用第三种方法:Module Definition File,这种方法比较麻烦,但是可以不改变submodule的内容,其文件格式后缀为.def,可以在该文件里,列出所有需要exportdll的函数的签名,如下图所示:
在这里插入图片描述
Cherno给出的文件代码如下所示,这里把ShowDemoWindow、End等四个函数进行了dllexport的操作,不过这玩意儿很难写:
在这里插入图片描述



相关知识补充

vector emplace
这一部分的内容参考了:http://c.biancheng.net/view/6834.html
以前自己写代码总是用push_back这种的,vector的插入操作写得少:
vector容器提供了 insert() 和 emplace() 这 2 个成员函数,用来实现在容器指定位置处插入元素,最早的vector是用的insert函数,代码如下:

#include <iostream> 
#include <vector> 
#include <array> 
using namespace std;
int main()
{
    std::vector<int> demo{1,2};
    // 1. iterator insert(pos,elem),插入单个元素,返回表示新插入元素位置的迭代器。
    demo.insert(demo.begin() + 1, 3);//{1,3,2}
    // 2. iterator insert(pos,n,elem),插入多个相同元素,返回表示第一个新插入元素位置的迭代器。
    demo.insert(demo.end(), 2, 5);//{1,3,2,5,5}
    // 3. iterator insert(pos,first,last) ,插入其他的所有容器,返回表示第一个新插入元素位置的迭代器
    std::array<int,3>test{ 7,8,9 };
    demo.insert(demo.end(), test.begin(), test.end());//{1,3,2,5,5,7,8,9}
    // 4. iterator insert(pos,initlist),插入初始化列表(用大括号{}括起来的多个元素)中所有的元素,并返回表示第一个新插入元素位置的迭代器。
    demo.insert(demo.end(), { 10,11 });//{1,3,2,5,5,7,8,9,10,11}

    return 0;
}

emplace() 是 C++ 11 标准新增加的成员函数,用于在 vector 容器指定位置之前插入一个新的元素,写法如下:

// pos为迭代器,args...为元素的构造函数对应的参数
iterator emplace (const_iterator pos, args...);

// 举个例子
std::vector<int> demo1{1,2};
//emplace() 每次只能插入一个 int 类型元素
demo1.emplace(demo1.begin(), 3);

insert函数和emplace函数的区别在于, emplace() 在插入元素时,是在容器的指定位置直接构造元素,而insert函数是先单独生成,再将其复制(或移动)到容器中。因此,在实际使用中,推荐大家优先使用 emplace()。


使用for循环时需要提前设置迭代器
如下图所示,提示我没有声明对应的begin函数,也就是说要实现for遍历功能,这个类得定义begin和end函数,返回类型为一个迭代器
在这里插入图片描述
加上对应的begin迭代器函数就可以了,代码如下所示:

	class HAZEL_API LayerStack
	{
	public:
		LayerStack();
		~LayerStack();
		void PushLayer(Layer*);
		void PushOverlay(Layer*);
		Layer* PopLayer();

		std::vector<Layer*>::iterator begin() {	return m_Stack.begin(); }
		std::vector<Layer*>::iterator end() {	return m_Stack.end(); }
	private:
		std::vector<Layer*>m_Stack;
		std::vector<Layer*>::iterator curStackItr;
	};

注意的是,这里的begin和end函数返回的必须是指针或迭代器类型,int这种类型是不可以的,否则会编译报错,如下图所示:
在这里插入图片描述


添加glad库遇到的问题
glad库里的内容与glfw冲突了,报错位置为:

#ifdef __gl_h_
#error OpenGL header already included, remove this include, glad already provides it
#endif
#define __gl_h_

可以看到这个函数定义了两次,glfw定义了一次,所以到glfw就报错了,glfw定义的地方在这里:

...
elif !defined(GLFW_INCLUDE_NONE)

 #if defined(__APPLE__)
...
 #else /*__APPLE__*/

  #include <GL/gl.h>
  #if defined(GLFW_INCLUDE_GLEXT)
   #include <GL/glext.h>
  #endif
  #if defined(GLFW_INCLUDE_GLU)
   #include <GL/glu.h>
  #endif

 #endif /*__APPLE__*/

解决办法有两种:

  • 要么把include<glad.h>放到glfw的include的前面
  • 给glfw加上宏GLFW_INCLUDE_NONE

然而我在glfw的premake5文件里加上了GLFW_INCLUDE_NONE的宏后还是报错,报错信息如下:
You must not define any header option macros when compiling GLFW"
可以看到在glfw库的internal.h文件夹里有如下内容:

#if defined(GLFW_INCLUDE_GLCOREARB) || \
    defined(GLFW_INCLUDE_ES1)       || \
    defined(GLFW_INCLUDE_ES2)       || \
    defined(GLFW_INCLUDE_ES3)       || \
    defined(GLFW_INCLUDE_ES31)      || \
    defined(GLFW_INCLUDE_ES32)      || \
    defined(GLFW_INCLUDE_NONE)      || \                 // 这里变颜色了,说明定义宏的位置应该错了
    defined(GLFW_INCLUDE_GLEXT)     || \
    defined(GLFW_INCLUDE_GLU)       || \
    defined(GLFW_INCLUDE_VULKAN)    || \
    defined(GLFW_DLL)
 #error "You must not define any header option macros when compiling GLFW"
#endif

结果发现GLFW_INCLUDE_NONE的Macro不应该在GLFW库项目上进行定义,而是应该在同时使用GLFW库和Glad库的Hazel工程上应用,因为单独build两个工程生成lib文件都是OK的,而是Hazel同时使用了两个的头文件,才导致有两次include <gl/gl.h>的操作,导致报错。

修复了上述错误,再进行调试,发现还是报错了,报错语句在glClear(GL_COLOR_BUFFER_BIT),也就是在OpenGL的部分,报错信息为:0xC0000005: Access violation executing location 0x0000000000000000

因为原本的gl相关的创建部分在glfw部分,我们用GLFW_INCLUDE_NONE宏将其去掉了,所以要使用gl相关函数,需要使用glad或glew库,在创建窗口的Init函数里加入下面两行代码进行检验,

int status = gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);
HAZEL_ASSERT(status, "Failed to init glad"); // 报错,返回的status是0

然而视频上Cherno这么做返回的status是1,最后发现是顺序问题,上述代码应该在创建Window和调用glfwMakeContextCurrent之后


Hazel.dll和Application的exe拥有不同的堆
当我在Sandbox.cpp里调用这个函数的时候,碰到了这么一个错误:

	void OnEvent(Hazel::Event& e) override
	{
		e.ToString();
	}
	
	//----ToString函数里是这么写的----
	std::string ToString() const override
	{
		std::stringstream a;
		a << "MouseMovedEvent: xPos = " << GetXPos() << ", yOffset = " << GetYPos();
		return a.str();
	}	
	

在这里插入图片描述
为什么会出现这个报错,可以看到是在debug_heap.cpp报的错,说明与Heap有关,出现这个的原因是因为dll和exe分别拥有自己的Heap,导致的同一块内存在堆A上创建,又在堆B上释放

As this is a DLL, the problem might lie in different heaps used for allocation and deallocation (try to build the library statically and check if that will work).The problem is, that DLLs and templates do not agree together very well. In general, depending on the linkage of the MSVC runtime, it might be problem if the memory is allocated in the executable and deallocated in the DLL and vice versa (because they might have different heaps). And that can happen with templates very easily, for example: you push_back() to the vector inside the removeWhiteSpaces() in the DLL, so the vector memory is allocated inside the DLL. Then you use the output vector in the executable and once it gets out of scope, it is deallocated, but inside the executable whose heap doesn’t know anything about the heap it has been allocated from. Bang, you’re dead.

为了解决这个问题,需要保证dll和exe享用同一块Heap。总而言之就是,e.ToString函数是dll里面的,调用该函数会在dll对应的堆上创建string对象,然而这个对象,在出了app的{}范围后,就会被exe释放,而自己的exe也有一个堆,这个时候把原本属于dll堆的对象创建到了app的堆上,所以就报错了。如下所示:

void OnEvent(Hazel::Event& e) override
{
	e.ToString();// ToString会导致在dll的heap上创建内存
}// 出了这个范围,e的生命期截至了,其在heap上分配的内存也应该被释放

因为dll的堆和app的堆不应该共享同一块内存,具体如何让二者各自拥有自己的堆,牵扯到了如下图所示的这个选项:
在这里插入图片描述
为了搞清楚这个设置,首先需要搞清楚这个Runtime Library的定义代表什么意思,VS的Runtime Library里面一共有四个选项:

  • /MT Multi-threaded
  • /MTd Multi-threaded Debug
  • /MD Multi-threaded DLL
  • /MDd Multi-threaded Debug DLL

Runtime Library代表着运行时链接库的方式,这里涉及到了系统的库文件,就不作多介绍了,总之能用/MD模式就用/MD模式,多的我也不去深究和了解了,总之结尾加d的是debug模式,不加d的是Release模式

所以要在Premake5.lua文件里这么修改,由于修改的是BuildOptions,Dll生成的选项应该在DLL对应的项目,Hazel下的项目属性进行修改,如下所示:

project "Hazel"
    location "%{prj.name}" -- 规定了targetdir和objdir还需要这个吗,需要,这里的location是生成的vcproj的位置
    kind "SharedLib"
    language "C++"
	targetdir ("bin/" .. outputdir .. "/%{prj.name}") --记得要加括号
	objdir   ("bin-int/" .. outputdir .. "/%{prj.name}") --这里的中英文括号看上去好像
	links {"GLFW", "opengl32.lib"}

    pchheader "hzpch.h"
    pchsource "%{prj.name}/Src/hzpch.cpp"
	-- 省略部分内容
	
    filter { "configurations:Debug" }
        defines { "DEBUG", "HZ_BUILD_DLL"}
		buildoptions {"/MDd"} -- 添加buildoptions
        symbols "On"
		runtime "Debug" -- 运行时链接的dll是debug类型的

    filter { "configurations:Release"}
        defines { "NDEBUG", "HZ_BUILD_DLL"}
		buildoptions {"/MD"}
        optimize "On"
		runtime "Release" -- 运行时链接的dll是release类型的

    filter { "configurations:Dist"}
		defines { "NDEBUG", "HZ_BUILD_DLL"}
		buildoptions {"/MD"}
	    optimize "On"

Debug下build成功,但是Release下失败
报错的代码如下,代码在Log.cpp文件里:

#include "hzpch.h"
#include "Log.h"

namespace Hazel
{
	Log::Log()
	{
	}

	Log::~Log()
	{
	}

	void Log::Init()
	{
		spdlog::set_pattern("%^[%T] %n: %v%$");

		s_CoreLogger = spdlog::stdout_color_mt("Hazel");// mt means multi threaded
		s_CoreLogger->set_level(spdlog::level::trace);

		s_ClientLogger = spdlog::stdout_color_mt("Console");
		s_ClientLogger->set_level(spdlog::level::trace);
	}

	std::shared_ptr<spdlog::logger>Log::s_ClientLogger = nullptr; // 编译错误  cannot define dllimport entity
	std::shared_ptr<spdlog::logger>Log::s_CoreLogger = nullptr; // 编译错误
}

build时报错信息为:

2>d:\hazel\hazel\hazel\src\hazel\log.cpp(25): error C2491: 'Hazel::Log::s_ClientLogger': definition of dllimport static data member not allowed

最后发现是自己在Release模式下,没有定义HZ_BUILD_DLL宏,我的Premake5.lua文件是这么写的:

...
filter { "configurations:Release", "HZ_BUILD_DLL"} --这里写错了,应该写在defines里
    defines { "NDEBUG" }
    optimize "On"
    runtime "Release" -- 运行时链接的dll是release类型的
...

能不能在submodule里添加文件
答案是否定的,我在加入ImGui库的时候,原本是直接使用的GitHub上的原仓库作为submodule的,问题在于,原仓库里面是没有对应的premake5.lua文件的,这就很尴尬了,因为我用的premake5构建的工程,所以必须在对应的submodule下面保证要有premake5.lua文件才行,所以我就在寻找怎么在submodule里添加文件的方法,最后发现,submodule本身就是一个整体,不应该自己在里面加东西,那这个怎么办呢?

答案是使用GitHub的Fork功能,舍弃在submodule里加文件的方法,而是去自己在Server上Copy对应的工程,自己在该工程就有权限添加premake5.lua文件了,提交了之后,再把自己Fork出来仓库的URL作为新的Submodule的URL就可以了。

glew和glad库的区别
之前学OpenGL的时候,前面既可以用glew,也可以用glad,两个的作用是类似的,都是为了连接C++代码与glfw库做的准备工作,具体目的有:

  • check out our kind of graphics drive a dll
  • load certain functions that we know are in there so we can call from c++ code

OpenGL仅仅是一种标准,显卡驱动的制造商根据OpenGL这种标准,基于不同的特定的显卡,来实现这些标准,OpenGL驱动有很多个版本,其中函数的位置不可以在Compile-time期间得到,而得在运行时得到,所以开发者需要在运行时得到这些函数的位置,这样才能对其进行调用,具体在Windows上的操作与下面的函数类似:
(Because OpenGL is only really a standard/specification it is up to the driver manufacturer to implement the specification to a driver that the specific graphics card supports. Since there are many different versions of OpenGL drivers, the location of most of its functions is not known at compile-time and needs to be queried at run-time. It is then the task of the developer to retrieve the location of the functions he/she needs and store them in function pointers for later use. Retrieving those locations is OS-specific. In Windows it looks something like this:)

// 声明一类函数指针,这种函数用来创建Buffer,第一个参数是int类型,第二个参数是uint指针
typedef void (*GL_GENBUFFERS) (GLsizei, GLuint*);
// 再去对应的进程里找到名为glGenBuffers的函数,将其函数指针付给glGenBuffers
GL_BUFFERS glGenBuffers = (GL_GENBUFFERS)wglGetProcAddress("glGenBuffers");
// 然后进行使用
unsigned int buffer;
buffer = glGenBuffers(1, &buffer);

如果每个函数都这么整,那也太麻烦了,所以glad库就为我们做了这一部分的工作,可以看一下glad里面的部分内容:

#define glGenBuffers glad_glGenBuffers
typedef GLboolean (APIENTRYP PFNGLISBUFFERPROC)(GLuint buffer);
GLAPI PFNGLISBUFFERPROC glad_glIsBuffer;

// 当前平台下
define GLAPI extern
#define APIENTRY    WINAPI
#define WINAPI      __stdcall


static void load_GL_VERSION_1_5(GLADloadproc load) {
	if(!GLAD_GL_VERSION_1_5) return;
    ...
	glad_glGenBuffers = (PFNGLGENBUFFERSPROC)load("glGenBuffers");
	...
}

glfw和opengl的区别

GLFW is a lightweight utility library for use with OpenGL.GLFW stands for Graphics Library Framework. It provides programmers with the ability to create and manage windows and OpenGL contexts, as well as handle joystick, keyboard and mouse input.

首先,glfw是OpenGL的一个轻量级的工具库,opengl里面的内容都是比较底层的、负责渲染的API。opengl里并没有任何Window的概念,也没有任何外设输入的概念,在使用OpenGL的过程中,有一个概念叫做OpenGL contex,OpenGL库本身是无法创建这个contex的,可以回顾一下,学习OpenGL时,写过下面的代码:

GLFWwindow *window = glfwCreateWindow(500, 500, "First Window", NULL, NULL);
glfwWindowHint(GLFW_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_VERSION_MINOR, 3);
glfwMakeContextCurrent(window1);
if (glewInit() != GLEW_OK)
{
    cout << "Failed to initialze glew!!" << endl;
	glfwTerminate();
	return -1;
}

contex相当于一个承载OpenGL数据的容器,有人翻译为OpenGL的上下文:

Think of an OpenGL context as some internal data that represents the current state of the OpenGL system.

Different platforms (such as Windows, Linux/X, OSX, iOS, Android) provide different mechanisms for creating contexts. For example Windows has its own function calls that allow the creation of OpenGL contexts that are very different from how OSX creates a context. Additionally Windows provides functions to support things like mouse events. This is where GLFW comes in: it provides a cross-platform library to handle the things that OpenGL doesn’t handle, like creating contexts and handling mouse events. By and large it’s not a wrapper library on top of OpenGL though it does provide some a small amount of functionality like this: like automatically building mip-map levels.


C++报错问题
如下所示,两种写法,第一种写法是错的,第二种的是对的:

	class HAZEL_API Application
	{
	public:
		Application();
		virtual ~Application();
		inline static Application Get() { return *s_Instance;  } // 编译报错
        inline static Application& Get() { return *s_Instance;  } // 编译正常
		void OnEvent(Event& e);
		void Run();
		bool OnWindowClose(WindowCloseEvent& e);
		void PushLayer(Layer* layer);
		Layer* PopLayer();
	private:
		static Application* s_Instance;

	protected:
		LayerStack m_LayerStack;
		std::unique_ptr<Window>m_Window;
		bool m_Running = true;
	};

	Application* CreateApplication();

需要研究一下这一行报错是什么意思,如下图所示:
在这里插入图片描述
报错的信息为function "Constructor throw()" (declared implicitly) cannot be referendced -- it is a deleted function

然而在前面自己加一个默认构造就可以编译通过了,如下图所示:
在这里插入图片描述

原因:还是因为C++不够熟悉,这里的Get如果前面不加&符号,意味着返回的在函数中创建并返回一个Application对象,不过这里提到的it is a deleted function指的是复制构造函数,默认是没有复制构造函数的。
至于这里的,复制构造函数,到底默认是deleted,还是已经被Compiler自动生成了的问题remain,留着参考:
https://stackoverflow.com/questions/4943958/conditions-for-automatic-generation-of-default-copy-move-ctor-and-copy-move-assi#:~:text=The%20copy%20constructor%20is%20auto,(%C2%A712.8%2F8).


SetKeyCallback与SetCharCallback
使用ImGui时,需要额外添加KeyTypedEvent,而用SetKeyCallback不能代替这个事件吗,两个函数有啥区别?
看了一下相关解释,SetKeyCallback是用来判断物理上,一个键是否被按下的事件的,而SetCharCallback在乎的是,特定的字符是否被输入,所以在范围上,KeyCallback比CharCallback要广,因为不是所有的Key都代表着可以输入的字符。


基类的static成员的初始化
之前确认过,对于非static成员的继承,分为public、private和protected三种方式,如下图所示:
在这里插入图片描述
好像对于static函数,不是这样,因为我看到了这样的代码:

========在Input.h中========
class HAZEL_API Input 
{
	...
private:
	static Input* s_Instance;
};

========在WindowsInput.cpp中========
// WindowsInput是Input的子类
Input* Input::s_Instance = new WindowsInput();

这里的子类居然可以直接初始化基类的私有static成员,所以这里问题就来了,我们知道普通成员在不同继承模式下的访问性,那么不同继承模式下,static成员的访问性又是怎样的呢?

答案是,一样的,继承基类时,static成员的访问性与non-static成员的访问性是一样的,举个例子:

class Base 
{
public:
	static void A() {};
protected:
	static void B() {};
private:
	static void C() {};
};

class Derived1 : public Base {
	static void func() 
	{
		A();//成功
		B();//成功
		C();//编译Error,不可以访问C
	}
};

而上述的代码Input* Input::s_Instance = new WindowsInput();之所以成立,是因为这一行代码虽然是写在WindowsInput.cpp中,但是可以说跟WindowsInput类没有任何关系,初始化的作用域是全局范围的,不在类的范围内,所以在全局范围初始化类的私有静态成员,是没有问题的

posted @ 2020-11-12 23:16  弹吉他的小刘鸭  阅读(395)  评论(0编辑  收藏  举报