<2021SC@SDUSC> 开源游戏引擎 Overload 代码模块分析 之 OvTools(六)—— Utils(下)

2021SC@SDUSC
开源游戏引擎 Overload 代码模块分析 之 OvTools(六)—— Utils(下)

前言

本篇是开源游戏引擎 Overload 模块 OvTools 的 Utils 小模块的下篇,也是 Utils 的最后一篇,但不是 OvTools 的终篇。Utils(上)可前往上上篇文章查看;Utils(中)可前往上篇文章查看。而本篇,笔者将探究 Utils 的最后两部分:String 与 SystemCalls

另外,想先大致了解 Overload 可前往这篇文章,想看其他相关文章请前往笔者的 Overload 专栏自主选择。

分析

1、String

1.1 String.h

1.1.1 头文件
#include <string>
#include <functional>

string 不必多说;functional 已讲述过,请自行回顾

1.1.2 主体代码

这是一个 string 类,但不是 C++ 库的 string 型,所以很重要的是 namespace:

namespace OvTools::Utils

而这个 string 类的官方注释的描述却是处理随机数生成,其定义的代码如下:

class String
	{
	public:
		/**
		* Disabled constructor
		*/
		String() = delete;

		/**
		* Replace the first occurence of p_from by p_to in the given string
		* @param p_target (The string to modify)
		* @param p_from
		* @param p_to
		*/
		static bool Replace(std::string& p_target, const std::string& p_from, const std::string& p_to);

		/**
		* Replace every occurence of p_from by p_to in the given string
		* @param p_target (The string to modify)
		* @param p_from
		* @param p_to
		*/
		static void ReplaceAll(std::string& p_target, const std::string& p_from, const std::string& p_to);

        /**
        * Generate a unique string satisfying the availability predicate
        * @param p_source
        * @param p_isAvailable (A callback that must returning true if the input string is available)
        */
        static std::string GenerateUnique(const std::string& p_source, std::function<bool(std::string)> p_isAvailable);
	};

构造函数的 delete 不多说了,可自行回顾;而其他函数,从这些注释中却看到都是在处理字符串,与官方描述仿佛不符合。所以我们需要到 String.cpp 中去一探究竟。

1.2 String.cpp

头文件只有上文的 String.h ,所以让我们直接看三个函数吧:

Replace() 函数
bool OvTools::Utils::String::Replace(std::string & p_target, const std::string & p_from, const std::string & p_to)
{
	size_t start_pos = p_target.find(p_from);

	if (start_pos != std::string::npos)
	{
		p_target.replace(start_pos, p_from.length(), p_to);
		return true;
	}

	return false;
}

该函数作用是只替换检索到的第一段要替换的字符串,它先是调用了 C++ string 的 find() 函数,得到要改写的 p_from 字符串在 p_target 中的位置,赋值给 size_t 型的变量 start_pos;其中,size_t 是 unsigned __int64 型的重命名,也是 find() 函数的唯一的返回类型。但是,如果没有找到,就会返回下述的 npos 标记:

在 if 的判断中,函数用到了 string::npos 静态成员常量。该常量有三种常见的用法:

1、表示 size_t 型的元素具有的最大可能值;

2、当这个值是关于字符串时,该值表示的是字符串的结尾;

3、作为表明没有匹配的返回值。

显然,此处若找到了,其含义就是字符串的结尾,最后直接用 C++ string 的 replace() 函数按需替换字符串并返回 true;当然,如果是没找到,start_pos 是 npos,就返回 false了。

ReplaceAll() 函数
void OvTools::Utils::String::ReplaceAll(std::string& p_target, const std::string& p_from, const std::string& p_to)
{
	if (p_from.empty()) return;

	size_t start_pos = 0;
	while ((start_pos = p_target.find(p_from, start_pos)) != std::string::npos)
	{
		p_target.replace(start_pos, p_from.length(), p_to);
		start_pos += p_to.length();
	}
}

