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
主要是以下幾個任務
- 建立宏,來實現dllimport和dllexport,這次的import的內容不再是函數了,而是類Application,宏HZ_BUILD_DLL用來區分是否是dll項目,宏HAZEL_API用來表示dllexport或dllimport,最後加一個平臺宏HZ_PLATFORM_WINDOWS,確保只在64位下進行
- 建立Apllication文件,然後在exe裏實現public繼承
- 定義extern Application* CreateApplication接口,然後這個接口由exe具體實現
- main函數放在Hazel引擎下的EntryPoint的頭文件裏,記得加宏
- 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
主要是以下幾個任務
- 創建Log類,然後有s_CoreLogger和s_ClientLogger,分別處理引擎的log和client的log
- 使用spdlog,具體主要是怎麼利用git submodule使用該庫
- 使用宏來封裝對應的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與函數重載
函數重載與三個東西有關,參數類型,參數個數和參數之間的順序有關,但是與返回值無關,那麼先提兩個問題:
- 在類中 void A(){} 和 void A() const{} 是一個函數還是兩個函數
- 在類中 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;
};