Hazel引擎學習(一)

Project Setup

1.SetUp項目,生成的文件在bin目錄下,生成的intermediate文件在bin-int目錄下,大概是這麼個目錄結構

bin/Debug-x64/ProjectName/
bin-int/Debug-64x/ProjectName/

注意這裏的當前路徑都是取的該vxproj文件所在的路徑,所以如下圖所示,我這麼寫目錄結構
在這裏插入圖片描述
各自對應的bin和bin-int都會在對應的項目文件夾裏,如下圖所示:
在這裏插入圖片描述
如果要想統一生成在同一個文件夾,能不能這麼寫:
在這裏插入圖片描述
然而這樣VS會警告你,因爲把不同的Project裏生成的東西放在了同一個文件夾下,這樣是不好的,所以最終應該這麼寫:
在這裏插入圖片描述

2.兩個工程,一個叫Hazel,作爲引擎,一個叫SandboxApp,作爲實際使用的例子, 一個是dll,一個是exe
3.可能要編輯solution的順序,讓sandboxApp在最上面,最爲setup project
4.只保留64位平臺
5.Hazel相關內容應該放在對應的namespace裏面


Entry Point

主要是以下幾個任務

  1. 建立宏,來實現dllimport和dllexport,這次的import的內容不再是函數了,而是類Application,宏HZ_BUILD_DLL用來區分是否是dll項目,宏HAZEL_API用來表示dllexport或dllimport,最後加一個平臺宏HZ_PLATFORM_WINDOWS,確保只在64位下進行
  2. 建立Apllication文件,然後在exe裏實現public繼承
  3. 定義extern Application* CreateApplication接口,然後這個接口由exe具體實現
  4. main函數放在Hazel引擎下的EntryPoint的頭文件裏,記得加宏
  5. Application作爲基類,其析構函數爲虛函數

目錄結構如下,Core.h用來存放宏的定義,Application用來定義基類
在這裏插入圖片描述
創建EntryPoint
從上面的步驟可知,如果要使用Engine的內容,需要包含Core.h、Application.h,以後還可能會有更多內容,所以爲了避免這個情況,添加一個EntryPoint頭文件,把所有需要的都放在裏面,這樣Sandbox就只需要Include一個文件了,在這裏,這個EntryPoint文件,叫做Hazel.h

爲什麼這麼設計
這節課的主要目的是創建一個dll對應的Main函數,進行Engine對應的操作,同時,封裝一個接口函數,用於創建對應的Application,具體創建Application對象的任務交給了對象,對象需要new出自己的應用對象,繼承於Engine裏的Application基類,然後把這個指針傳過來,之後的內存釋放就交給引擎來處理了

碰到的小問題
使用dllexport來導出類的時候,發現對應的dll能生成,但是lib文件不會生成了,而且提示這個報錯:

__declspec(dllexport)': "ignored on left of when no variable is declared"\

仔細閱讀代碼後,發現是_delcspec(dllexport)應該寫在class的左邊:

_delcspec(dllexport) class MyClass // 錯誤的寫法
class _delcspec(dllexport) MyClass // 正確的寫法

LogSystem

主要是以下幾個任務

  1. 創建Log類,然後有s_CoreLogger和s_ClientLogger,分別處理引擎的log和client的log
  2. 使用spdlog,具體主要是怎麼利用git submodule使用該庫
  3. 使用宏來封裝對應的log函數,使用宏可以更好的方便不同平臺的應用

遊戲引擎少不了LogStystem,這裏使用了別人做好的Github項目,叫做spdlog,Cherno使用這個Molude的方式很特別,用到了Github的submodule的特性,這樣的好處是,可以直接update該module,而不用再從網上Copy和Paste這個項目,具體操作如下,在對應的Git Bash界面,輸入

//git submodule add 對應GitHub項目的url 對應的文件夾路徑
git submodule add https://github.com/gabime/spdlog.git Hazel/vendor/spdlog

