<2021SC@SDUSC> 开源游戏引擎 Overload 代码模块分析 之 OvTools—— Utils(上)
Posted Egovix
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了<2021SC@SDUSC> 开源游戏引擎 Overload 代码模块分析 之 OvTools—— Utils(上)相关的知识,希望对你有一定的参考价值。
<2021SC@SDUSC>
开源游戏引擎 Overload 代码模块分析 之 OvTools(四)—— Utils(上)
前言
本篇是开源游戏引擎 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。代码虽然短,但是也有值得学习的地方。
以上是关于<2021SC@SDUSC> 开源游戏引擎 Overload 代码模块分析 之 OvTools—— Utils(上)的主要内容,如果未能解决你的问题,请参考以下文章
<2021SC@SDUSC>开源游戏引擎Overload代码分析三(OvWindowing结束):OvWindowing——Dialogs
<2021SC@SDUSC>开源游戏引擎Overload代码分析一:OvWindowing——Window.cpp
<2021SC@SDUSC> 开源游戏引擎 Overload 代码模块分析 之 OvGame—— Utils(终)大纲及 FPSCounter & Debug(上)大纲及 DriverInfo