Bingo, Computer Graphics & Game Developer

Unreal为每个平台的底层都封装了一致的高层代码,本文讲述的是Windows下Unreal的源代码细节表现

###1. 使用Mutex来保证当前程序是当前系统中唯一开启的

Unreal源代码

bool MakeNamedMutex( const TCHAR* CmdLine )
{
    bool bIsFirstInstance = false;

    TCHAR MutexName[MAX_SPRINTF] = TEXT( "" );

    FCString::Strcpy( MutexName, MAX_SPRINTF, TEXT( "UnrealEngine4" ) );

    GNamedMutex = CreateMutex( NULL, true, MutexName );

    if( GNamedMutex    && GetLastError() != ERROR_ALREADY_EXISTS && !FParse::Param( CmdLine, TEXT( "NEVERFIRST" ) ) )
    {
        // We're the first instance!
        bIsFirstInstance = true;
    }
    else
    {
        // Still need to release it in this case, because it gave us a valid copy
        ReleaseNamedMutex();
        // There is already another instance of the game running.
        bIsFirstInstance = false;
    }

    return( bIsFirstInstance );
}

其中使得程序不会启动两次的关键在于互斥锁CreateMutex()。我参考了这篇文章CreateMutex,其中这一段讲解清晰了原理

进程在启动时首先检查当前系统是否已经存在有此进程的实例,如果没有,进程将成功创建并设置标识实例已经存在的标记。此后再创建进程时将会通过该标记而知晓其实例已经存在,从而保证进程在系统中只能存在一个实例。

如有同名程序运行,则通过 GetLastError()得到错误代码 ERROR_ALREADY_EXIST。


###2. do{ some_code }while(false); 代码块实现模板switch

参考以下文章 Link_1, Link_2

Unreal源代码

#define checkCode( Code )        do { Code } while ( false );

大致含义就是代码块中一旦出现错误或者指定情况下可以直接跳过(break)后部代码,实现类似goto的逻辑代码。说的直白一些,我个人更倾向于认为这像是”模板Switch”的实现。

// 若任何类型的switch存在 就会像下面这样
switch(condition)
{
case condition1:
    break;
case condition2:
    break;
    // ...
}

因为switch只能满足标量类型,而搭配此,就无需在每步中额外提供一些标示符来表示执行到的位置。其优点就相当于switch(int)和一堆的if/else组合对比下的优势。就像下面这样,阅读感受很糟糕。

// if/else
// condition1
if ( data ==  & array[o1])
    operation = O1;
// condition2
else if ( data ==  & array[o2])
    operation = O2;
// conditionxxx
else if
    //...
// conditionn
else if ( data ==  & array[on])
    operation = ON;

Log( "operation:",operation);

相对比之下,checkCode()的优势很明显

// macro switch
checkCode(
    if(!condition1)
        break;
    if(!condition2)
        break;
    // ...
)

除了我个人理解的switch以外,它也有一定的错误代码跳转能力。以下某种情况下,some_codesome_further_code是有可能不被执行的,实现了类似goto的逻辑。

// template switch
do
{
    if (!condition1) break;
    some_code;
    if (!condition2) break;
    some_further_code;
    // …
} while(false);

###3. class后部用于方便导出到dll的”关键字”

Unreal源代码

class RHI_API FDynamicRHI
{
public:
    // ...
}

找了很久,在WindowsPlatfrom.h下找到类似定义

#define DLLEXPORT __declspec(dllexport)
#define DLLIMPORT __declspec(dllimport)

因为我只使用过一次dll导出函数使用,同时也没有过导出类的经验,因此遇到这个宏时一无所知。参考与以下文章CSDN, C++编写DLL的方法, 其中第二篇更为精确一点。

这个宏出现最核心的原因就在于,客户端和DLL本身共用同一份头文件

如若是客户端,那么需要在class后声明_declspec(dllimport)用以导入该类给自己,DLL则是class后声明_declspec(dllexport),问题就出在这里,要共用一份头文件,但class却有两个写法,岂不是无法起到共用的效果?

解决方案如下
.h

#ifdef  DLL_API
#else
#define DLL_API _declspec(dllimport)
#endif

.cpp

#define DLL_API     _declspec(dllexport)

#include "DLLTest.h"

此时客户端在导入这个类时,使用_declspec(dllimport),DLL使用时已定义DLL_API,因此依旧是class被认定为导出_declspec(dllexport)

下次打算自己亲手试一试Windows平台下的dll导出。这样印象更深一点。