就一行命令,就能創建對應的文件夾,clone該倉庫,而且會在當前目錄生成一個類似於.gitignore的.submodule文件,具體內容如下圖所示:
在這裏插入圖片描述
關於git submodule add操作,如果網不好的下載速度很慢,如果失敗了再add可能會出問題,如果出問題了,可以去把.git文件的modules下對應的文件夾刪除,再進行add,我就是這樣弄成功的

對於這個LogSystem,底層使用的是spdlog,上層當然得封裝一層Hazel的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;
	std::shared_ptr<spdlog::logger>Log::s_CoreLogger = nullptr;

這裏報了個錯,記得C++Class內的static對象,需要在類外進行定義,不能只進行聲明。

額外的宏操作,這裏的宏的寫法可以實現函數的宏,代碼如下所示:

#define LOG(...)      ::Hazel::Log::GetClientLogger()->info(__VA_ARGS__)
#define LOG_WARNING(...)   ::Hazel::Log::GetClientLogger()->warn(__VA_ARGS__)
#define LOG_ERROR(...)     ::Hazel::Log::GetClientLogger()->error(__VA_ARGS__)

最後效果如圖,還挺好看的
在這裏插入圖片描述
有意思的是 spdlog庫的核心內容都是在頭文件裏通過內聯函數使用的,所以只需要包含其頭文件就可以,文件裏的其他所有文件都被設置爲了Excluded From Project,如下圖所示,然後我做了個測試,刪除了所有的cpp文件,一樣能夠成功build:
在這裏插入圖片描述


編寫Premake5.lua管理項目

官方給了個簡單的模板:

-- premake5.lua
workspace "HelloWorld"
   configurations { "Debug", "Release" }

project "HelloWorld"
   kind "ConsoleApp"
   language "C"
   targetdir "bin/%{cfg.buildcfg}"

   files { "**.h", "**.c" }

   --這裏面寫附加庫的頭文件目錄,對應的VS項目屬性裏的Additional Include Directories
   include {}
   --filter後面一般加特定平臺的Configurations,其範圍會一直持續到碰到下一個filter或project
   filter "configurations:Debug"
      defines { "DEBUG" }
      symbols "On"
   --到這,碰到fitler,上面的filter對應Debug平臺下的filter範圍結束
   filter "configurations:Release"
      defines { "NDEBUG" }
      optimize "On"

這裏的filter一般限定了範圍,比如說特定的Windows平臺,如下所示:

--比如說windows平臺下的filter
filter "system:windows"
   cppdialect "C++17"
   staticruntime "On"   --表示會Link對應的dll
   systemversion "latest" --使用最新的windows sdk版本,否則會默認選擇8.1的版本
  
filter "configurations:Release"
   defines { "NDEBUG" }
   optimize "On"

如上所示,filter相當於篩選器,上述寫法,如果是安卓平臺的Release模式,則下面的filter還是會執行,如果想限定兩個,比如只生在windows的Release情況下的filter,則應該這麼寫

filter {"system:windows", "configurations:Release"}
   cppdialect "C++17"
   staticruntime "On"   --表示會Link對應的dll
   systemversion "latest" --使用最新的windows sdk版本,否則會默認選擇8.1的版本
   defines { "NDEBUG" }
   optimize "On"

學到了一個單詞,叫做token,我原本以爲叫做Macro,token表示一些代表符號,比如VS裏的$(SolutionDir),而Premake的宏大概是這麼寫:

%{wks.name}
%{prj.location}
%{cfg.targetdir}

在Github上Premake的Wiki界面搜索Token可以找到對應的一些符號,如下所示:

wks.name
wks.location -- (location where the workspace/solution is written, not the premake-wks.lua file)

prj.name
prj.location -- (location where the project is written, not the premake-prj.lua file)
prj.language
prj.group

cfg.longname
cfg.shortname
cfg.kind
cfg.architecture
cfg.platform
cfg.system
cfg.buildcfg
cfg.buildtarget -- (see [target], below)
cfg.linktarget -- (see [target], below)
cfg.objdir

file.path
file.abspath
file.relpath
file.directory
file.reldirectory
file.name
file.basename -- (file part without extension)
file.extension -- (including '.'; eg ".cpp")