该函数和 Replace() 函数原理差不多,不过是替换所有要替换的字符串,所以 while 循环条件中调用的 find() 函数有两个参数,表示第二个参数作为起始位开始寻找字符串,此处初值是 0 ,start_pos 会检索到第一个要替换的字符串。

接着 while 中的第一轮操作,在 replace() 替换掉字符串后,start_pos 会增加,使位置刚好到替换掉的字符串的尾;然后新一轮循环,start_pos 就在 find() 中作为新的起始位。所以该 while 循环的功能就是不断检索到要替换的字符串,替换后将检索起始位置标到字符串尾巴,再检索,再替换 …… 直至目标字符串被全部检索完毕。

GenerateUnique() 函数

该函数功能是生成唯一可用断言,代码非常长,我们一段一段看吧:

std::string OvTools::Utils::String::GenerateUnique(const std::string& p_source, std::function<bool(std::string)> p_isAvailable)
{

先看传入的参数,string 型 p_source,和来自 functional 头文件的一个类模板 function,一个可调用对象的包装器。它可以在统一包装器中包含各种可调用对象,例如函数、表达式或其他函数对象,指向成员函数、数据成员的指针等等;另外,如果没有可调用目标,那么调用它时会抛出一个异常 std::bad_function_call。在本函数中,该类的对象将用于判断字符串是否可用

接着,该函数先声明了下列几个变量:

    auto suffixlessSource = p_source;

    auto suffixOpeningParenthesisPos = std::string::npos;
    auto suffixClosingParenthesisPos = std::string::npos;

    // Keep track of the current character position when iterating onto `p_source`
    auto currentPos = decltype(std::string::npos){p_source.length() - 1};

前三个变量不用多说,npos 在上文已有提及。第四个变量 currentPos,根据注释,它的目的是在 p_source 迭代时,跟踪当前字符的位置。该变量使用了关键词 decltype,类型说明符,它的作用是选择并返回操作数的数据类型,此处显然是返回了 size_t 型,并且变量 currentPos 指向 p_source 的末尾。

现在来到函数的第一个循环,目的是寻找 “( ” 、“ )” 符号位置:

    // Here we search for `(` and `)` positions. (Needed to extract the number between those parenthesis)
    for (auto it = p_source.rbegin(); it < p_source.rend(); ++it, --currentPos)
    {
        const auto c = *it;

        if (suffixClosingParenthesisPos == std::string::npos && c == ')') suffixClosingParenthesisPos = currentPos;
        if (suffixClosingParenthesisPos != std::string::npos && c == '(') suffixOpeningParenthesisPos = currentPos;
    }

循环条件中用到了反向迭代器 rbegin()之前的文章已有简要说明;循环内部,npos 表示字符串结尾位置,其余很简单,不多赘述。

之后函数声明了一个变量 counter,将用于存取括号内的数字;此处默认为 “ 1 ”,表示如果未找到括号:

    // We need to declare our `counter` here to store the number between found parenthesis OR 1 (In the case no parenthesis, AKA, suffix, has been found)
    auto counter = uint32_t{ 1 };

此处,uint32_t 型是重命名了 unsigned int 型。

接着,是一个有点复杂的 if 语句:

    // If the two parenthis have been found AND the closing parenthesis is the last character AND there is a space before the opening parenthesis
    if (suffixOpeningParenthesisPos != std::string::npos && suffixClosingParenthesisPos == p_source.length() - 1 && suffixOpeningParenthesisPos > 0 && p_source[suffixOpeningParenthesisPos - 1] == ' ')
    {
        // Extract the string between those parenthesis
        const auto between = p_source.substr(suffixOpeningParenthesisPos + 1, suffixClosingParenthesisPos - suffixOpeningParenthesisPos - 1);

        // If the `between` string is composed of digits (AKA, `between` is a number)
        if (!between.empty() && std::find_if(between.begin(), between.end(), [](unsigned char c) { return !std::isdigit(c); }) == between.end())
        {
            counter = static_cast<uint32_t>(std::atoi(between.c_str()));
            suffixlessSource = p_source.substr(0, suffixOpeningParenthesisPos - 1);
        }
    }
    
	auto result = suffixlessSource;

首先,if 判断如果左括号非结尾位(即找到了双括号),右括号位于最后,且左括号非起始位,其前一位有空格,则通过判断;其次,调用库函数 substr 复制括号内的字符串(不包括两括号)存储在变量 between 中。

然后,又一个内部 if 判断,其中用到了库函数 find_if:同 find() 一样,能在给出的序列范围内依次查询,当第一次遇到能使给出的表达式为 true 的元素时,返回该元素;如果没有找到,会返回给出序列的结束迭代器。另外还用到了 isdigit(),该函数主要用于检查传入参数是否为十进制数字字符。所以,该 if 判断如果 between 非空且 between 内是非数字的字符是 beteen 的结束迭代器(即相当于全是数字字符),则通过。

判断的内部,使用了 c_str() 获得 between 字符串指针常量,atoi() 转换其为整型数,static_cast<> 再强制转换为 uint32_t 即 unsigned int 型,并赋值给 counter;而变量 suffixlessSource 获得左括号之前的子字符串,并赋值给变量 result。

在这之后,该函数只剩一个 while 循环来检测 result :

    // While `result` isn't available, we keep generating new strings
    while (!p_isAvailable(result))
    {
        // New strings are composed of the `suffixlessSource` (Ex: "Foo (1)" without suffix is "Foo")
        result = suffixlessSource + " (" + std::to_string(counter++) + ")";
    }

    return result;
}

