从零开始手敲次世代游戏引擎(五)

总体思路确定了,我们进入编码。首先搭个架子。(演示命令行为Linux。Windows大部分类似,小部分命令名字不同请自行置换)

确认我们目前所处的位置:

[tim@iZ625ivhudwZ GameEngineFromScratch]$ git branch
* article_1
  master

新建一个branch用于保存本篇文章开发的内容:

[tim@iZ625ivhudwZ GameEngineFromScratch]$ git checkout -b article_5
Switched to a new branch 'article_5'
[tim@iZ625ivhudwZ GameEngineFromScratch]$ git branch
  article_1
* article_5
  master

新建一个目录,用于存放框架。其中再建立一个Common子目录,用于存放各平台通用代码;建立一个Interface子目录,用于存放模块间接口代码:

[tim@iZ625ivhudwZ GameEngineFromScratch]$ mkdir Framework
[tim@iZ625ivhudwZ GameEngineFromScratch]$ cd Framework
[tim@iZ625ivhudwZ Framework]$ mkdir Common
[tim@iZ625ivhudwZ Framework]$ mkdir Interface
[tim@iZ625ivhudwZ Framework]$ dir
Common  Interface

现在整个项目目录的结构如下:

[tim@iZ625ivhudwZ GameEngineFromScratch]$ tree .
.
├── Framework
│   ├── Common
│   └── Interface
├── LICENSE
├── main.c
└── README.md

将main.c移动到Framework/Common之下。也就是说,我们准备在不同平台直接共用同一个程序入口:

[tim@iZ625ivhudwZ GameEngineFromScratch]$ mv main.c Framework/Common/
[tim@iZ625ivhudwZ GameEngineFromScratch]$ tree .
.
├── Framework
│   ├── Common
│   │   └── main.c
│   └── Interface
├── LICENSE
└── README.md

3 directories, 3 files

在Framework/Interface之下新建Interface.hpp

[tim@iZ625ivhudwZ GameEngineFromScratch]$ vi Framework/Interface/Interface.hpp

通过宏定义定义几个alias,用以提高代码的可读性:

#pragma once

#define Interface class

#define implements public

在Framework/Interface执行新建IRuntimeModule.hpp

vi Framework/Interface/IRuntimeModule.hpp

在其中定义每个Runtime Module都应该支持的一些方法:

#pragma once

#include "Interface.hpp"

namespace My {
        Interface IRuntimeModule{
public:
        virtual ~IRuntimeModule() {};

        virtual int Initialize() = 0;
        virtual void Finalize() = 0;

        virtual void Tick() = 0;
        };

}

说明一下:

#pragma once

这是是声明这个头文件在编译的时候只需要处理一次。项目大了,同一个头文件可能会被多个源文件(.cpp文件)包含。由于编译器处理包含文件的方式是将其展开在源文件当中,所以如果不加这个条件编译指令,头文件的内容会在多个源文件里面多次展开,那么编译器就会报错,说同一个东西被多次定义。

这个条件编译指令对于很老的C/C++编译器来说是不认识的。如果遇到这种情况,就需要将其改成下面这种形式:

#ifndef __INTERFACE_H__
#define __INTERFACE_H__

<代码>

#endif // __INTERFACE_H__

效果是一样的。就是啰嗦一些。

       virtual ~IRuntimeModule() {};

虚函数的析构函数。因为是空函数,这个在Visual Studio里面不定义也是可以的。但是按照严格的C++标准,包括《C++ Primer》这本书里面的推荐做法,对于有其他虚函数的类,建议把析构函数也声明为virtual。这是因为如果不这么做,那么当使用基类指针释放派生类的实例的时候,可能导致只调用了基类的析构函数,从而产生memory leak的情况。在某些平台上,比如PSV,如果不定义这个虚析构函数,编译器会报Warning。

       virtual int Initialize() = 0;
        virtual void Finalize() = 0;

        virtual void Tick() = 0;

纯虚成员函数。定义为纯虚函数的目的是强制派生类实现这些方法。可以有效避免遗漏。

然后再说一下这3个函数(接口)的作用:

  1. Initialize(), 这是用来初始化模块的
  2. Finalize(),这是用来在模块结束的时候打扫战场的
  3. Tick(),这个是用来让驱动模块驱动该模块执行的。每调用一次,模块进行一个单位的处理

之所以要单独定义模块的初始化/反初始化函数,而不是在类的构造函数/析构函数里面完成这些工作,主要是有以下一些考虑:

  1. 在C/C++当中,全局变量(包括static变量)的初始化顺序是不可预知的(未定义的)。对于不同的平台,可能顺序不同。类当中的成员变量的初始化顺序也有类似的问题
  2. 有些模块我们可能只是想预加载到内存,后面再初始化。或者有些模块在流程当中可能出现临时不用的情况,想要释放相关的平台资源,但是想保留其状态,并不想将其从内存卸载。

接下来定义Application接口。这个接口用于抽象化不同平台的Application(并将其模块化),使得我们可以用同一个主入口(main.c)启动程序(也意味着我们可以使用同一套启动参数)

[tim@iZ625ivhudwZ GameEngineFromScratch]$ vi Framework/Interface/IApplication.hpp

内容如下:

#pragma once
#include "Interface.hpp"
#include "IRuntimeModule.hpp"

namespace My {
        Interface IApplication : implements IRuntimeModule
        {
        public:
                virtual int Initialize() = 0;
                virtual void Finalize() = 0;
                // One cycle of the main loop
                virtual void Tick() = 0;

                virtual bool IsQuit() = 0;
        };
}

可以看到它继承了我们刚才定义的IRuntimeModule,重载了IRuntimeModule的3个接口,另外增加了一个公共接口:IsQuit(),用于查询应用程序是否需要退出。这是因为,在很多平台上用户关闭应用程序都是通过系统通知过来的。我们的程序自身并不会直接进行这方面的判断。所以当我们收到这样的关闭通知的时候,我们就通过这个接口告诉主循环,我们该结束了。

可以看到这仍然是一个纯虚类。接下来我们可以直接从这个类派生出各个平台的Application类。但是实际上,各个平台的Application虽然有很多不同,共通点也是很多的。提高代码可维护性的一个重要做法,就是要避免同样的代码分散在不同的文件当中。否则很容易出现只改了一处而没有改其他的情况。

因此,我们在Framework/Common下面,新建两个文件,用来提供各平台共通的Application实现:

[tim@iZ625ivhudwZ GameEngineFromScratch]$ touch Framework/Common/BaseApplication.{hpp,cpp}

大括号是Linux Bash情况下的一种Hack,就是小技巧,可以一次生成两个文件。如果是Windows,请分别生成这两个文件。

现在我们编辑这两个文件:

BaseApplication.hpp

#pragma once
#include "IApplication.hpp"

namespace My {
        class BaseApplication : implements IApplication
        {
        public:
                virtual int Initialize();
                virtual void Finalize();
                // One cycle of the main loop
                virtual void Tick();

                virtual bool IsQuit();

        protected:
                // Flag if need quit the main loop of the application
                bool m_bQuit;
        };
}

BaseApplication.cpp

#include "BaseApplication.hpp"

// Parse command line, read configuration, initialize all sub modules
int My::BaseApplication::Initialize()
{
        m_bQuit = false;

        return 0;
}


// Finalize all sub modules and clean up all runtime temporary files.
void My::BaseApplication::Finalize()
{
}


// One cycle of the main loop
void My::BaseApplication::Tick()
{
}

bool My::BaseApplication::IsQuit()
{
        return m_bQuit;
}

好了。这个类里面有一个受保护的变量m_bQuit,用于记录应用程序是否被通知退出。

最后让我们来修改我们的main.c。首先把它重新命名为main.cpp,因为我们用到了C++的特性,类。然后改写成下面这个样子:

#include <stdio.h>
#include "IApplication.hpp"

using namespace My;

namespace My {
        extern IApplication* g_pApp;
}

int main(int argc, char** argv) {
        int ret;

        if ((ret = g_pApp->Initialize()) != 0) {
                printf("App Initialize failed, will exit now.");
                return ret;
        }

        while (!g_pApp->IsQuit()) {
                g_pApp->Tick();
        }

        g_pApp->Finalize();

        return 0;
}

因为我们将不同平台的应用程序进行了抽象,所以我们的main函数不需要关心我们目前到底是工作在哪个平台。我们只需要通过IApplication接口提供的方法进行调用就可以了。

好了,一个基本的架子我们已经搭建好了。但是要让它跑起来之前,我们还需要做一些事情。什么事情?注意这一行:

namespace My {
        extern IApplication* g_pApp;
}

我们需要定义一个具体的Application实例。让我们新建一个Empty目录,代表一个特殊的平台(无平台),然后在里面写一个EmptyApplication.cpp, 来创建这个实例:

#include "BaseApplication.hpp"

namespace My {
        BaseApplication g_App;
        IApplication* g_pApp = &g_App;
}

好了,现在我们的项目差不多是这个样子:

[tim@iZ625ivhudwZ GameEngineFromScratch]$ tree
.
├── Empty
│   └── EmptyApplication.cpp
├── Framework
│   ├── Common
│   │   ├── BaseApplication.cpp
│   │   ├── BaseApplication.hpp
│   │   └── main.cpp
│   └── Interface
│       ├── IApplication.hpp
│       ├── Interface.hpp
│       └── IRuntimeModule.hpp
├── LICENSE
└── README.md

4 directories, 9 files

为了编译它,我们需要创建CMakeLists.txt。首先我们需要在项目根目录创建一个如下:

cmake_minimum_required (VERSION 3.1)
set (CMAKE_C_STANDARD 11)
set (CMAKE_CXX_STANDARD 11)
project (GameEngineFromScrath)
include_directories("${PROJECT_SOURCE_DIR}/Framework/Common")
include_directories("${PROJECT_SOURCE_DIR}/Framework/Interface")
add_subdirectory(Framework)
add_subdirectory(Empty)

注意前三行是因为我们的引擎后面会用到一些C/C++ 11的特性。现在删掉这三行也可以。

然后是Framework目录里面需要一个:

add_subdirectory(Common)

这个只是用来完成一个CMake的递归搜索

Framework/Common下面需要一个,用来建立Framework库:

add_library(Common
BaseApplication.cpp
main.cpp
)

最后就是Empty目录下面一个,用来建立Empty平台的最后的可执行文件:

add_executable(Empty EmptyApplication.cpp)
target_link_libraries(Empty Common)

好了,可以编译了。仍然是采用out of source tree的方式,退回到项目根目录,创建一个build目录,进入build目录,执行

cmake ..
make

就可以了。生成文件为build/Empty/Empty(.exe)

如果是Linux,也可以采用docker的方式进行编译。这也是我推荐的方式,可以有效避免在系统里安装一大堆开发用的包和工具。同样回到根目录,执行

[tim@iZ625ivhudwZ GameEngineFromScratch]$ docker run -it --rm -v $(pwd):/usr/src tim03/clang
bash-4.4# cd build
bash-4.4# cmake ../
-- The C compiler identification is GNU 6.3.0
-- The CXX compiler identification is GNU 6.3.0
-- Check for working C compiler: /usr/bin/gcc
-- Check for working C compiler: /usr/bin/gcc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /usr/src/build

bash-4.4# make
Scanning dependencies of target Common
[ 20%] Building CXX object Framework/Common/CMakeFiles/Common.dir/BaseApplication.cpp.o
[ 40%] Linking CXX static library libCommon.a
[ 60%] Built target Common
Scanning dependencies of target Empty
[ 80%] Building CXX object Empty/CMakeFiles/Empty.dir/EmptyApplication.cpp.o
[100%] Linking CXX executable Empty
[100%] Built target Empty
bash-4.4# Empty/Empty
^C

注意我们这个引擎目前正常情况下不会有任何输出,而且会死循环。按Ctrl+C退出。

从零开始手敲次世代游戏引擎(四)

上一篇我们在Linux上建立了Clang的编译环境。现在我们可以开始着手引擎的开发了。

软件开发有几种模式。传统的是瀑布式,就是先完成顶层的设计,然后进行细节设计和实现。这种方式有一个隐含的假设,那就是”成竹在胸”。好比一个造船厂,已经有着丰富的造船经验,对于新用户的新订单,虽然有着很多定制化要求,但是整体的技术已经十分稳定,就可以采用这种模式。另外一种常用的则是迭代开发,将每次迭代控制在一个较小可控的范围之内,每次迭代都产生一个比上一个版本进化了的,功能受限但是可执行的版本,日积月累,是一个不断试错的过程。这种方式就好比”摸着石子过河”,是一种探索式的开发方式,适用于开发经验不足的领域或者需求不定的领域。

— 脱线开始 —

本系列的目的主要是展示一个挑战的过程,就好比直播一个《血源》的游戏过程。展示也有两种方法,一种是先在线下将《血源》玩成割草游戏,然后上线展示;另外一种则是展示一个普通玩家的挑战学习过程。本系列的立意是后者。

这是因为很多事情,说是一回儿事情,做起来就是另外一回儿事情了。很多事情都是道理很简单但是操作起来就有无数的细节。就好比开发一个引擎是很难,但并不是难到里面写的都是外星文。事实上,知识都在那里,概念很多人也都知道,当代先进的商用引擎也都开源放在那里。建造一个引擎,甚至是打造一个主机平台,这里面主要是工程的问题。而这些问题,即使是开源,没有实际动手参与其中的,也很难知道。

所谓”工欲善其事,必先利其器”。在笔者的日常工作当中,遇到了很多不读平台文档,不做准备工作就开始匆匆上马开发的人员。最终他们都卡在了操作细节上,而不是所谓的大局观。有明明没有连上所有配线却在问为啥无法访问开发机的,有没有安装sdk却问编译为啥不通过的,有到了游戏快发布却不知道在哪里看控制台输出的log的,有不知道调试程序并不是一定要打包安装之后才可以进行的,有到了出补丁的时候却忘记了原始版本放在哪里的。这样的例子并不是少数。

— 脱线结束 —

虽然我们选用第二种方式,但是我们仍然需要有一个目标和基本方向。现实社会是复杂非线性的,任何模型只能是某种程度上的近似,迭代也只能发生在这个方向上。

下面就是关于这个引擎的一些基本考虑:

  1. 该引擎的设计意图是用于教学演示目的
  2. 因此代码应该简单易读
  3. 模块清晰,且容易替代,方便做各种改进和尝试
  4. 不依赖任何特定硬件,尽量使用标准技术
  5. 随时追加新校则(?)

指导本开发总体的主要书籍是

gameenginebook.com/

在具体的设计实现方面,尽量吸收当时能够获取到的最新信息。

同时当然十分欢迎感兴趣的朋友的加入。无论是通过评论的方式,投稿的方式,还是github的pull request的方式。

(– 题外话开始 –)

知乎的手机app写作功能还是十分受限,所以这两期格式上会比较单调。日后上pc修正。

(– 题外话结束 –)

一个游戏引擎的主体架构,可能随着游戏类型的丰富和游戏复杂度的提升,在最近的10-20年里有了很多的变化。比如2D到3D的进化,线性流程到开放世界的进化,确定性AI到不确定性AI的进化,卡通风格画面到电影品质画面的进化,单机到网络的变化,小团队制作到动则数百人大制作的变化,等等。

然而,如果从计算机硬件的角度来看,无论是哪种类型的游戏,其基本的流程又是十分类似的。获取外部的输入,执行某种策略,更新场景物体状态,绘制画面,输出画面。

从软件系统分类角度来看,游戏系统属于软实时系统。所谓软实时系统,就是它首先要求特定的任务在特定的时间片段内完成(保持一定的帧率)。但如果没有完成,也不会造成伤及人身的重大事故的系统。

在工业界,对于用于生产的实时系统,一般是采用实时操作系统。这种系统所执行的每个任务的执行时间都是事先设计好的。到了时间没有执行完,就会执行紧急措施。

在主机游戏开发领域,早期的游戏开发其实很类似这种情况。那时候的主机并没有什么游戏引擎的概念,连一个像样的操作系统也是没有的。游戏开发工程师们读着硬件的各种参数资料,数着byte和cycle,用一张庞大的spreadsheet计算着各种资源的存放位置和调用周期。这种风格的开发至少延续到PS2世代。在那个世代里,最强的是日本的开发商。因为他们有秋叶原,有极强的计划性。这些老头们现在仍然是日本大厂的中坚力量,然而他们显然已经被时代甩在了后面。

随着硬件的摩尔定律式的发展,游戏内容容量也出现雪崩式增长,这样的开发手法已经变得没有实际操作性了。人们已经不可能去一一指定所有的细节,取而代之的则是制定一些规则,让计算机按照这些规则自己去调度安排资源分配,这就是操作系统和引擎runtime开始导入到主机的一个重要原因。而这些是欧美的强项,游戏圈因此也出现了往欧美一边倒的局面。

另外一个方面,如此庞大的内容资源制作和管理也日渐成为游戏开发的核心问题。增加人力自然是一个办法,也是业界最先采用的方法。然而人是典型的非线性资源,每增加一个人带来的不仅仅是一个人的工资,更是增加了(n*(n-1))/2-((n-1)*(n-2))/2=n-1条沟通路径,和无数的不确定性,劳动生产率快速下降。

因此,队伍规模不能无限地扩张。于是下一个目标就是提高生产单位的生产率。加班加点从原来的忘我工作成为了现在的强制要求。

然而即使是这样也是不够的。疲劳在累积,到了一定程度人会作自由落体运动。新人补进来又是长长的适应周期。

所以剩下的办法就是改革生产工具,计算机不仅仅是用来跑游戏,还要用来写游戏。这就萌生了所谓游戏制作pipeline的概念,也是游戏制作走向工业化的标志。

知乎里有很多诸如“中国为什么没有AAA大作?”这样的问题,有很多大牛从很多方向做了解答。我觉得答得都很棒。但是我想加一条,那就是中国的游戏开发,甚至是更为广泛的软件开发,其生产工业化水平较低,这也是一个很重要的原因。