-- These values are available on build and link targets
-- Replace [target] with one of "cfg.buildtarget" or "cfg.linktarget"
--   Eg: %{cfg.buildtarget.abspath}
[target].abspath
[target].relpath
[target].directory
[target].name
[target].basename -- (file part without extension)
[target].extension -- (including '.'; eg ".cpp")
[target].bundlename
[target].bundlepath
[target].prefix
[target].suffix

還可以使用postbuildcommand來實現build完成之後的文件拷貝和複製工作,如下所示:

postbuildcommand
{
    -- %{cfg.buildtarget.relpath}是生成文件,相較於當前premake5.lua文件的相對路徑
    {"COPY" %{cfg.buildtarget.relpath} ../.. output../Sandbox"}
}

編寫Premake5.lua文件的一點疑惑
由於Sanbox是使用了Hazel的文件的,所以我這麼寫是對的:

	files { "%{prj.name}/Src/**.h", "%{prj.name}/Src/**.cpp" }
    includedirs
	{
		"Hazel/vendor/spdlog/include",
		"Hazel/Src"
	}

但爲什麼不可以像下面這麼寫呢:

	files { "%{prj.name}/Src/**.h", "%{prj.name}/Src/**.cpp", "Hazel/Src/**.h" } -- 直接把所有的.h文件加進來,而不是include
    includedirs
	{
		"Hazel/vendor/spdlog/include",
		--"Hazel/Src"
	}

具體原因涉及到了VS尋找Header文件的方式了, Visual Studio looks for headers in this order:

  • In the current source directory.
  • In the Additional Include Directories in the project properties (Project -> [project name] Properties, under C/C++ | General).
  • In the Visual Studio C++ Include directories under Tools → Options → Projects and Solutions → VC++ Directories.
  • In new versions of Visual Studio (2015+) the above option is deprecated and a list of default include directories is available at Project Properties → Configuration → VC++ Directories

儘管這裏在項目裏看得到該目錄,實際上該位置VS是不知道的,也就是說,vs會先去尋找sandbox所在的目錄,然後找附加include目錄,等等,自然會找不到,因爲這些文件在Hazel文件夾下,如下圖所示:
在這裏插入圖片描述
如果在SandboxApp.cpp所在的文件夾加入Hazel.h等相關的所有頭文件,則會開始編譯Hazel.h了,但是由於Hazel.h裏的內容如下,

#pragma once

//
#include "Hazel/Core.h"

#include "Hazel/Application.h"

//---------EntrtryPoint ------------
#include "Hazel/EntryPoint.h"
//----------------------------------

#include "Hazel/Log.h"

又會這樣報錯:
在這裏插入圖片描述
問題就在於,我是這麼放置路徑的啊,問題肯定出在,對於Hazel.h的位置,不是這個路徑:
在這裏插入圖片描述
突然意識到,之前寫的是Hazel/Core.h,Hazel文件夾裏放Core.h,這麼寫果然就可以編譯了:
在這裏插入圖片描述
總結一 對於每一個cpp,VS都會先針對其cpp所在的目錄,尋找頭文件(頭文件裏包含的頭文件的路徑也是基於該CPP),然後再去Additional Include Directory裏找,最後去VC++ Directories裏找。

總結二 首先,對於Sanbox工程,在premake5.lua裏把所有的Hazel工程的頭文件放在files裏並不合適,而是應該用Addtional Include Files, 如果加到files裏,會看到如下所示的相同的文件列表,至於到底能不能這麼做,我猜是理論上是可以的,那我的步驟就變成了:一. 把對應的頭文件加到自己新的工程裏;二. 把頭文件複製到自己的cpp所在的目錄,而且要保證頭文件的相對於自己的cpp層次結構都是對的(感覺很麻煩),而且如果自己引用的cpp目錄結構不同,很可能出問題,或者,把原來的頭文件目錄加入到AddtionalIncludeDirectory裏,但這不就是脫褲子fp麼,那又何必把所有的Hazel工程的頭文件放在Sandbox的工程files列表裏呢?
在這裏插入圖片描述


EventSystem

思路是由Application創建自己的窗口window,對應的window類不應該知道任何Application的信息,所以Application在創建自己的window時,還要同時創建一個callback


這一塊是難啃的骨頭,首先學習一個C++的名詞,叫做std::function

std::function
Class template std::function is a general-purpose polymorphic function wrapper. Instances of std::function can store, copy, and invoke any Callable target – functions, lambda expressions, bind expressions, or other function objects, as well as pointers to member functions and pointers to data members.
The stored callable object is called the target of std::function. If a std::function contains no target, it is called empty. Invoking the target of an empty std::function results in std::bad_function_call exception being thrown.
std::function satisfies the requirements of CopyConstructible and CopyAssignable.

std::function可以用來存儲callable object,這也叫做the target of std::function,函數也是callable object,示例如下,感覺很像是一個函數指針:

#include <iostream>
#include <functional>
void print(int n)
{
	std::cout << n << "\n";
}

int main()
{
	std::function <void(int)> a = print;
	a(2);
}

不過需要注意的是,std::function不能像一般的函數重載那樣,如果我下面這麼寫,會報錯

#include <iostream>
#include <functional>

void print()
{
	std::cout << 0 << "\n";
}

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

int main()
{
	std::function <void()> a = print;
	std::function <void(int)> b = print;
	a();
	b(2);
}

如下圖所示,大意是不知道print到底是什麼類型
在這裏插入圖片描述
要寫,得這麼寫,用static_cast進行轉換,具體原因我也不理解,以後再深入把,現階段就把std::funtion當作一個函數指針的wrapper用:

int main()
{
	typedef void(*fun_ptr_a)();
	typedef void(*fun_ptr_b)(int);

	std::function <void()> a = static_cast<fun_ptr_a>(print);
	std::function <void(int)> b = static_cast<fun_ptr_b>(print);

	a();
	b(1);
}

再介紹一個Hazel代碼裏涉及到的問題,C++中的enum和enum class

C++中的enum和enum class
C++11中引入了enum class,旨在解決原來的enum的各種缺點,在這裏介紹一下之前的Enum類型的三大缺點:

  • C++原本的enum值會被隱式轉換(implicitly convert)爲int類型,這跟C#就不一樣,也很不科學
  • C++裏的enum好像是一個全局的範圍,類似於全局變量,而且沒有前綴,這很容易造成命名衝突和理解錯誤。如下圖所示:
    在這裏插入圖片描述
  • C++的enum,無法規定用多少位的數據結構去存儲它,存儲的類型可能是char、short、int等類型,選擇一個夠用的數據結構就行,舉個例子:
// 儘管選只用8位,但並不可以保證
enum E_MY_FAVOURITE_FRUITS
{
    E_APPLE      = 0x01,
    E_WATERMELON = 0x02,
    E_COCONUT    = 0x04,
    E_STRAWBERRY = 0x08,
    E_CHERRY     = 0x10,
    E_PINEAPPLE  = 0x20,
    E_BANANA     = 0x40,
    E_MANGO      = 0x80,
    E_MY_FAVOURITE_FRUITS_FORCE8 = 0xFF // 'Force' 8bits, how can you tell?
};

如果加一行下列語句,編譯器並不會報錯,而是會換另一個更大的數據結構,可能是short,去存儲

E_DEVIL_FRUIT  = 0x100, // New fruit, with value greater than 8bits

本來想用8位數保存enum,這種enum就很不安全。而C++11就可以這麼寫:

enum class E_MY_FAVOURITE_FRUITS : unsigned char
{
    E_APPLE        = 0x01,
    E_WATERMELON   = 0x02,
    E_COCONUT      = 0x04,
    E_STRAWBERRY   = 0x08,
    E_CHERRY       = 0x10,
    E_PINEAPPLE    = 0x20,
    E_BANANA       = 0x40,
    E_MANGO        = 0x80,
    E_DEVIL_FRUIT  = 0x100, // Warning!: constant value truncated
};

所以,enum class 比enum好,好在以下三點:

  • They don’t convert implicitly to int.
  • They don’t pollute the
    surrounding namespace.
    They can be forward-declared.

使用#和##來實現宏操作
在C++的宏中,使用#,可以把#後面的內容加上""符號,當作字符串處理,如下所示:

#include <iostream>

// 碰到ToString(x)這種東西,就把x轉換爲字符串代替原來的部分
#define ToString(s) #s

int main(int argc, char *argv[])
{
	std::cout << ToString(3413SF345人sss) << std::endl;
	return 0;
}

輸出:3413SF345人sss

前面說的#符號是將其轉換成字符串,那麼##符號就是單純的字母替換,舉個例子:

#include <iostream>

// 碰到print(s, f)這種東西,就把x轉換爲字符串代替原來的部分
#define print(s, f) {std::cout << "輸出字符s:" << #s << std::endl;\
std::cout << "輸出乘積" << ints##f << std::endl;\
}\