while 使用了 function 类对象 p_isAvailable 的括号 “()” 重载函数,判断参数是否可用。若不可用则进入循环,并将 suffixlessSource 添加上括号包裹的字符串型的新数字后缀,重新生成新的字符串后赋值 result。最后返回 result,完成任务。

现在,看完了所有的 String 类的函数,笔者觉得官方对该类的作用注释应该是出错了,用成了 Random 类的注释;此外,从其他探究 Overload 引擎的队员反馈中也得到了证实,其它模块也存在一些注释错误

接下来,就是最后一个部分 SystemCalls:

2、SystemCalls

2.1 SystemCalls.h

该文件仅有 string 头文件,不多说,直接看主体代码,SystemCalls 类:

SystemCalls 类
class SystemCalls
	{
	public:
		/**
		* Disabled constructor
		*/
		SystemCalls() = delete;

		/**
		* Open the windows explorer at the given path
		* @param p_path
		*/
		static void ShowInExplorer(const std::string& p_path);

		/**
		* Open the given file with the default application
		* @param p_file
		* @param p_workingDir
		*/
		static void OpenFile(const std::string& p_file, const std::string & p_workingDir = "");

		/**
		* Open the given file for edition with the default application
		* @param p_file
		*/
		static void EditFile(const std::string& p_file);

		/**
		* Open the given url with the default browser
		* @param p_url
		*/
		static void OpenURL(const std::string& p_url);
	};

该类就像之前文章所说的,集中于函数功能,所以构造函数默认删除;其他函数的功能都有注释,不多赘述,让我们到 SystemCalls.cpp 看它们的具体实现方法:

2.2 SystemCalls.cpp

2.2.1 头文件
#include "OvTools/Utils/PathParser.h"
#include "OvTools/Utils/SystemCalls.h"

#include <Windows.h>

该文件不仅包含了上述的 SystemCalls.h,还包含了同是 Utils 小模块的另一个部分 PathParser 的头文件 PathParser.h。略有遗忘或还没有阅读过 PathParser 的读者可以前往Utils(上)查看。其次,文件还包括了 Windows.h,Windows 程序的必备头文件之一,涉及了 Windows 内核 API,图形界面接口,图形设备函数等重要功能。

2.2.2 主体代码

该文件主要对上面声明的四个函数做了具体定义。

ShowInExplorer() 函数
void OvTools::Utils::SystemCalls::ShowInExplorer(const std::string & p_path)
{
	ShellExecuteA(NULL, "open", OvTools::Utils::PathParser::MakeWindowsStyle(p_path).c_str(), NULL, NULL, SW_SHOWNORMAL);
}

