<2021SC@SDUSC>
開源游戲引擎 Overload 代碼模塊分析 之 OvTools(四)—— Utils(上)
目錄
- 前言
- Utils 模塊概述與探究計劃
- 分析
- 1、PathParser
- 1.1 PathParser.h
- 1.1.1 頭檔案
- 1.1.2 主體代碼
- 1.2 PathParser.cpp
- 1.2.1 頭檔案
- 1.2.2 主體代碼
- MakeWindowsStyle() 函式
- MakeNonWindowsStyle() 函式
- GetContainingFolder() 函式
- GetElementName() 函式
- GetExtension() 函式
- FileTypeToString() 函式
- GetFileType() 函式
- 2、Random
- 2.1 Random.h
- 2.1.1 頭檔案
- 2.1.2 主體代碼
- 2.2 Random.cpp
- Generate() 函式
- CheckPercentage() 函式
- 總結
前言
本篇是開源游戲引擎 Overload 模塊 OvTools 的第四篇分析,想大致了解 Overload 可前往這篇文章,想看其他相關文章請前往筆者的 Overload 專欄自主選擇,
本篇將探究 OvTools 的最后一個小模塊:Utils,我們先來大致了解其檔案有哪些并計劃一下探究行程吧!
Utils 模塊概述與探究計劃
Utils 模塊是多種工具(例如亂數生成、系統呼叫等)的集合,其包含檔案如下:


顯然,該小模塊包括了 PathParser、Random、ReferenceOrValue、SizeConverter、String、SystemCalls 六個部分,筆者快速瀏覽了一下代碼長度與復雜度,計劃分為三篇文章進行探究,本篇我們將先探究前兩個部分:PathParser 與 Random,
分析
1、PathParser
1.1 PathParser.h
1.1.1 頭檔案
該檔案僅包含了一個 string 頭檔案,不多贅述:
#include <string>
1.1.2 主體代碼
該檔案的主體代碼是一個 PathParser 類及其函式定義,該類的作用是提供一些工具來獲得給出路徑的相關資訊,代碼如下:
class PathParser
{
public:
enum class EFileType
{
UNKNOWN,
MODEL,
TEXTURE,
SHADER,
MATERIAL,
SOUND,
SCENE,
SCRIPT,
FONT
};
/**
* Disabled constructor
*/
PathParser() = delete;
/**
* Returns the windows style version of the given path ('/' replaced by '\')
* @param p_path
*/
static std::string MakeWindowsStyle(const std::string& p_path);
/**
* Returns the non-windows style version of the given path ('\' replaced by '/')
* @param p_path
*/
static std::string MakeNonWindowsStyle(const std::string& p_path);
/**
* Returns the containing folder of the file or folder identified by the given path
* @param p_path
*/
static std::string GetContainingFolder(const std::string& p_path);
/**
* Returns the name of the file or folder identified by the given path
* @param p_path
*/
static std::string GetElementName(const std::string& p_path);
/**
* Returns the extension of the file or folder identified by the given path
* @param p_path
*/
static std::string GetExtension(const std::string& p_path);
/**
* Convert the EFileType value to a string
* @param p_fileType
*/
static std::string FileTypeToString(EFileType p_fileType);
/**
* Returns the file type of the file identified by the given path
* @param p_path
*/
static EFileType GetFileType(const std::string& p_path);
};
建構式默認洗掉,之前的文章已講述過;其他函式的作用已有英文注釋,不多贅述;代碼定義了類變數 EFileType,內含以檔案功能分類的檔案型別名(如模型 MODEL、紋理 TEXTURE 等),并用 enum 關鍵詞限定,表示僅能從類內給出的值中選擇回傳,
由此可以看出,該類可以處理判斷路徑指向檔案的功能型別或以需求的方式(例如更改為 Windows 格式等)輸出等,具體實作方式讓我們看看 PathParser.cpp 檔案:
1.2 PathParser.cpp
1.2.1 頭檔案
#include <algorithm>
#include "OvTools/Utils/PathParser.h"
除了包含 PathParser.h 檔案外,還包含了 algorithm,該頭檔案屬于 C++ 標準庫,能提供多種演算法函式,例如常用的 sort(),stable_sort(),partical_sort(),nth_element() 等等,
1.2.2 主體代碼
主體代碼都是函式具體定義,讓我們一個個看:
MakeWindowsStyle() 函式
std::string OvTools::Utils::PathParser::MakeWindowsStyle(const std::string & p_path)
{
std::string result;
result.resize(p_path.size());
for (size_t i = 0; i < p_path.size(); ++i)
result[i] = p_path[i] == '/' ? '\\' : p_path[i];
return result;
}
該函式通過 resize 函式定義 result 字串長度為路徑長度,而后用一個三目運算子依次將路徑中的 “ / ” 改為 “ \ ” —— 符合 Windows 系統的路徑格式 —— 同時賦值 result 回傳,簡單一提,for 回圈中 i 的型別 size_t 重命名前為 unsigned __int64,
MakeNonWindowsStyle() 函式
std::string OvTools::Utils::PathParser::MakeNonWindowsStyle(const std::string & p_path)
{
std::string result;
result.resize(p_path.size());
for (size_t i = 0; i < p_path.size(); ++i)
result[i] = p_path[i] == '\\' ? '/' : p_path[i];
return result;
}
該函式和 MakeWindowsStyle 函式同理,但功能是反向操作,將 Windows 格式的 “ \ ” 轉為 “ / ”,
GetContainingFolder() 函式
std::string OvTools::Utils::PathParser::GetContainingFolder(const std::string & p_path)
{
std::string result;
bool extraction = false;
for (auto it = p_path.rbegin(); it != p_path.rend(); ++it)
{
if (extraction)
result += *it;
if (!extraction && it != p_path.rbegin() && (*it == '\\' || *it == '/'))
extraction = true;
}
std::reverse(result.begin(), result.end());
if (!result.empty() && result.back() != '\\')
result += '\\';
return result;
}
該函式可以回傳路徑檔案夾的父檔案路徑,讓我們直接看 for 回圈:rbegin() 函式是一個反向迭代器,即與 begin() 獲取第一個元素相反,它獲得的是倒數第一個元素;相對的,rend() 就是獲得第一個元素了,所以 auto(自判斷型別)it 的初值是路徑的末尾,
接著先看第二個 if,它判斷若 extration 為 false 且 it 掃描到 “ \ ” 或 “ / ” 時,代表已經掃描完了子檔案,接下來是父檔案的路徑,于是賦值 extration 為 true,之后的回圈即可在第一個 if 內不斷記錄下路徑,
但是,rbegin() 是反向迭代,回圈輸入后 result 中的路徑是相反的,所以代碼呼叫 reverse() 函式倒序 result 為路徑正序,并在最后補上 “ \ ”,最終成功輸出,
GetElementName() 函式
std::string OvTools::Utils::PathParser::GetElementName(const std::string & p_path)
{
std::string result;
std::string path = p_path;
if (!path.empty() && path.back() == '\\')
path.pop_back();
for (auto it = path.rbegin(); it != path.rend() && *it != '\\' && *it != '/'; ++it)
result += *it;
std::reverse(result.begin(), result.end());
return result;
}
該函式和 GetContainingFolder() 函式同理,不過是反向操作,它能獲得路徑的末尾子檔案:if 判斷末尾若是 “ \ ” 則洗掉,接著 for 回圈反向迭代記錄子檔案名稱,遇到 “ \ ” “ / ” 時代表子檔案輸入完畢,退出回圈;最后 reverse 使路徑正序輸出,
GetExtension() 函式
std::string OvTools::Utils::PathParser::GetExtension(const std::string & p_path)
{
std::string result;
for (auto it = p_path.rbegin(); it != p_path.rend() && *it != '.'; ++it)
result += *it;
std::reverse(result.begin(), result.end());
return result;
}
該函式能獲得檔案擴展名,原理與上兩個函式一樣,只是將終止條件更換為 “ . ”,不多贅述,
FileTypeToString() 函式
std::string OvTools::Utils::PathParser::FileTypeToString(EFileType p_fileType)
{
switch (p_fileType)
{
case OvTools::Utils::PathParser::EFileType::MODEL: return "Model";
case OvTools::Utils::PathParser::EFileType::TEXTURE: return "Texture";
case OvTools::Utils::PathParser::EFileType::SHADER: return "Shader";
case OvTools::Utils::PathParser::EFileType::MATERIAL: return "Material";
case OvTools::Utils::PathParser::EFileType::SOUND: return "Sound";
case OvTools::Utils::PathParser::EFileType::SCENE: return "Scene";
case OvTools::Utils::PathParser::EFileType::SCRIPT: return "Script";
case OvTools::Utils::PathParser::EFileType::FONT: return "Font";
}
return "Unknown";
}
該函式用 switch 判斷并轉換檔案型別為字串,非常簡單,也不多說,值得注意的是,這里的檔案型別都來自 PathParser 類自己定義的類 EFileType,是按功能分類的,
GetFileType() 函式
OvTools::Utils::PathParser::EFileType OvTools::Utils::PathParser::GetFileType(const std::string & p_path)
{
std::string ext = GetExtension(p_path);
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
if (ext == "fbx" || ext == "obj") return EFileType::MODEL;
else if (ext == "png" || ext == "jpeg" || ext == "jpg" || ext == "tga") return EFileType::TEXTURE;
else if (ext == "glsl") return EFileType::SHADER;
else if (ext == "ovmat") return EFileType::MATERIAL;
else if (ext == "wav" || ext == "mp3" || ext == "ogg") return EFileType::SOUND;
else if (ext == "ovscene") return EFileType::SCENE;
else if (ext == "lua") return EFileType::SCRIPT;
else if (ext == "ttf") return EFileType::FONT;
return EFileType::UNKNOWN;
}
該函式能判斷檔案格式型別,并轉換為 EFileType 中記錄的功能型別,若未知回傳 EFileType 中的 UNKNOWN,實作方式很簡單,呼叫上述過的函式 GetExtension 得到檔案擴展名再 if 判斷,
值得注意的是 transform 函式,這里是它的一元操作用法,四個引數的含義依次是:操作起始位、操作終止位、操作后記錄的起始位、某種操作,該函式的作用是從操作起始位元素開始,依次按需操作元素直至終止位并記錄;此處的操作是 ctype.h 中的 tolower(小寫操作),這樣就能符合后續 if 內的小寫字母判斷了,transform 是標準庫函式,所以更多的用法在此不作講解,
至此,PathParser 的所有函式已經探究完了,其實了解了使用的幾個庫內函式,就能輕松理解,讓我們繼續看下一個部分 Random,
2、Random
2.1 Random.h
2.1.1 頭檔案
#include <random>
檔案僅包含了亂數庫,不多說,
2.1.2 主體代碼
主體代碼定義了一個類 Random,作用是處理亂數的生成,代碼如下:
class Random
{
public:
/**
* Disabled constructor
*/
Random() = delete;
/**
* Generate a random between two given integers (Closed interval)
* @param p_min
* @param p_max
*/
static int Generate(int p_min, int p_max);
/**
* Generate a random between two given floats (Closed interval)
* @param p_min
* @param p_max
*/
static float Generate(float p_min, float p_max);
/**
* Verify if the percentage is satisfied
* @param p_percentage (must be between 0 and 1)
*/
static bool CheckPercentage(float p_percentage);
private:
static std::default_random_engine __GENERATOR;
};
該類的功能很少,函式也有注釋,不多贅述,關于唯一的變數,其型別為 default_random_engine,但這是 random 頭檔案里用 using 關鍵詞重命名的 mt19937 型別,而 mt19937 也是 using 重命名了的一個 random 里的類模板,而這個類模板就是 mersenne_twister_engine 梅森選擇演算法,其特點是以一個梅森素數作為周期長度,
梅森素數由梅森數而來,先講梅森數,是指形如 2p - 1 的一類數,其中 p 是素數;而是素數的梅森數就稱為梅森素數,例如 7、127 等等 —— 當然,用在這個演算法中的梅森素數更大,雖然這個演算法很強大,能生成高質量的序列,但存在速度相對較慢的缺點,該演算法很復雜,在此就講解這么多,
另外,該類模板除了有 mt19937 生成隨機的無符號 32 位整數,還有 mt19937_64 生成無符號的 64 位整數,其中,mt19937 亂數生成器的周期長度為 219937-1,因而得名,
了解完變數,現在讓我們到 Random.cpp 看看函式們的具體實作方法:
2.2 Random.cpp
該檔案的頭檔案只有上述的 Random.h 檔案,不多贅述,直接看函式:
Generate() 函式
int OvTools::Utils::Random::Generate(int p_min, int p_max)
{
std::uniform_int_distribution<int> distribution(p_min, p_max);
return distribution(__GENERATOR);
}
float OvTools::Utils::Random::Generate(float p_min, float p_max)
{
std::uniform_real_distribution<float> distribution(p_min, p_max);
return distribution(__GENERATOR);
}
該函式有兩個函式多載,僅變數型別不同,函式功能的實作也很簡單,它宣告了一個 random 頭檔案中的類物件,能直接生成一個指定范圍內的亂數,
值得一提的是,生成亂數的類 uniform_int_distribution 與 uniform_real_distribution 都繼承了同是 random 里的 uniform_real 類模板;該類模板的內含一個功能是將一個亂數生成器物件作為引數值傳給均勻分布函式物件,從而獲得一個隨機值,該類還包含了其它功能函式,在此不多贅述,
所以,在回傳物件 distribute 時,代碼這里還多傳入了一個 __GENERATOR 到一個括號 “ ( ) ” 中,其實,該括號不是什么函式的括號,而是括號符號的多載:
template <class _Engine>
_NODISCARD result_type operator()(_Engine& _Eng) const {
return _Eval(_Eng, _Par);
}
顯然,需要更深入去查看 _Eval(),這是 uniform_real 類中的私有函式:
template <class _Engine>
result_type _Eval(_Engine& _Eng, const param_type& _Par0) const {
return _NRAND(_Eng, _Ty) * (_Par0._Max - _Par0._Min) + _Par0._Min;
}
其中,_NRAND 是用到了 generate_canonical 均勻分布亂數函式的一個函式,而 generate_canonical 函式模板會提供一個浮點值范圍在 [0,1) 內且有給定的隨機位元數的標準均勻分布,至此,我們就了解了整個亂數生成并回傳的程序,
CheckPercentage() 函式
bool OvTools::Utils::Random::CheckPercentage(float p_percentage)
{
std::uniform_real_distribution<float> distribution(0.0f, 100.0f);
return distribution(__GENERATOR) <= p_percentage;
}
該函式功能是判斷生成數值是否超出給出的百分比值,呼叫的類模板與函式和 Generate() 一樣,所以原理也是一樣的,在此就不多贅述,
總結
至此,Utils 小模塊的前兩個部分我們已經探究完畢,總結來說,這些類以及內含的函式們的邏輯并不復雜,許多都是直接依靠 C++ 標準庫來實作,由此可見 C++ 標準庫的完備與強大;此外,雖然大多是標準庫函式,但我們依然可以學習撰寫者的邏輯思維以及代碼的規范性,這都可以成為在工程專案實際應用中的經驗,
Utils 分析的中篇,我們將探究接下來兩個部分:ReferenceOrValue 與 SizeConverter,代碼雖然短,但是也有值得學習的地方,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/336321.html
標籤:其他
下一篇:二十一天——打卡第三天