int main(int argc, char *argv[])
{
	int ints8 = 3;
	print(hehe, 8);
	return 0;
}

輸出如下,其實就是一些文字代替的簡單東西,看習慣了就好了:

輸出字符s:hehe
輸出乘積3

C++虛函數的override關鍵字
關於基類,其虛函數要聲明爲virtual這是自然是,在C++裏對於子類複寫的函數,需不需要加virtual呢?而且在C++11中,提出了新的關鍵字override,這個關鍵字又有什麼用呢?

首先看這麼個例子,下面四種寫法都可以成功編譯:

class Base
{
public:
	virtual void print() const = 0;
	virtual void printVirtual() const = 0;
	virtual void printOverride() const = 0;
	virtual void printBoth() const = 0;
};

class inhert : public Base
{
public:
	// only virtual keyword for overriding.
	void print() const {}	// 什麼都不加

	virtual void printVirtual() const {}	// 只加virtual

	// only override keyword for overriding.
	void printOverride() const override {} // 只加override

	// using both virtual and override keywords for overriding.
	virtual void printBoth() const override {} // 既加virtual,也加override
};

直接說重點,對於override的函數,加不加virtual都無所謂,只要基類的虛函數加了virtual就可以了,但override關鍵字就重要一點了,它可以避免一些bug,因爲我們寫函數,如果不加override的時候,一旦函數類型寫錯了,這個函數其實就是一個新的函數,並沒有override原來的虛函數,但是編譯器不會提示這個錯誤,這就很蛋疼了,比如下面這個例子:

class Base
{
public:
	virtual void print1(int a, unsigned char c) const = 0;
	virtual void print2(int a, unsigned char c) const = 0;
	virtual void print3(int a, unsigned char c) const = 0;
};

class inhert : public Base
{
public:
	void print1(int a, char c) const
	{
		// Do Something 
	}

	virtual void print2(int a, char c) const
	{
		// Do Something 
	}
	
	void print3(int a, char c) const override //注意override關鍵字放在const後面
	{
		// Do Something 
	}
};

看看結果,發現寫了override的時候纔會報錯,否則能正常編譯,完全不會進行提示:
在這裏插入圖片描述
結論: 以後在定義任何虛函數的時候,都要在聲明的最後一句加上關鍵字override,這是一個代碼規範的好習慣


順便提一個有意思的東西,就是const關鍵字與函數重載的關係
const與函數重載
函數重載與三個東西有關,參數類型,參數個數和參數之間的順序有關,但是與返回值無關,那麼先提兩個問題:

  1. 在類中 void A(){} 和 void A() const{} 是一個函數還是兩個函數
  2. 在類中 void A(int){} 和 void A(const int){} 是一個函數還是兩個函數

首先看第一個問題,可以舉一個例子:

#include<iostream> 
using namespace std;

class Test
{
protected:
	int x;
public:
	Test(int i) :x(i) { }
	void fun() const
	{
		cout << "fun() const called " << endl;
	}
	void fun()
	{
		cout << "fun() called " << endl;
	}
};

int main()
{
	Test t1(10);
	const Test t2(20);
	t1.fun();
	t2.fun();
	return 0;
}

輸出結果:

fun() called
fun() const called

結果能成功運行,且調用了不同的函數,這說明類的成員函數,後面加const和不加const,是兩個函數,這是函數重載(C++ allows member methods to be overloaded on the basis of const type)