又跑远了。国内能写这种总结式批评文章的人太多,知乎上就一大把,我还是回到实际操作上。

当代引擎除了runtime,还有一个重要的组成部分就是editor。这个editor就是用来提高生产率的。在以前,游戏的各种资源必须由程序一个一个放进去。而有了editor,游戏本身的制作就可以交给程序以外的人员了。《古墓丽影4》开放了一个关卡编辑器,那个就是当代引擎editor部分的祖先之一。editor在计算机软件属于应用程序,和runtime的性质很不一样。但是,editor的性质和游戏却是很相近的。比如南梦宫的《坦克大战》,里面就是自带关卡编辑器的,而且这个编辑器小孩都能玩得很溜。

前面提到我们的引擎设计的一个要求是要跨平台。然而editor所要求的GUI部分是最难跨平台的部分之一,因为不同平台这部分的API实在是差得太远。但是我们既然要写一个跨平台游戏引擎,那么为什么不就用这个引擎来跑editor呢?这其实也是目前主流商用引擎比较广泛采用的一个办法。当然游戏的渲染引擎与GUI的绘制方式上有很多本质的差别,一些著名的商用引擎至今没有很好用的GUI控件,这个我们后面相关章节再讨论。

综合以上,我们可以确定开发顺序是先进行runtime的开发,然后是editor的开发。

根据上面写了的一般游戏的通用处理流程,我们可以很自然地想到我们将会需要下面这些模块:

  1. 输入管理模块,用来获取用户输入
  2. 策略模块,用来执行策略
  3. 场景管理模块,用来管理场景和更新场景
  4. 渲染模块,用来执行渲染和画面输出
  5. 音频音效模块,用来管理声音,混音和播放
  6. 网络通信模块,用来管理网络通信
  7. 文件I/O模块,用来管理资源的加载和参数的保存回复
  8. 内存管理模块,用来调度管理内存上的资源
  9. 驱动模块,用来根据时间,事件等驱动其它模块
  10. 辅助模块,用来执行调试,log输出等辅助功能
  11. 应用程序模块,用来抽象处理配置文件,特定平台的通知,创建窗口等需要与特定平台对接的部分

从零开始手敲次世代游戏引擎(三)

前一篇文章我们建立了windows上的clang工具链。
为了今后时刻可以检验我们的代码也可以在LINUX等环境下正常编译运行,我们现在开始建立LINUX下的编译环境。
LINUX可以选择任意一个发行版本。不同的发行版本在api接口方面是接近的,主要的不同是软件包的版本,以及包管理方式等。
ubuntu系列更新比较频繁,包体的版本较新。
centos系列是redhat系列的免费版,比较稳扎稳打,包体相对旧一些。
debian则更新非常的慢了。
bsd系列(这个严格来说不是LINUX,而是类UNIX系统) 中规中矩,严守unix规范,相对难用一些。
最重要的是,是否有最大的两家GPU厂商的驱动支持。目前N卡的支持情况相对较好,A卡之前很差,现在有了OpenGPU这个项目,以及amdpro系列驱动,情况有所好转。
我所用的是带GPU的阿里云主机,有A卡版本和N卡版本。N卡版本比A卡版本贵不少,而且考虑到当代主机是以A卡为主,所以选用了A卡主机。
可能因为虚拟化的关系,阿里云上的A卡主机需要阿里云和AMD合作定制的特殊驱动。AMD官网上下载的驱动装上之后设备初始化会出错。而阿里云不提供这个特殊版驱动的单体,只提供预装了驱动的系统镜像。目前可选的只有CentOS和ubuntu。
需要注意的是由于amdpro驱动是内核模块插入方式,因此对内核的升级就比较敏感。亲测无论是选用CentOS还是ubuntu,如果升级最新内核就会导致驱动失效,所以在新驱动出来之前需要锁定内核版本。
没有云服务器的,也可以在windows机器上安装一个vmware虚拟机,在那个里面装一个linux。免费的vmware workstation player就可以。注意目前对显卡支持较好的vmware是12以上的版本。早期版本只能支持到DX9级别,也就是固定管道渲染。其它虚拟机对显卡的支持更差,比如virtualbox。
Linux下构建toolchain的过程与windows类似(参考上一篇文章),可以使用yum或者apt安装发行版提供的编译好的包,也可以采用下载源码编译的方式。
作为演示,我这里采用另外一种方式,导入docker。docker是一种将特定应用环境容器化的方式,可以是实现应用的快速部署,以及和宿主环境的隔离。
docker入门教程参考:
docker.org.cn/book/dock
安装好docker之后,使用:
docker pull tim03/clang
就可以将我事先准备好的clang环境部署到本地。创建这个docker image的dockerfile则在下面这个地址,如果你想要知道它是怎么创建出来的话:
github.com/netwarm007/d
当docker pull全部完成之后,我们进入到我们Hello Engine所在的目录(就是main.c所在目录),执行:
docker run -it –rm -v $(pwd):/usr/src tim03/clang
我们就会进入带有clang的docker容器,并且我们的源代码目录被映射到了/usr/src目录之下。执行下面的命令编译并观察执行结果:

bash-4.4# ls
LICENSE README.md a.out main.c
bash-4.4# clang main.c
main.c:3:1: warning: return type of 'main' is not 'int' [-Wmain-return-type]
void main() {
^ main.c:3:1: note: change return type to 'int'
void main() {
^~~~ int
1 warning generated.

bash-4.4# ls -l
total 24
-rw-rw-r-- 1 1000 1000 1067 Aug 18 20:12 LICENSE
-rw-rw-r-- 1 1000 1000 100 Aug 18 20:12 README.md
-rwxr-xr-x 1 root root 10368 Aug 18 22:02 a.out
-rw-rw-r-- 1 1000 1000 66 Aug 18 20:15 main.c
bash-4.4# ./a.out
Hello, Engine!
bash-4.4# exit

从零开始手敲次世代游戏引擎(二)

开始摆脱Visual Studio,建立独立的Toolchain

上一篇我们写了一个最基本的Hello Engine,并用Visual Studio的命令行工具,cl.exe进行了编译。

然而,Visual Studio只能在Windows上面使用。而且Visual Studio对C/C++进行了很多非标准的扩展。因此基于Visual Studio写出来的程序,除非你写的时候就很清楚哪些可以用哪些不可以用,否则基本是不可以移植到别的平台的。因为Windows并不是一个POSIX (POSIX – Wikipedia)系统,也就是说是一个非常不“标准”的系统。基于这样的系统的API写出来的程序基本只能跑在这个系统上。

我打算让这个手打引擎跑在所有我可以接触到的平台上。目前我可以接触到的平台有:Windows/Linux/PS4/PSV/Android/IOS

所以我需要打造一个独立于特定平台的编译工具包,也就是Toolchain。

目前在开源领域用得比较多的Toolchain是GCC和Clang。GCC历史比较长,很多开源软件,包括Linux内核都是GCC编译的。但厚重的历史也使其很臃肿,里面包括很多已经死掉的东西。而Clang则较年轻,现在也比较流行。

另外,PS4的编译器就是基于Clang的。AMD的OpenGPU计划,以及Vulkan图形API等也是基于Clang的。苹果的最新开发平台一样是基于Clang的。所以,我选择Clang。

准备编译Clang的环境

Clang的项目页面在Clang – Getting Started

首先我们按照Clang项目页面的提示,在Windows上面安装Subversion,这个是获取Clang源代码用的。我推荐安装TortoiseSVN,这个相对比较好用。注意命令行工具缺省是不安装的,需要手工勾选安装。

Home · TortoiseSVN

然后是CMake。我们在Visual Studio里面建立工程的时候,会自动创建Solution和Project文件来进行代码的组织管理和编译选项的存储。然而,这些同样是只有Visual Studio才能使用的文件格式。在Linux等平台上一般是使用make,或者GNU版的make:gmake。make是依靠一个叫做Makefile的文件来存储项目文件清单和编译选项的。可以直接手写,但是文件多了一般我们就希望自动生成。况且,在不同平台上面,虽然都有C/C++编译器,能够编译C/C++代码,但是各种库的头文件、静态链接库、动态链接库的存储位置,甚至是名字都会有很微妙的差异。所以,如果直接手写Makefile,那么结果就是我们需要为每个平台单独写一个。有一些早期GNU软件就是这样的。这很不利于管理。比如我们添加了一个C++文件,那么我们就需要改所有不同版本的Makefile。

所以有一个工具叫Auto Tools,包括automake autoconf等一系列工具。这些工具可以根据一个叫做Makefile.am的模板(与Makefile的区别是里面基本只写项目里的文件,因为这些文件的位置是我们自己可以控制的)自动生成Makefile。这些工具可以为我们自动检测一些常见平台的差异,并在生成的Makefile里面消除这些差异。

然而这个Auto Tools本身也是足够复杂的,使用起来并不是很方便,况且不支持Windows平台。有兴趣的可以参考

Autotools Introduction

CMake是近年兴起的新秀,支持包括Windows在内的诸多平台,使用也比Auto Tools要方便不少。只需要写个CMakelists.txt就可以了。CMake在这里下载。

CMake

安装的时候,同样需要注意,因为我们工作在命令行,需要让安装程序设置环境参数,如上图。否则在命令行会找不到cmake。

接下来是Python。注意Python 2和Python 3是不兼容的。Python 2很古老但是久经考验,Python 3比较新,但是还不是很成熟。我们这里需要的是Python 2.7(因为Clang的test case是2.7接口的)。话说Python近年随着阿尔法🐶大红大紫,因为人工智能领域用Python用得很多。一般来说,越是偏应用方向的(比如人工智能算法研究),越是用高阶的语言(脚本),避免在本来关心的事物之外花费时间。

Download Python

当然,我们这里安装Python是为了跑Clang的测试case,确认我们自己编译出的Clang功能正常。这个步骤是十分重要的。因为如果是编译器的bug带来的问题,一般都可以轻易将码农坑在里面几个月出不来。比如一个变量明显代入了1,后面读出来偏偏变成了2…(CPU Cache控制问题)这种问题是最难查出来的问题之一。

最后是GnuWin32 Tools,这是一组开源命令行工具。Linux什么的都是自带或者可以很方便地安装的。Windows上面就需要下载安装:

GetGnuWin32 – Maintaining a Gnuwin32 Package archive

这些工具数量众多,我们这里主要也是为了跑Clang的测试Case,就不一一展开了。

需要注意的是,网页上能下载的东西只是装了个下载器,装完之后需要进入安装目标目录,执行download.bat和install.bat完成安装。之后需要更改环境变量PATH,保证在我们的命令行里面可以找到这些工具。(具体路径请根据你安装的路径修改)

关于如何改Windows的环境变量,参考下面

jingyan.baidu.com/artic

好了。现在我们重新启动命令行,来使修改的环境变量生效。(命令行窗口会一直保持启动的时候的环境变量,所以改了环境变量之后需要重启命令行才能反映出我们的修改)

输入svn help,看到类似下面的输出,说明subversion安装OK了:

C:UsersTim.AzureADSource>svn help
usage: svn <subcommand> [options] [args]
Subversion command-line client.
Type 'svn help <subcommand>' for help on a specific subcommand.
Type 'svn --version' to see the program version and RA modules
  or 'svn --version --quiet' to see just the version number.

Most subcommands take file and/or directory arguments, recursing
on the directories.  If no arguments are supplied to such a
command, it recurses on the current directory (inclusive) by default.

Available subcommands:
   add
   ...

输入python,看到类似下面的输出,则说明python安装OK了:

C:UsersTim.AzureAD>python
Python 2.7.13 (v2.7.13:a06454b1afa1, Dec 17 2016, 20:53:40) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>

按Ctrl+Z,回车,退出python交互模式。

输入grep,看到类似下面的输出,则说明GnuWin32工具也安装成功了。

C:UsersTim.AzureAD>grep
Usage: grep [OPTION]... PATTERN [FILE]...
Try `grep --help' for more information.

开始编译Clang

Clang是基于LLVM的。所谓LLVM,就是一个小小的虚拟机。这个虚拟机抽象了不同的硬件平台,如x86/arm/mips等。最近还抽象了GPU。有点像Java的VM,但是又和Java的VM很不同。Java的VM是比较高层的,它的byte code包括很多硬件平台并不能直接支持的功能。而LLVM的byte code则是更加接近硬件(CPU/GPU)的实际功能,只不过它是独立于任何一个具体硬件存在的。非常简单粗糙地比喻的话,各种CPU/GPU就好比各个地方的人,说的是各个地方的方言;而LLVM的byte code则有些像普通话,与方言有着比较类似1对1的对应关系。(当然严格地来讲并不是这么回儿事情)

所以,首先需要签出LLVM的代码,如下操作:

C:UsersTim.AzureADSource>svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm

Clang是作为LLVM的一个前端,即,把C/C++翻译为LLVM可以懂的byte code的工具。LLVM再把byte code翻译为具体的机器指令。执行下面的命令签出Clang的代码并放在LLVM妥当的位置:

C:UsersTim.AzureADSource>cd llvmtools

C:UsersTim.AzureADSourcellvmtools>svn co http://llvm.org/svn/llvm-project/cfe/trunk clang

还记得我们前面编译的main.c吗?编译产生的输出,也就是中间文件main.obj,target文件main.exe都是和main.c在一个目录里的。

C:UsersTim.AzureADSourceReposGameEngineFromScratch>dir
 驱动器 C 中的卷是 OS
 卷的序列号是 38A2-CBDD

 C:UsersTim.AzureADSourceReposGameEngineFromScratch 的目录

2017/08/18  09:31    <DIR>          .
2017/08/18  09:31    <DIR>          ..
2017/08/18  08:30               302 .gitignore
2017/08/18  08:30             1,088 LICENSE
2017/08/18  09:29                71 main.c
2017/08/18  09:31            97,280 main.exe
2017/08/18  09:31             1,285 main.obj
2017/08/18  08:30               103 README.md
               6 个文件        100,129 字节
               2 个目录 883,355,103,232 可用字节

对于简单的程序我们可以这样。对于大型软件来说,如果我们这样编译,成千上万的中间文件会把整个目录搞得混乱不堪,非常不利于管理。最为关键的是,如果我们的代码支持一些编译选项,可以从一套代码里编译出不同的版本(比如最常见的,Debug版和Release版),那么不同编译选项编译所生成的中间文件就会相互覆盖,最后搞得编译器也弄不清楚哪些文件编译过,是怎么编译的(按照什么选项编译的)。在我们码农的日常当中,如果我们遇到了一个项目第一次编译得过,第二次开始就出错,有的时候clean了重新编译也没用,那么多半就是这个原因了。

这种编译方式老外叫做”build in the (source) tree”,这是不良的习惯。我们应该改掉。推荐的是“build outside the (source) tree”

所以让我们从llvmtools这个目录出去,然后建立一个build目录,专门用来保存编译过程当中生成的文件。

C:UsersTim.AzureADSourcellvmtools>cd ....

C:UsersTim.AzureADSource>mkdir build

C:UsersTim.AzureADSource>cd build

C:UsersTim.AzureADSourcebuild>

因为我们现在电脑上还只有Visual Studio所提供的编译工具,所以我们需要使用CMake工具来生成Visual Studio所需的Solution文件和Project文件,以便使用Visual Studio来编译LLVM

C:UsersTim.AzureADSourcebuild>cmake -G "Visual Studio 15" ..llvm

-G “Visual Studio 15” 表示生成Visual Studio 2017用的项目文件。为什么叫”Visual Studio 15″,这是因为在Visual Studio 6之后,微软改变了产品命名方式,Visual Studio 7叫Visual Studio .NET了。后面的版本更是,一会儿差一年一会儿差两年的。但是实际上他们内部仍然继续着这个序号,证据就是你看Windows里面的注册表当中的信息,就知道这个序号仍然在继续。(Office也是类似)

所以,从6开始数,Visual Studio 2017正好是15,Visual Studio 2015是14,Visual Studio 2013则是12。(嗯?13呢?被吃掉了?估计是13这个数字风水不好。。。)

如果记不住,可以用cmake –help命令查看:

C:UsersTim.AzureADSourceReposGameEngineFromScratch>cmake --help
Usage

  cmake [options] <path-to-source>
  cmake [options] <path-to-existing-build>

Specify a source directory to (re-)generate a build system for it in the
current working directory.  Specify an existing build directory to
re-generate its build system.

Options
  -C <initial-cache>           = Pre-load a script to populate the cache.
  -D <var>[:<type>]=<value>    = Create a cmake cache entry.
  -U <globbing_expr>           = Remove matching entries from CMake cache.
  -G <generator-name>          = Specify a build system generator.
  -T <toolset-name>            = Specify toolset name if supported by
                                 generator.
  -A <platform-name>           = Specify platform name if supported by
                                 generator.
  -Wdev                        = Enable developer warnings.
  -Wno-dev                     = Suppress developer warnings.
  -Werror=dev                  = Make developer warnings errors.
  -Wno-error=dev               = Make developer warnings not errors.
  -Wdeprecated                 = Enable deprecation warnings.
  -Wno-deprecated              = Suppress deprecation warnings.
  -Werror=deprecated           = Make deprecated macro and function warnings
                                 errors.
  -Wno-error=deprecated        = Make deprecated macro and function warnings
                                 not errors.
  -E                           = CMake command mode.
  -L[A][H]                     = List non-advanced cached variables.
  --build <dir>                = Build a CMake-generated project binary tree.
  -N                           = View mode only.
  -P <file>                    = Process script mode.
  --find-package               = Run in pkg-config like mode.
  --graphviz=[file]            = Generate graphviz of dependencies, see
                                 CMakeGraphVizOptions.cmake for more.
  --system-information [file]  = Dump information about this system.
  --debug-trycompile           = Do not delete the try_compile build tree.
                                 Only useful on one try_compile at a time.
  --debug-output               = Put cmake in a debug mode.
  --trace                      = Put cmake in trace mode.
  --trace-expand               = Put cmake in trace mode with variable
                                 expansion.
  --trace-source=<file>        = Trace only this CMake file/module.  Multiple
                                 options allowed.
  --warn-uninitialized         = Warn about uninitialized values.
  --warn-unused-vars           = Warn about unused variables.
  --no-warn-unused-cli         = Don't warn about command line options.
  --check-system-vars          = Find problems with variable usage in system
                                 files.
  --help,-help,-usage,-h,-H,/? = Print usage information and exit.
  --version,-version,/V [<f>]  = Print version number and exit.
  --help-full [<f>]            = Print all help manuals and exit.
  --help-manual <man> [<f>]    = Print one help manual and exit.
  --help-manual-list [<f>]     = List help manuals available and exit.
  --help-command <cmd> [<f>]   = Print help for one command and exit.
  --help-command-list [<f>]    = List commands with help available and exit.
  --help-commands [<f>]        = Print cmake-commands manual and exit.
  --help-module <mod> [<f>]    = Print help for one module and exit.
  --help-module-list [<f>]     = List modules with help available and exit.
  --help-modules [<f>]         = Print cmake-modules manual and exit.
  --help-policy <cmp> [<f>]    = Print help for one policy and exit.
  --help-policy-list [<f>]     = List policies with help available and exit.
  --help-policies [<f>]        = Print cmake-policies manual and exit.
  --help-property <prop> [<f>] = Print help for one property and exit.
  --help-property-list [<f>]   = List properties with help available and
                                 exit.
  --help-properties [<f>]      = Print cmake-properties manual and exit.
  --help-variable var [<f>]    = Print help for one variable and exit.
  --help-variable-list [<f>]   = List variables with help available and exit.
  --help-variables [<f>]       = Print cmake-variables manual and exit.

Generators

The following generators are available on this platform:
  Visual Studio 15 2017 [arch] = Generates Visual Studio 2017 project files.
                                 Optional [arch] can be "Win64" or "ARM".
  Visual Studio 14 2015 [arch] = Generates Visual Studio 2015 project files.
                                 Optional [arch] can be "Win64" or "ARM".
  Visual Studio 12 2013 [arch] = Generates Visual Studio 2013 project files.
                                 Optional [arch] can be "Win64" or "ARM".
  Visual Studio 11 2012 [arch] = Generates Visual Studio 2012 project files.
                                 Optional [arch] can be "Win64" or "ARM".
  Visual Studio 10 2010 [arch] = Generates Visual Studio 2010 project files.
                                 Optional [arch] can be "Win64" or "IA64".
  Visual Studio 9 2008 [arch]  = Generates Visual Studio 2008 project files.
                                 Optional [arch] can be "Win64" or "IA64".
  Visual Studio 8 2005 [arch]  = Deprecated.  Generates Visual Studio 2005
                                 project files.  Optional [arch] can be
                                 "Win64".
  Borland Makefiles            = Generates Borland makefiles.
  NMake Makefiles              = Generates NMake makefiles.
  NMake Makefiles JOM          = Generates JOM makefiles.
  Green Hills MULTI            = Generates Green Hills MULTI files
                                 (experimental, work-in-progress).
  MSYS Makefiles               = Generates MSYS makefiles.
  MinGW Makefiles              = Generates a make file for use with
                                 mingw32-make.
  Unix Makefiles               = Generates standard UNIX makefiles.
  Ninja                        = Generates build.ninja files.
  Watcom WMake                 = Generates Watcom WMake makefiles.
  CodeBlocks - MinGW Makefiles = Generates CodeBlocks project files.
  CodeBlocks - NMake Makefiles = Generates CodeBlocks project files.
  CodeBlocks - NMake Makefiles JOM
                               = Generates CodeBlocks project files.
  CodeBlocks - Ninja           = Generates CodeBlocks project files.
  CodeBlocks - Unix Makefiles  = Generates CodeBlocks project files.
  CodeLite - MinGW Makefiles   = Generates CodeLite project files.
  CodeLite - NMake Makefiles   = Generates CodeLite project files.
  CodeLite - Ninja             = Generates CodeLite project files.
  CodeLite - Unix Makefiles    = Generates CodeLite project files.
  Sublime Text 2 - MinGW Makefiles
                               = Generates Sublime Text 2 project files.
  Sublime Text 2 - NMake Makefiles
                               = Generates Sublime Text 2 project files.
  Sublime Text 2 - Ninja       = Generates Sublime Text 2 project files.
  Sublime Text 2 - Unix Makefiles
                               = Generates Sublime Text 2 project files.
  Kate - MinGW Makefiles       = Generates Kate project files.
  Kate - NMake Makefiles       = Generates Kate project files.
  Kate - Ninja                 = Generates Kate project files.
  Kate - Unix Makefiles        = Generates Kate project files.
  Eclipse CDT4 - NMake Makefiles
                               = Generates Eclipse CDT 4.0 project files.
  Eclipse CDT4 - MinGW Makefiles
                               = Generates Eclipse CDT 4.0 project files.
  Eclipse CDT4 - Ninja         = Generates Eclipse CDT 4.0 project files.
  Eclipse CDT4 - Unix Makefiles= Generates Eclipse CDT 4.0 project files.

好了,然后用下面的命令build生成的Solution。 (注意要在Visual Studio的命令行里面。也就是开始菜单里面的Developer Command Prompt)当然你也可以双击LLVM.sln打开Visual Studio的IDE进行编译。效果其实一样的。

C:UsersTim.AzureADSourcebuild>msbuild LLVM.sln

这个编译看机器性能。我在i7 8核的SSD机器上大概1个半小时。


(注意:采用上面的方法编译出的是x86的Debug版本。如果需要便宜x64的Release版本,请如下使用CMake):

cmake -G "Visual Studio 15 2017 Win64" -DCMAKE_BUILD_TYPE=Release -Thost=x64 ..llvm
msbuild ALL_BUILD.vcxproj /V:m /p:Platform=x64 /p:Configuration=Release /t:rebuild

编译完成之后,我们来测试我们编译出的clang是否有问题。首先我们需要将生成物的目录加入环境变量PATH,以便在命令行能够找到它:(目录请根据你的本地实际情况修改)

重启命令行,检查是否可以找到clang

C:UsersTim.AzureADSource>clang -v
clang version 6.0.0 (trunk 311143)
Target: i686-pc-windows-msvc
Thread model: posix
InstalledDir: C:UsersTim.AzureADSourcebuildDebugbin
Found CUDA installation: /Program Files/NVIDIA GPU Computing Toolkit/CUDA/v8.0, version 8.0

最后一行CUDA是我的环境里面别的事情安装的。与目前无关。没有安装的应该看不到这一行。

然后确保我们目前是处于LLVM的顶级目录,就是下面有llvm和build这两个目录的那一级目录,执行下面的命令:

C:UsersTim.AzureADSource>python.exe llvmutilslitlit.py -sv --param=build_mode=Win32 --param=build_config=Debug --param=clang_site_config=buildtoolsclangtestlit.site.cfg llvmtoolsclangtest

我这里的环境是执行会失败,python抱怨找不到一些测试用的程序。需要修改buildtoolsclangtestlit.site.cfg

原来的版本:

## Autogenerated from C:/Users/Tim.AzureAD/Source/llvm/tools/clang/test/lit.site.cfg.in
## Do not edit!

import sys

config.llvm_src_root = "C:/Users/Tim.AzureAD/Source/llvm"
config.llvm_obj_root = "C:/Users/Tim.AzureAD/Source/build"
config.llvm_tools_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_mode)s/bin"
config.llvm_libs_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_mode)s/lib"
config.llvm_shlib_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_mode)s/bin"
config.llvm_plugin_ext = ".dll"
config.lit_tools_dir = ""
config.clang_obj_root = "C:/Users/Tim.AzureAD/Source/build/tools/clang"
config.clang_src_dir = "C:/Users/Tim.AzureAD/Source/llvm/tools/clang"
config.clang_tools_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_mode)s/bin"
config.host_triple = "i686-pc-win32"
config.target_triple = "i686-pc-win32"
config.llvm_use_sanitizer = ""
config.have_zlib = 0
config.clang_arcmt = 1
config.clang_default_cxx_stdlib = ""
config.clang_staticanalyzer = 1
config.clang_staticanalyzer_z3 = ""
config.clang_examples = 0
config.enable_shared = 0
config.enable_backtrace = 1
config.host_arch = "AMD64"
config.enable_abi_breaking_checks = ""

改为

## Autogenerated from C:/Users/Tim.AzureAD/Source/llvm/tools/clang/test/lit.site.cfg.in
## Do not edit!

import sys

config.llvm_src_root = "C:/Users/Tim.AzureAD/Source/llvm"
config.llvm_obj_root = "C:/Users/Tim.AzureAD/Source/build"
config.llvm_tools_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_config)s/bin"
config.llvm_libs_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_config)s/lib"
config.llvm_shlib_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_config)s/bin"
config.llvm_plugin_ext = ".dll"
config.lit_tools_dir = ""
config.clang_obj_root = "C:/Users/Tim.AzureAD/Source/build/tools/clang"
config.clang_src_dir = "C:/Users/Tim.AzureAD/Source/llvm/tools/clang"
config.clang_tools_dir = "C:/Users/Tim.AzureAD/Source/build/%(build_config)s/bin"
config.host_triple = "i686-pc-win32"
config.target_triple = "i686-pc-win32"
config.llvm_use_sanitizer = ""
config.have_zlib = 0
config.clang_arcmt = 1
config.clang_default_cxx_stdlib = ""
config.clang_staticanalyzer = 1
config.clang_staticanalyzer_z3 = ""
config.clang_examples = 0
config.enable_shared = 0
config.enable_backtrace = 1
config.host_arch = "AMD64"
config.enable_abi_breaking_checks = ""

就是把所有的%(build_mode)改为%(build_config)

如果是用vim修改,可以用“:%s/build_mode/build_config/g”这条命令一次修改完毕。

感觉上应该是不同的Visual Studio对于项目文件当中Output目录宏展开的方式不同导致的。

测试正常执行的样子是这样的:

C:UsersTim.AzureADSource>python llvmutilslitlit.py -sv --param=build_mode=Win32 --param=build_config=Debug --param=clang_site_config=buildtoolsclangtestlit.site.cfg llvmtoolsclangtest
lit.py: C:/Users/Tim.AzureAD/Source/llvm/tools/clang/test/lit.cfg:200: note: using clang: 'C:/Users/Tim.AzureAD/Source/build/Debug/bin/clang.EXE'
lit.py: C:UsersTim.AzureADSourcellvmutilslitlitdiscovery.py:190: warning: test suite 'Clang-Unit' contained no tests
-- Testing: 9208 tests, 8 threads --
********************
Testing: 0 .. 10.. 20.. 30.. 40.. 50.. 60.. 70.. 80.. 90..
Testing Time: 843.01s
********************
Failing Tests (2):
    Clang :: Driver/offloading-interoperability.c
    Clang :: Driver/openmp-offload-gpu.c

  Expected Passes    : 9077
  Expected Failures  : 24
  Unsupported Tests  : 105
  Unexpected Failures: 2

1 warning(s) in tests.

星号当中的是进度条。在i7 8核心的机器上大约需要10分钟左右。

我这里执行的过程当中出现一些CUDA相关的错误,应该是版本不匹配(我的是CUDA 8.0,比较新)导致,可以无视。

用新的Toolchain编译我们的Hello Engine

C:UsersTim.AzureADSourceReposGameEngineFromScratch>clang main.c
main.c:3:1: warning: return type of 'main' is not 'int' [-Wmain-return-type]
void main() {
^
main.c:3:1: note: change return type to 'int'
void main() {
^~~~
int
1 warning generated.

可以看到,在Visual Studio下面编译完全没有问题的代码,在clang下面出现了warning。所以,我们需要尽早摆脱微软的安乐窝。(*^_^*)

参考引用

  1. Clang – Getting Started

从零开始手敲次世代游戏引擎(一)

开发环境准备

就如我在(序)中所写,可能由于历史的原因,我更喜欢命令行。

况且,考虑到项目的庞大,和剩下的时日(可能也就1~2万天了),基于已经过去的1万多天的经验,这过程当中比尔盖茨一定会死去,冯氏架构也可能成为遗物。所以,保持代码的独立性尤为重要。用流行的话来说,要奉行极简主义。

另外还是想再啰嗦几句,我写这个的目的更多是为了展现其过程。世面上好的商用引擎已经大多开源,投入的资源和精力绝非我在剩下的区区万日当中能够付出的。我只是想学学去西藏布达拉宫的那些朝拜者者一样,三步九叩,享受这个前进的过程,也是对自己的一个梳理。很可能会挂在半路,但那又如何呢?

总之,我觉得人生来就是一个容器,前1万天是大家给你灌水,后1万天是你给大家灌水。目的是传承,这就是我理解的人生意义。

———- 鸡汤分割线 ————

其实我个人是比较喜欢Linux系统的,因为那个更纯粹。不过考虑到现实读者可能大多数是Windows用户,自己也是从MS系起步的,那么就从Windows开始吧。

首先是给代码找个家,这个很重要。接下来的路很长,充满着未知的危险,我们需要Save-Load大法。

全世界程序员目前比较常用的代码托管工具之一GitHub,我也常用,就在这里安家吧。

netwarm007/GameEngineFromScratch

Git这个工具思想比较奇特,对于没有用过代码管理,或者只用过SVN的人来说,可能不太容易上手。不过GitHub本身是网页版的,下个ZIP包展开还是比较容易的。

Git工具的教程网上很多,需要的自己搜一下。关键字“git 教程”或者“git tutorial”。

关于科技类的搜索,我是推荐谷歌。如果上不去,Bing也是不错的。记得切换到国际版。

关于本地目录,这个随便了,选你自己喜欢的地方就好。要说经验,一般来说源代码文件都是小文件,几k几十k一个,编译的时候特别是链接的时候,toolchain(就是编译链接工具)会同时打开很多个文件。所以对于大的代码树的编译,磁盘IO性能是挺重要的。另外,编译器本身就是计算机科学当中十分深奥晦涩的东西,涉及大量数据结构和算法。在编译的过程当中,会在内存当中生成很多各种各样的数据结构,用来确定寄存器的分配,对代码进行分析和优化。因此,毋庸置疑CPU和内存对于编译效率也是至关重要的。如果内存过小,导致不得不使用页交换文件(就是将硬盘上的一个文件作为内存使用),那编译过程真是爽极了。

扯个无用的,听说NaughtyDog有一台256?核心的超级服务器编译神海,大概可以做到半小时一个版本?(有点记不清了)

Windows系统下可用的Git主要有

Git for Windows

另外Visual Studio里面也有一个版本可以安装的。

toolchain方面,为了跨平台,最终会使用clang。不过就如上面鸡汤的,我这里主要是想用一系列文章展示整个过程,包括换toolchain的过程。所以既然是Windows,我们先用Visual Studio。

安装完Visual Studio(版本随便吧。我自己因为有订阅,各种版本都有。赶时髦就用最新的,2017)之后,在开始菜单里面应该可以找到一个叫Developer Command Prompt的东西,点击那个,就会启动一个命令行。这个命令行与普通命令行的区别是,它里面预先设置好了Visual Studio的工具的查找路径,好比下面这些:

C:UsersTim.AzureADSourceReposGameEngineFromScratch>path
PATH=C:Program Files (x86)Microsoft Visual Studio2017ProfessionalVCToolsMSVC14.10.25017binHostX86x86;C:Program Files (x86)Microsoft Visual Studio2017ProfessionalCommon7IDEVCVCPackages;C:Program Files (x86)Microsoft Visual Studio2017ProfessionalCommon7IDECommonExtensionsMicrosoftTestWindow;C:Program Files (x86)Microsoft Visual Studio2017ProfessionalCommon7IDECommonExtensionsMicrosoftTeamFoundationTeam Explorer;C:Program Files (x86)Microsoft Visual Studio2017ProfessionalMSBuild15.0binRoslyn;C:Program Files (x86)Microsoft Visual Studio2017ProfessionalTeam ToolsPerformance Tools;C:Program Files (x86)Microsoft Visual StudioSharedCommonVSPerfCollectionTools;C:Program Files (x86)Microsoft SDKsWindowsv10.0AbinNETFX 4.6.1 Tools;C:Program Files (x86)Windows Kits10binx86;C:Program Files (x86)Windows Kits10bin10.0.14393.0x86;C:Program Files (x86)Microsoft Visual Studio2017Professional\MSBuild15.0bin;C:WINDOWSMicrosoft.NETFrameworkv4.0.30319;C:Program Files (x86)Microsoft Visual Studio2017ProfessionalCommon7IDE;C:Program Files (x86)Microsoft Visual Studio2017ProfessionalCommon7Tools;C:Program Files (x86)SCEORBIS SDKs4.500host_toolsbin;C:Program Files (x86)SCEORBISToolsTarget Manager Serverbin;C:Program Files (x86)SCEORBISToolsPublishing Toolsbin;C:Program Files (x86)SCEPSP2ToolsPublishing Toolsbin;C:Program FilesNVIDIA GPU Computing ToolkitCUDAv8.0bin;C:Program FilesNVIDIA GPU Computing ToolkitCUDAv8.0libnvvp;C:Program Files (x86)SCECommonSceVSI-VS14bin;C:Program Files (x86)SCECommonSceVSI-VS12bin;C:Program Files (x86)SCECommonSN-DBSbin;C:VulkanSDK1.0.37.0Bin;C:Program FilesNVIDIA GPU Computing ToolkitCUDAv7.5bin;C:Program FilesNVIDIA GPU Computing ToolkitCUDAv7.5libnvvp;C:ProgramDataOracleJavajavapath;C:Program Files (x86)SCEPSP2ToolsTarget Manager Serverbin;C:WINDOWSsystem32;C:WINDOWS;C:WINDOWSSystem32Wbem;C:WINDOWSSystem32WindowsPowerShellv1.0;C:Program FilesIntelWiFibin;C:Program FilesCommon FilesIntelWirelessCommon;C:Program FilesPerforce;C:Program FilesPerforceDVCS;C:Program FilesMicrosoft SQL Server110ToolsBinn;C:Program Files (x86)GtkSharp2.12bin;C:Program FilesMicrosoft SQL Server130ToolsBinn;C:Program FilesGitcmd;C:Program Files (x86)NVIDIA CorporationPhysXCommon;C:Program Files (x86)NVIDIA CorporationCgbin;C:Program Files (x86)NVIDIA CorporationCgbin.x64;C:Program Files (x86)PuTTY;C:Program FilesMATLABR2016bbin;C:Program FilesTortoiseSVNbin;C:Program Files (x86)Windows Kits10Windows Performance Toolkit;C:WINDOWSsystem32;C:WINDOWS;C:WINDOWSSystem32Wbem;C:WINDOWSSystem32WindowsPowerShellv1.0;C:UsersTim.AzureADAppDataLocalProgramsPythonPython36Scripts;C:UsersTim.AzureADAppDataLocalProgramsPythonPython36;C:Program Files (x86)CMakebin;C:UsersTim.AzureADAppDataLocalMicrosoftWindowsApps;C:Program FilesGPAC;

然后打入git命令,将代码库下载到本地。

C:UsersTim.AzureADSourceRepos>git clone https://github.com/netwarm007/GameEngineFromScratch.git
Cloning into 'GameEngineFromScratch'...
remote: Counting objects: 8, done.
remote: Compressing objects: 100% (8/8), done.
remote: Total 8 (delta 2), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (8/8), done.
Checking connectivity... done.

C:UsersTim.AzureADSourceRepos>cd GameEngineFromScratch

C:UsersTim.AzureADSourceReposGameEngineFromScratch>

好了,这里就是将来代码生长的地方了。

为了能方便的回到每篇文章所对应的代码状态,我在每篇文章开始的时候给代码打个tag

C:UsersTim.AzureADSourceReposGameEngineFromScratch>git tag -a v0.0 -m "Initial"

C:UsersTim.AzureADSourceReposGameEngineFromScratch>git tag
v0.0

这样我们就可以通过下面的命令随时回到这个状态

C:UsersTim.AzureADSourceReposGameEngineFromScratch>git checkout -b article_1 v0.0
Switched to a new branch 'article_1'

C:UsersTim.AzureADSourceReposGameEngineFromScratch>git branch
* article_1
  master

新建的article_1这个branch就处于我们这篇文章开头的状态了。

现在让我们来写第一个文件,HelloEngine.cpp。你可以通过输入

C:UsersTim.AzureADSourceReposGameEngineFromScratch>notepad

来启动记事本,开始写你的代码。或者用任何一款编辑器。我个人是喜欢Vim。能在Windows下用的Vim可以从这里安装:

download : vim online

不过这个编辑器是很古老很另类的,缺省不支持鼠标,分为命令模式和编辑模式,全部操作均是键盘完成。如果想要学习,同样,网上有很多资料,请搜索。

另外有个选项是gVim,这个支持鼠标。

https://gvim.en.softonic.com/

好了,在编辑器里敲入如下内容:

#include <stdio.h>

void main() {
	printf("Hello Engine!n");
}

然后保存为main.c (如果是用的vim,按ESC退出编辑模式,输入:w main.c ),退出编辑器(vim的话,输入:q)

回到命令行,输入:

C:UsersTim.AzureADSourceReposGameEngineFromScratch>cl main.c
用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.10.25019 版
版权所有(C) Microsoft Corporation。保留所有权利。

main.c
Microsoft (R) Incremental Linker Version 14.10.25019.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:main.exe
main.obj

这样就好了。打入dir命令看看生成了啥文件:

C:UsersTim.AzureADSourceReposGameEngineFromScratch>dir
 驱动器 C 中的卷是 OS
 卷的序列号是 38A2-CBDD

 C:UsersTim.AzureADSourceReposGameEngineFromScratch 的目录

2017/08/18  09:31    <DIR>          .
2017/08/18  09:31    <DIR>          ..
2017/08/18  08:30               302 .gitignore
2017/08/18  08:30             1,088 LICENSE
2017/08/18  09:29                71 main.c
2017/08/18  09:31            97,280 main.exe
2017/08/18  09:31             1,285 main.obj
2017/08/18  08:30               103 README.md
               6 个文件        100,129 字节
               2 个目录 930,610,339,840 可用字节

执行main.exe

C:UsersTim.AzureADSourceReposGameEngineFromScratch>main.exe
Hello Engine!

好了,基本环境准备完毕。

实施Save大法,保存我们的成果:

C:UsersTim.AzureADSourceReposGameEngineFromScratch>git add main.c

C:UsersTim.AzureADSourceReposGameEngineFromScratch>git commit -m "article 1 end"
[article_1 2be2939] article 1 end
 Committer: Tim <wenli.chen@scesh.cn>
Your name and email address were configured automatically based
on your username and hostname. Please check that they are accurate.
You can suppress this message by setting them explicitly:

    git config --global user.name "Your Name"
    git config --global user.email you@example.com

After doing this, you may fix the identity used for this commit with:

    git commit --amend --reset-author

 1 file changed, 6 insertions(+)
 create mode 100644 main.c

C:UsersTim.AzureADSourceReposGameEngineFromScratch>git config --global user.name "Chen Wenli"

C:UsersTim.AzureADSourceReposGameEngineFromScratch>git config --global user.emal "chenwenli(at)chenwenli.com"

C:UsersTim.AzureADSourceReposGameEngineFromScratch>git commit -m "article 1 end"
On branch article_1
nothing to commit, working directory clean

从零开始手敲次世代游戏引擎(序)

大家好。我“正式”从事软件工程师这个职业已经快15年了。至于编程的历史则更长,有20余年了。记忆当中第一次编程的机器里只有ROM BASIC,用“*”打了个金字塔。屏幕是那种单色的cga,只能显示绿色的字符。

因为这样,至今我也喜欢手敲代码。我的意思是,从零开始一个字符一个字符的敲。我觉得这个过程特别有意思,有成就感。

我从小就喜欢科技类的东西,父母是做航天工业的,所以也算有些基因,也有机会较早接触到电脑。十多岁在爸爸的办公室看到了装在屏蔽笼子里的图形工作站,旁边一台绘图仪。好像是德国进口的?反正单位里没几个人懂德语,国家花了大价钱买来,也没啥人会用,就供在那里。后来来了个研究生,外语比较好,把厚厚的说明书研究了半天,总算调出了一个例程,可以控制绘图仪画一张航天飞机模型的线框图。记得绘图仪左边一共有6只水笔,一个固定在横杆上的机器手(夹子),可以沿横杆前后(y方向)移动;同时横杆自体在步进马达控制下可以水平(x方向)移动。换色就是移动到左边换笔,然后在纸上绘图。夹子有落笔和提笔两种动作。

扯得有些远了,但总之从那时开始便对计算机绘图产生了浓厚兴趣。后来又有了游戏机,就觉得更有兴趣了。

然而直到近年有幸进了SIE,其实之前一直没有机会能够从事和游戏开发直接相关的工作。毕业后做了4年GUI,又搞了4年DLNA,1年中医四诊仪,3年视频监控设备,中间还客串了2年市场部,写了个销售制造管理系统,才终于进入了游戏行业。

目前我支持着国内200余家主机游戏开发商的开发项目。同时也和诸如中国传媒大学,上海交通大学等在一起推进游戏开发相关的教育项目。

不过这个专栏是纯粹的个人分享,与我所在团体和公司无任何直接关系。

我计划在这个专栏里通过一系列文章的分享,完成并展现一个手敲版游戏引擎的制作过程。可能的话,还会包括在合适的开发板上全手工建立操作系统(这部分主要是交叉编译,不是从零手敲),并在上面跑起来这个手敲版引擎的过程。引擎的部分首先着重runtime部分,editor等host tools在runtime之后。runtime部分则首先做图形渲染部分,然后逐渐迭代扩充。

工程十分浩大,老实说我也不知道要多少篇文章才能写完。平常也很忙,人也不是很勤快,十年肯定是不够的,慢慢写吧。反正也不是很在乎有没有人看,😄。

好了,这篇就这样吧,算开个头。

(– EOF –)

本作品采用知识共享署名 4.0 国际许可协议进行许可。

UE4.14.2在安装Vulkan SDK的环境当中编译出错

UE4.14.2在安装Vulkan SDK的环境当中编译出错。具体出错位置是在

VulkanShaders.cpp:

#if (!PLATFORM_ANDROID)
#include <vulkan/icd-spv.h>
#endif

原因是本机安装的Vulkan SDK当中没有icd-spv.h这个文件。可以将UE源代码树当中包含的文件复制到Vulkan SDK对应的目录解决这个问题。

C:…UE4-QAEngineSourceThirdPartyVulkanWindowsIncludevulkan

C:VulkanSDK1.0.37.0Includevulkan

在Linux上纯手工建立Win64交叉编译环境

I have made an dockerfile for steps described in this article:
https://github.com/netwarm007/dockerfile-mingw64

因为想要在linux上面编译gimp给win10(64bit)使用,花了好几天时间研究了一下在Linux上面建立Windows 64bit交叉编译环境的方法。

虽然很多Linux发行版提供mingw64的安装包,但为了最大的自由度,我选择了从源代码手工进行建立的方法。过程很苦逼,但是很能学习到一些东西。

一开始我是选用FreeBSD进行搭建的。但由于FreeBSD系统所带make sed等命令均不是gnu版本,中间遇到一些麻烦,所以又换用基于gnu的Ubuntu。这并不是说FreeBSD不能搭建,只是相对麻烦些。

(主要参考文档:MinGW-w64 – for 32 and 64 bit Windows

首先,需要建立一个交叉编译的目录树。参考了crosstool的相关脚本。为了方便,首先导出几个环境变量:

文件名: setbuilddev.sh

#!/usr/local/bin/bash
export PRJROOT=`realpath ~/work/w64`
export TARGET=x86_64-w64-mingw32
export PREFIX=${PRJROOT}/tools
export BUILD=${PRJROOT}/build-tools
export TARGET_PREFIX=${PREFIX}/${TARGET}

然后是生成交叉编译的目录树:

文件名: createdir.sh

#!/usr/local/bin/bash
if [ -d ${PRJROOT} ]; then
DATE=`date +%y%m%d`
mv ${PRJROOT} ${PRJROOT}_${DATE}
fi
mkdir -p ${PRJROOT}
mkdir -p ${PRJROOT}/build-tools ${PRJROOT}/tools ${PRJROOT}/kernel
mkdir -p ${BUILD}/build-binutils ${BUILD}/build-boot-gcc ${BUILD}/build-gcc
mkdir -p ${BUILD}/build-glibc ${BUILD}/build-glibc-headers
mkdir -p ${TARGET_PREFIX}
之后通过下面的命令建立交叉编译的目录结构。
source ./setbuilddev.sh
chmod +x createdir.sh
./createdir.sh
之后就是准备binutils和gcc的源代码。可以利用crosstool提供的源代码,也可以自己去下。我这里为了以后版本更新的方便,以及追踪我自己的变更,采用了git clone方式。由于国外的git仓库速度太慢,我在Git@OSC上面做了一个中转仓库,加速国内访问的速度。
cd $BUILD
这里我首先遇到的一个问题是,gcc.git太大,即使是从Git@OSC进行clone,中间也会出问题。而git clone一旦出问题,不会在本地保留任何东西,下次必须从头再来。通过查阅互联网上的相关资料,最后找到的解决方法是使用–depth这个选项。(如果是用git://或者git+ssh://类型的REPO似乎可以避免这个问题)
首先
git clone –recursive https://git.oschina.net/netwarm007/gcc.git –depth=100
(–recursive选项是因为这个REPO当中含有gmp/mpc/mpfr 3个子模块。如果是直接使用官方的git repo或者源代码包,则需要另外下载这3个模块。具体参见后文。)
然后
git pull –depth=10000
git pull –depth=100000
git pull –unshallow
根据实际的网络情况,可能需要调整–depth的具体数值。
然后开始交叉编译binutils
cd $BUILD/buid-binutils/
../binutils-gdb/configure –target=$TARGET –prefix=$PREFIX –with-sysroot=$PREFIX
make -j4
make install -j4
注意:如果是FreeBSD等BSD系统,需要使用gmake,而不是make
"-j8"选项需要根据实际编译的环境调整。我用的是4核心环境。如果是8核心,需要改为-j8。这个选项是打开多线程,对于最终编译的结果应该没有影响。也可以添加为环境变量:export MAKEOPTS=-j4,这样就不用每次手动指定了。
注意:由于MAKEOPTS的设置,本文之后的make都不再指定-j4。
之后是gcc的交叉编译。但是编译gcc之前,需要安装TARGET环境的头文件。由于本次是采用mingw-w64,那么就要下载安装mingw-64的源代码。同样,首先是git clone获取源代码。(也可以直接下载源代码包)
cd $BUILD
然后开始安装头文件。首先创建编译用工作目录:(最好不要在源代码目录进行编译,会打乱源代码目录)
mkdir -p $BUILD/build-mingw-w64-header/
cd $BUILD/build-mingw-w64-header/
配置并安装mingw头文件:
../mingw-w64/configure –prefix=$TARGET_PREFIX –without-crt
make install
这里需要特别注意的就是–prefix的指定比较特别。这是因为接下来交叉编译gcc的需要。

不过在此之前,我们需要建几个软link满足gcc的要求:

ln -s $TARGET_PREFIX $PREFIX/mingw
mkdir -p $TARGET_PREFIX/lib
ln -s $TARGET_PREFIX/lib $TARGET_PREFIX/lib64
如果使用的gcc是官方的版本,还需要如下的dependencies:
configure: error: Building GCC requires GMP 4.2+, MPFR 2.4.0+ and MPC 0.8.0+.
Try the –with-gmp, –with-mpfr and/or –with-mpc options to specify their locations.
注意: --with-gmp, --with-mpfr and/or --with-mpc 这几个选项是用来指定install之后的lib的位置的。不是用来指定源代码的位置的。
因此,我们回到$BUILD目录,获取相关的代码:
cd $BUILD
hg clone  https://gmplib.org/repo/gmp/
svn checkout svn://scm.gforge.inria.fr/svn/mpfr/trunk mpfr
然后将这3个目录放在$BUILD/gcc目录的下面。(注意:似乎将一个git clone出来的目录放在另外一个git clone出来的目录里会带来问题。因此,如果gcc目录已经是一个git仓库了,请用
gti archive master | tar -x -C ./gcc/xxx
的方式来将目录放在gcc下面) (注意:这3个组件如果是以源代码的形式和GCC一起编译,必须放在gcc目录下面,并且必须是gmp,mpc,mpfr这样的名字,不能带版本号)
注意gmp目录下面如果没有configure,则需要运行.bootstrap来生成。mpc和mpfr也类似(需要先生成configure)。
接下来我们可以进行gcc的交叉编译了。注意gcc的交叉编译分为两步,第一步只编译编译器本身:

 

cd $BUILD/build-boot-gcc
../gcc/configure –target=${TARGET} –prefix=${PREFIX} –with_sysroot=${PREFIX} –enable-languages=c,c++
make all-gcc
make install-gcc
如果使用master进行编译,有时会遇到错误。
写这篇文章的时候,我这里使用的版本如下:
  • gcc 5.2.0
  • gmp 6.0.0
  • mpc 1.0.3
  • mpfr 3.1.3
注意:如果切换了gcc分支重新编译的时候,需要首先清空build-gcc目录。
现在我们有了交叉gcc了,接下来我们编译C运行时库函数,mingw-crt。
重要: export PATH="$PATH:$PREFIX/bin"
       * 因为从这里起其实已经是交叉编译。要让configure能够找到上面构建的交叉工具 
cd $BUILD/build-mingw-w64-crt
 ../mingw-w64/configure –-host=$TARGET –prefix=$TARGET_PREFIX –without-header –with-sysroot=$TARGET_PREFIX
make
make install
现在我们有了CRT(C语言运行时库)了,可以继续编译交叉GCC相关的库了。
cd $BUILD/build-gcc
../gcc/configure –target=${TARGET} –prefix=${PREFIX} –with_sysroot=${PREFIX} –enable-languages=c,c++
make all
make install
 至此交叉编译器以及相关的头文件、基础库就好了。写一个shell脚本进行交叉编译环境的设定,以供之后交叉编译软件使用。
文件名:setbuilddev_w64.sh
#!/usr/local/bin/bash
export PRJROOT=`realpath ~/work/w64`
export TARGET=x86_64-w64-mingw32
export PREFIX=${PRJROOT}/tools
export BUILD=${PRJROOT}/build-tools
export TARGET_PREFIX=${PREFIX}/${TARGET}
export PATH=${PATH}:${PREFIX}/bin
export PKG_CONFIG_LIBDIR=
export PKG_CONFIG_PATH=$TARGET_PREFIX/lib/pkgconfig
#export LD_LIBRARY_PATH=$TARGET_PREFIX/lib #设置这个会导致某些host工具无法正常运行,所以注释掉。
export C_INCLUDE_PATH=$TARGET_PREFIX/include
export ACLOCAL_FLAGS=”-I $TARGET_PREFIX/share/aclocal”
export CFLAGS=”-I$TARGET_PREFIX/include”
export CPATH=”$TARGET_PREFIX/include”
export LDFLAGS=”-L$TARGET_PREFIX/lib”
好了,大功告成,编译一个hello_world.exe在windows上面跑跑看吧。不仅可以写console程序,还可以写win32程序哦。

让Seafile使用阿里OSS作为Backend

点击下载修改过的alifs原程序

Seafile是一款很不错的开源云盘系统。但是其目前(3.1.5版)并不支持阿里云作为后端存储。

李伟所编写的阿里云CachedAliFS可以将OSS服务以Linux FUSE文件系统的方式进行mount,但由于其并没有实现rename方法,导致Seafile在使用其mount的虚拟文件系统时发生错误。

于是我进一步完善了这个程序,目前我的私有Seafile网站已经运行了2天多,尚没有发现问题。