首先调用了 PathParser 类的函数 MakeWindowsStyle,使得 p_path 路径格式符合 Windows 路径格式;而后 c_str 获得字符串指针常量;最后调用 Windows.h 的函数 ShellExecuteA,该函数能对指定的文件执行操作,此处是将路径指向的文件以原本的大小和位置打开窗体。

其中,ShellExecuteA 函数有多达六个参数,其意义从左到右分别是:

1、hwnd:父窗口的句柄,用于显示 UI 或错误消息;如果操作与窗口没有关联,则此值可以为 NULL。

2、lpOperation:指向空终止字符串的指针,指定要执行的动作。此处代码的 open 即为打开文件。

3、lpFile:指向空终止字符串的指针,指定要执行操作的文件或对象。

4、lpParameters:如果 lpFile 是一个可执行文件,则此参数是指向空终止字符串的指针,其字符串指定要传递给应用程序的参数;如果 lpFile 是一个文档文件,则此参数为 NULL。

5、lpDirectory:指向空终止字符串的指针,指定操作的默认工作目录;如果该值为 NULL,则使用当前工作目录。

6、nShowCmd:指定应用程序打开时如何显示。此处代码的 SW_SHOWNORMAL 含义是:以原本的大小和位置,激活并显示指定的窗体。

关于 ShellExecuteA 函数更多的信息及其各参数的具体用法,此处就不再赘述了,想了解的读者请前往官方文档

OpenFile() 函数
void OvTools::Utils::SystemCalls::OpenFile(const std::string & p_file, const std::string & p_workingDir)
{
	ShellExecuteA(NULL, "open", OvTools::Utils::PathParser::MakeWindowsStyle(p_file).c_str(), NULL,
		p_workingDir.empty() ? NULL : OvTools::Utils::PathParser::MakeWindowsStyle(p_workingDir).c_str(),
		SW_SHOWNORMAL);
}

该函数与上面的 ShowInExplorer() 函数代码大同小异,只是参数 IpDirectory 的值使用了一个三目运算符:如果 p_workingDir 是空的(即未传入工作目录,默认赋值空),则为 IpDirectory 为 NULL,即当前工作目录;否则为 p_workingDir 给出的目录。

EditFile() 函数
void OvTools::Utils::SystemCalls::EditFile(const std::string & p_file)
{
	ShellExecuteW(NULL, NULL, std::wstring(p_file.begin(), p_file.end()).c_str(), NULL, NULL, SW_NORMAL);
}

该函数与上述的两个函数道理也是一样的。此处使用的 ShellExecuteW() 的参数含义和上面的 ShellExecuteA 是相同的,其第二个参数 lpOperation 值虽然为 NULL,但是默认会自动替换为 open。具体用法请查看官方文档

OpenURL() 函数
void OvTools::Utils::SystemCalls::OpenURL(const std::string& p_url)
{
	ShellExecute(0, 0, p_url.c_str(), 0, 0, SW_SHOW);
}

该函数原理也一样,用到的 ShellExecute 的功能也和上述两个函数差不多。其中,参数里的 0 类似其他两个函数的 NULL;SW_SHOW 的含义是激活窗体,并将其显示在当前的大小和位置上。

上述的四个函数都用到了 ShellExecute 系列的函数,这些函数功能上相差无几,且与 Overload 系列文章联系也不是很大,所以此处就不多探究具体的区别,烦请想了解的读者自行前往官方文档寻找比较。

总结

String 与 SystemCalls 的各个函数其实都不复杂,都是使用已有库函数简单完成一些字符串或是系统回调的处理工作,理解起来很快。

至此,Overload 游戏引擎 OvTools 模块的所有主体内容已经较为详细地分析探究完了,想了解更深的读者请到笔者的这篇文章查看 Overload 源码的下载地址以及参考可能遇到的安装问题。另外,考虑到本篇的篇幅问题,笔者计划下一篇将是 OvTools 的终篇 —— OvTools 模块总结

clapping

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值