再看第二種問題在類中 void A(int){} 和 void A(const int){} 是一個函數還是兩個函數,這個問題可以換成,參數里加不加const,構不構成函數重載?

先直接說結論,如果參數是pointer類型或Reference,那麼加const屬於函數重載,但是其他類型就不屬於函數重載了,看下面的代碼,這種情況會報錯:

#include<iostream> 
using namespace std; 
  
void fun(const int i) 
{ 
    cout << "fun(const int) called "; 
} 
void fun(int i) 
{ 
    cout << "fun(int ) called " ; 
} 
int main() 
{ 
    const int i = 10; 
    fun(i); 
    return 0; 
} 

輸出:

Compiler Error: redefinition of 'void fun(int)'

也很好理解,傳一個int類型,傳遞的是值,這個int又不會改變什麼東西,所以值類型,加不加const都一樣,但是如果用引用,那就完全不一樣了,所以這麼寫是可以的:

#include<iostream> 
using namespace std;

void fun(const int& i)
{
	cout << "fun(const int) called \n";
}
void fun(int& i)
{
	cout << "fun(int ) called \n";
}

int main()
{
	const int i1 = 10;
	int i2 = 10;
	fun(i1);
	fun(i2);
	return 0;
}

輸出:

fun(const int) called
fun(int ) called

Event代碼編寫
OK,介紹完了這個,可以正式開始Event的設計了,首先需要定義的就是Event和EventType類,這裏把Event作爲基類,EventType是enum class,包含了基本的外設事件,如下所示:

	enum class EventType
	{
		None = 0,
		WindowClose, WindowResize, WindowFocus, WindowLostFocus, WindowMoved,//窗口的五種操作,0x001,0x010,0x011,0x100,0x
		AppTick, AppUpdate, AppRender,//APP的操作,暫時先不計較這個
		KeyPressed, KeyReleased,//鍵盤的兩種操作
		MouseButtonPressed, MouseButtonReleased, MouseMoved, MouseScrolled//鼠標的四種操作
	};

對於Event類型,作爲基類,那麼最基本的兩個接口應該爲:

  • 獲取該事件的類型
  • 獲取該事件的名字

作爲基類,這都是最基本的API,再者,爲了方便使用,仿照C#的方式,C#語言裏所有的Object都有一個ToString函數,方便我們打印一些消息,所以這個API我們也把它加入到Event基類裏,如下所示:

	class HAZEL_API Event
	{
	public:
		virtual EventType GetEventType() const = 0;
		virtual const char* GetName() const = 0;
		virtual const char* ToString() const = 0;
	};

目前就是這樣,然後我們還需要一個EventTypeFlag枚舉,以後用flag來快速篩選特定的Event:

#define BIT(x) (1 << x)
	// events filter
	enum EventCategory
	{
		None = 0,
		EventCategoryApplication    = BIT(0),
		EventCategoryInput          = BIT(1),
		EventCategoryKeyboard       = BIT(2),
		EventCategoryMouse          = BIT(3),
		EventCategoryMouseButton    = BIT(4)
	};

定義Event這個基類,就可以着手創建對應的子類的,拿鼠標事件舉例,一共有MouseMoved、MouseButtonPressed、MouseButtonReleased、MouseButtonScrolled四種,那麼我就建立四個子類,全放在MouseEvent.h文件下,拿MouseMoved舉例,其類型爲之前枚舉定義的MouseMovedEvent,其ToString應該是打印出鼠標移動的offset值,由於該類的所有Event類型都是一樣的,所以我們可以用一個stati變量去存儲該類型就夠了:

class MouseMovedEvent : public Event
{
public:
	static EventType GetStaticType() { return EventType::MouseMoved; }
	const EventType GetEventType() const override { return GetStaticType(); }
	const char* GetName() const override { return "MouseMoved"; }
	std::string ToString()
	{
		// Create a string with represents 
		std::string a = "MouseMovedEvent: xOffset = " + GetXOffset() + ", yOffset = " + GetYOffset();
		return a;
	}

	inline float GetXOffset() const { return m_xOffset; }
	inline float GetYOffset() const { return m_yOffset; }
private:
	float m_xOffset, m_yOffset;
};