基本上初步入门都是在这里正则表达式入门教程学到的,当然没有30分钟那么夸张。

许多时候如若不进行实际测试很难知道自己的正则表达式正确与否,一般我就在这里正则表达式在线测试或者Rebular直接在线测试,其实道理都一样。

从开始学习,到实际能够解读正则表达式,到真正运用正则表达式在CSS文件的解析上,我起码花了整整三天时间,甚至更多

首先说说我自己的感想,正则表达式难度不在于其各个元字符的搭配很难,而是解析的这一步很难。所以我自己结合了教程总结了如下经验。

案例1

^(\*?[-#/\*\w]+(\[[0-9a-z_-]+\])?)\s*
乍一看非常难以分解,不过我一般会这样做

^                           #字符串开始
(
      \*?                     #可选的*
      [-#/\*\w]+              #"-""#""\""*""字母数字下划线或汉字"出现一次或多次
      (
          \[[0-9a-z_-]+\]         #"["+0~9|a~z|_~空格+"]"
      )?                      #上述字符串是可选的
)
\s*                         #任意多空格

上述代码不会对正则表达式产生任何的影响,因为(?= #xxx)内部的内容表明为注释,这样的缩进可以更明显的看出来内容,我个人更偏向于下面这样的用法。

// ^                    字符串开始
// (
//     \*?                  可选的任意出现零次或者多次的字符
//     [-#/\*\w]+           "-""#""\""*""字母数字下划线或汉字"出现一次或多次
//     (\[[0-9a-z_-]+\])?   可选的"["+0~9|a~z|_~空格+"]"
// )
// \s*                  任意多空格
static const std::regex _RE0_ = RE(R"raw(^(\*?[-#/\*\w]+(\[[0-9a-z_-]+\])?)\s*)raw");

这样就有一个好处,在实际使用此正则表达式时,不用在表达式内部修改样式,有的OS例如Windows,会要求在每行的最后加上\类似于这样

string test2 = 
"body{\
    background-color: #d0e4fe;\
}\
\
h1{\
    color: orange;\
    text-align: center;\
}\
\
p{\
    font-family: \"Times New Roman\";\
    font-size: 20px;\
}";

这对有强迫症的人来说简直是致命的,但又没有什么好的办法。所以我个人偏向于将正则表达式的注释写到对应语言的注释中。

对于元字符其实没有什么太多能够讲述的

反义

代码 说明
\W 匹配任意不是字母,数字,下划线,汉字的字符
\S 匹配任意不是空白符的字符
\D 匹配任意非数字的字符
\B 匹配不是单词开头或结束的位置
[^x] 匹配除了x以外的任意字符
[^aeiou] 匹配除了aeiou这几个字母以外的任意字符

重复

代码 说明
* 重复零次或更多次
+ 重复一次或更多次
? 重复零次或一次
{n} 重复n次
{n,} 重复n次或更多次
{n,m} 重复n到m次

元字符

代码 说明
. 匹配除换行符以外的任意字符
\w 匹配字母或数字或下划线或汉字
\s 匹配任意的空白符
\d 匹配数字
\b 匹配单词的开始或结束
^ 匹配字符串的开始
$ 匹配字符串的结束

要说说的就是这个反义,^这个字符如若不在[]中,那么它的意思一般就是表示从字符串开始的位置,当然如果前面没有转义字符的话。一旦跑到了[]中,那么它就代表了反义。

案例2

^([^\{]+)

// ^        字符串开始处
// (
//       [^\{]+   除{以外其他的一个或者多个字符
// )

这里的两个^就展现了不同的含义,一开始我在这里费了不少功夫。

删除注释

有的正则表达式可以匹配得到多门语言当中各型各色的注释,以便于直接找到删除。删除注释大多数的语言注释的正则表达式都可以在这里找到。C/C++/Java因为语言相通所以注释匹配也是可以通用的。

在实际应用中,我使用/\*[^\*]*\*+([^/\*][^\*]*\*+)*/来匹配CSS的注释,读者可以自行测试。

在实际的CSS解析中最难以理解的表达式如下
^((?:'(?:\\'|.)*?'|\"(?:\\\"|.)*?\"|\([^\)]*?\)|[^\};])+)

// ^
// (
//    (
//        ?:            可选的:
//        '(?:\\'|.)*?'             单引号围起来的内容或者任意字符 若前方有',那么需要以'结尾
//        |
//        \"(?:\\\"|.)*?\"          双引号围起来的内容或者任意字符 若前方有",那么需要以"结尾
//        |
//        \([^\)]*?\)               (开始 匹配除了)以外的任意字符 字符可以出现零次或者多次[^x] 可选的)
//        |
//        [^\};]                    匹配除了} ;以外的任意字符
//    )
//    +                 可以有零个或者多个上述值出现
// )

这里的单引号的确是难以理解,后来才发现其实单引号不需要转义就可以直接表达自身的意思,可以对比下一行的双引号。因为有的CSS property: value;里的value是字符串/单一字母/xxx(xxx)/等等形式 如

background-color: rgb(0, 10, 20); 

background-color: #123456;

background-color: red;

很种类繁多,因此需要一并写出。



我将我尝试的心路历程都记录在了这里。

这里的快速实质上是代表了代码书写的高效率。在重造数据结构里线性表的轮子的时候,遇到了需要构造NxM的矩阵需求,因为NxM可以动态构建出来,这没什么。

如下,可以非常方便的造一个简单的NxM的矩阵轮子出来

class t3Matrix
{
public:
    // 构造函数参数里给出行列即可
    t3Matrix(int row, int col):col(col), row(row)
    {
        m = new int*[row]();
        for(int i = 0; i < row; i++)
            m[i] = new int[col]();
    }

    ~t3Matrix()
    {
        for(int i = 0; i < row; i++)
            delete[] m[i];

        delete[] m;
    }

    int **m;
    int col, row;
};

问题来了,如何赋值,倘若是以往的2x2, 3x3事实上都是好解决的。

void t3Matrix2x2::create(int a00, int a01, int a10, int a11)
{
    // do something...
}

void t3Matrix3x3::create(int a00, int a01, int a02, int a10, int a11, int a12)
{
    // do something...
}

// etc... 

实际使用的时候这样的方法也不免为一个好决策。这里我颇受Eigen的影响, 他的矩阵初始化办法颇为简便易读,他是这样的。

void main()
{
    // NxM
    t3Matrix m(2, 3);

    // core!
    m << 1, 2,
         3, 4,
         5, 6;
}

这才像话,立马动手抄,但是Eigen过于庞大,也只能依样画葫芦尝试着去实现。

尝试1

移位运算符与stream联系非常迫切,兴许有效。这里我尝试着这样去试着使用istream,ostream配合着完成这样的初始化。

class t3Matrix
{
public:
    t3Matrix();
    ~t3Matrix();
};

ostream& operator<<(ostream& os, t3Matrix& m);

istream& operator>>(istream& is, t3Matrix& m);

思来想去,我最多能够做到使用cin,cout为矩阵初始化和输出,并不能在代码中实现初始化动作。

void fuck()
{
    t3Matrix m(n, m);
    // 标准输入初始化矩阵
    cin >> m;
    // 抛却m.print()方式标准输出
    cout << m;
}

然而这并没有什么卵用,期间我甚至尝试着让t3Matrix继承std::ostream。注意!OSX下XCode不会对以下代码报错

class A:public std::ostream
{
public:
    // 如若不为ostream构造,那么你将面临两次调用matrix析构函数的问题(Windows上VS编译无法通过)
    // 解决办法是写为ostream(NULL)提供一个streambuf参数即可,当然也可以给NULL 
    A(){}

    // 会被二次调用
    ~A(){}
};
class A:public std::ostream
{
public:
    // 运行正常
    A():std::ostream(NULL){}

    ~A(){}
};


尝试2

这回另辟蹊径,想起了重载逗号运算符。重载这个运算符实在是太少见了,以至于我和同学说起时,惊讶的反应:这**玩意也要重载?

其实需求很明显只要m << 1, 2, 3, 4;能够一路正确的读取即可,<<的优先级高于,,加上从左到右的运算顺序。

所以只要满足m << 1的返回值能够与重载,的运算符左参数相符,那么其实就是t3Matrix自身了。

以下写法已证实可行

class t3Matrix
{
public:
    // ...
    t3Matrix& operator,(int value)
    {
        // 赋该value到正确的位置
        // do something...
        return *this;
    }

    t3Matrix& operator<<(int value)
    {
        // 赋该value到正确的位置
        // do something...
        return *this;
    }
};

void main()
{
    t3Matrix m(2, 3);
    m << 1, 2,
         3, 4,
         5, 6;
}

其实难度不在于”<<”的重载,而是想到”,”的重载才是

已更新至t3DataStructures



P74页中倒数第二行的else if

if(next_row == EXIT_ROW && next_col == EXIT_COL)
    found = TRUE;
else if(!maze[next_row][next_col] && !mark[nextRow][next_col]){
    // do something...
}

此处的else if我认为可以更正为if,那么最后一个结点也可以入栈,或者为了性能起见,将stack.push()的在第一步if中添加上也可。个人更倾向于第二种做法。

即(t3DataStructures.t3Maze中就是这样做的)

if(next_row == EXIT_ROW && next_col == EXIT_COL){
    stack.push(row, col, dir);
    found = TRUE;
}
else if(!maze[next_row][next_col] && !mark[next_row][next_col]){
    // do something...
}



大体就是这样了,本工序基本上是按照《数据结构-C语言》来重现出一个大致的数据结构基本内容的轮廓了的。

本身代码的难度不是很大,除了几个较难理解的算法重新实现以外,抑郁的基本上是对自己基本功的一次完整的复习,希望可以抛砖引玉。

一旦重现70%书中内容之后,打算接《深入剖析STL》来对照着看具体的差距在哪里。因为已经有不少跨平台工程的经验,所以本数据结构应该可以做到多平台的编译运行(Windows下也徐汇遇到文件格式问题,只需要重新保存成Unicode即可,OSX和Win的平台差异没办法似乎难以解决)

工程Github链接t3DataStructures,欢迎Star。



本文的理解建立在此 v_JULY_v 以及 阮一峰 的详解之上

本文是对 数据结构–C语言版 书中代码的详解与个人理解

上一节我只讲述了一下代码层面的理解,如果有算法背景不了解的可以去开篇的两篇教程中寻找答案。这里只阐述自己对于算法中较为难以理解部分的想法

为了能清晰的讲述KMP总共大概花费了一礼拜的时间; 这里我将书上的failure数组称为next数组以节省篇幅; s代表代求解字符串,p代表模式字符串

这一段代码我认为是我个人最难以理解的。

    i = failure[i];

首先这里的背景是,在求next数组某一循环时发现p[j]!=p[i+1],且i>=0。换言之p0,p1, … pi != pj-1, pj-i+1, …pj,因此我希望能够在p0,p1, … pi中找到一个位置x(x < p),使得p0,p1,…px = pj-x, pj-x+1,…pj

简而言之,就是希望能够找到一条稍微短一些的公共缀。可以参考v_JULY_v教程里的图

而算法的答案是:你可以试试x = next[i],说不定可行。注意,这里x = next[i]不是一次就可以找到,因此也就是while存在的价值。

难点依然存在,为什么x = next[i]就可以快速的找到这个x呢?这也是我这么多天一直困扰的地方。

    while(pat[j] != pat[i+1] && i>0)
        i = failure[i];

总结了一下,其实难点在于几个指针的意义不明确。这里j很好理解,指向当前需要求解的位置,i才是重点。

注意,这里的指针概念全部都引用自KMP心得-1里的代码实现

  1. next[i]代表了前缀末尾的下标
  2. next[i]+1代表了前后缀的长度
  3. next[i]代表了字符s[i]可回溯到的下标


这里我用一句话概括求解next数组的中心思想:让模式串p与自身匹配

知晓了此中心思想,理解算法就游刃有余了。

假定现在已知模式串为,我将结果和序号一一标示在下边

0 1 2 3 4 5 6 7
A B A C A B A B
-1 -1 0 -1 0 1 2

此时i=2(i=next[j-1]),为求解B的next值,根据算法比对前缀ABAC与后缀ABABpat[j]!=pat[i],B!=C),不匹配,这个时候就是关键点了。

直接从字面上寻找,当然很快就能找到s[1] = s[7]的事实,也就是next[7] = 1。

如果这个过程改为p于自身匹配,这个时候就很好理解了。

A B A C A B A B

- - - - A B A C A B A B

模拟pmatch的过程,现前循环是这样的。发现B!=C,此时不是从头开始再次匹配,而是回溯到某一个位置重新比对,也就是next[i](next[2]=0),这就是为什么i可以通过赋值next[i]来迭代寻找的核心原因!

A B A C A B A B

- - - - - - A B A C A B A B

此时s[7]==p[1],next[7]=1!因为谁也不能保证p[next[i]]=p[j],因此这里用while循环。其实和pmatch里的while是一个道理,我并不清楚回溯之后string[i]和pat[j]是否相等,所以while里j的值一直迭代。


总结一下,其实求解next数组受益于求解前后缀,其实质就是在匹配自身字符串,大致的核心思想都是相同的。



Bingo

@BentleyJobs

Graduated from JNU, interested in cg & game developing, once worked in Aurogon to develope GuJianOL. See about for more info.