對於string這種拼接,想象的很美好,可惜C++並不能像C#這樣簡單的組成一個字符串,所以我們需要通過stringstream來完成這個功能,寫法如下:

std::string ToString() const override
{
	std::stringstream a;
	a << "MouseMovedEvent: xOffset = " << GetXOffset() << ", yOffset = " << GetYOffset();
	return a.str();
}

OK,寫完了這個類,就可以繼續寫類似的鼠標事件的類了,但是我發現有一些代碼都是非常類似的,寫起來很麻煩,也很影響閱讀:

class MouseMovedEvent : public Event
{
public:
	static EventType GetStaticType() { return EventType::MouseMoved; }
    const EventType GetEventType() const override { return GetStaticType(); }
	const char* GetName() const override { return "MouseMoved"; }
	...
}
class MouseButtonPressedEvent: public Event
{
public:
	static EventType GetStaticType() { return EventType::MouseButtonPressed; }
	const EventType GetEventType() const override { return GetStaticType(); }
	const char* GetName() const override { return "MouseButtonPressed"; }
	...
}

class MouseButtonReleasedEvent: public Event
{
public:
	static EventType GetStaticType() { return EventType::MouseButtonReleased; }
	const EventType GetEventType() const override { return GetStaticType(); }
	const char* GetName() const override { return "MouseButtonReleased"; }
	...
}

所以我學到了一個方法,用宏去代替我們編寫這麼長的語句,這個宏名就叫做EVENT_CLASS_TYPE(typename),來爲我們生成對應的函數,通過###符號,可以達到這種效果,別記反了,一個#是轉換成字符串,兩個#是原語句替換,所以就是這麼簡化:

#define EVENT_CLASS_TYPE(type) \
static EventType GetStaticType() { return EventType::##type; }\
const EventType GetEventType() const override { return GetStaticType(); }\
const char* GetName() const override { return #type; }

class MouseMovedEvent : public Event
{
public:
	EVENT_CLASS_TYPE(MouseMoved)
	...
}

就像這樣,我們可以把所有鼠標事件的類定義好,接下來還需要的定義的輸入類就是Window Event、ApplicationEvent和KeyEvent,先說前兩種Event,

最後着重提一下KeyEvent,鍵盤事件的輸入處理並不像點擊鼠標那麼簡單,通常(簡單的事件系統裏)我們是沒有長按鼠標的操作的,但是卻有長按鍵盤的操作,當我們按鍵盤時,會先打印一個字母,然後停頓一下,如果這個時候還按着按鈕,就繼續打印剩餘的字母。

所以說,按鍵的時候,第一次會立馬打印第一個字母,然後需要記錄我按的次數(或者記錄按的時間),當記錄的值達到一定閾值(或時間)時,纔會繼續不停打印接下來的字母,這裏我們不用時間記錄,而是用一個int值,記錄按相同鍵的次數。

設計KeyEvent類的時候,可以發現,KeyPressedEvent會比KeyReleasedEvent的數據多一個,前者會額外記錄按下Key時,key走過的Loop的總數,所以這個時候可以設計一個基類叫做KeyEvent,這裏放通用的數據,就是Key的keycode,用於存放Key類型共有的內容,設計思路如下所示:

class HAZEL_API KeyEvent : public Event
{
public:
   inline int GetKeycode() const { return keycode;}
   
protected:
	// 構造函數設爲Protected,意味着只有其派生類能夠調用此函數
	KeyEvent(int code): keycode(code){} 
	int keycode;
};

然後再寫對應的子類

class HAZEL_API KeyPressedEvent : public KeyEvent
{
public:
	KeyPressedEvent(int keycode, int repeatCount)
		: KeyEvent(keycode), m_RepeatCount(repeatCount) {}

	inline int GetRepeatCount() const { return m_RepeatCount; }

	std::string ToString() const override
	{
		std::stringstream ss;
		ss << "KeyPressedEvent: " << m_KeyCode << " (" << m_RepeatCount << " repeats)";
		return ss.str();
	}

	EVENT_CLASS_TYPE(KeyPressed)
private:
	int m_RepeatCount;
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章