作为一个跨平台的复杂项目,我们首先要支持在Windows下用Visual Studio开发,Mac下用XCode开发,IDE(Integrated Development Environment)新版本发布也能快速迁移过去,或者打算使用特定的编译器脱离IDE自带的,来达到不同平台下都能顺畅地发布游戏程序。同时,在项目结构上,我们也希望能尽量地简单直接,清晰,比如说我们自己设计一些叫Binary,Intermediate的文件夹去存放编译出来的二进制文件,临时文件;比如按引擎功能模块去建立Rendering,Physical,Audio等文件夹分门别类地存放代码,再自动地按照目录结构包含到项目中。我们需要使用现代化的make工具来完成这些工作。

make工具介绍

开发人员早期会使用某个IDE进行开发工作,比如我在初学者阶段写一个窗口程序去绘制2D/3D图形时,会对着Google搜怎么使用Visual Studio菜单里那么多的设置选项去达到某个目的。而make工具则是通过编写makefile脚本来完成这件事,再通过不同IDE自带的make工具生成出配置相同的项目文件,比如VS的.sln。VS的Make工具就叫nmake,还有linux下常用的make。

  • 各类make工具
    • make
      • 原始的各个平台下的make,比如GNU Make,现在一般不会直接写,而是通过下面提到的其他高级make工具生成makefile,再传给make来生成工程文件
    • cmake
      • 应用广泛,编写体验很一般,因为脚本是DSL(Domain Specific Language),写起来有很多特殊的语法要记忆
    • premake
      • 使用方便,支持用lua编写makefile,对开源仓库提交PR能被采纳,但是文档和生态还是有点小
      • youtube上的游戏引擎教程使用了premake
      • 育碧内部的Ubi Art引擎使用了premake
      • bgfx使用的GENie构建工具也是premake fork出来的一个分支版本
    • xmake
      • 借鉴premake的思路,又额外做了一些新功能
      • 国内做的make工具,也支持lua,暂时没试过
    • bazel
      • 谷歌内部开源出来的make工具,目前很多开源库都开始转向bazel,暂时没试过
    • scons
      • 使用方便,支持用python编写makefile,开源游戏引擎Godot使用了scons
    • ninja
    • autoconf

从这些工具里挑选,我倾向于使用非DSL作为makefile脚本的make工具,所以最常见的cmake我并不喜欢。premake或者xmake我觉得是可以尝试的两个make工具,bazel也可以尝试。
考虑到目前经常打交道的RHI,bgfx也是premake的分支版本,用法基本都一致,所以我选择了premake,这样子我们只需要学习一次,就能同时满足读懂bgfx构建,构建自己的引擎项目两个需求。
开发中遇到麻烦的问题,我们再考虑给premake提交PR,或者切换到xmake之类的试试看。

Lua

学习Premake之前可以快速了解下lua的语法。lua常用于游戏开发,这门语言的特点是很容易集成到自己的C/C++项目中充当脚本模块。不同于python这类设计为一门完整的可独立开发应用的语言,lua除了基本语法和一些系统函数外,什么库也没有,因为它的目的是轻巧,嵌入到工程中充当脚本模块,需要支持哪些功能完全由该工程自己定制,也就是工程自己去绑定C/C++接口到lua层。

很像早期的游戏引擎/软件开发,某些软件会自己定制化一门简单的脚本语言,写好对应的解释器,然后把这门脚本开放给用户,或者上层开发者使用。比如说Maya的mel脚本,或者某些游戏引擎的自定义脚本。而lua的出现,主要是让这些工程的开发者不必自己设计新的脚本语言,达到统一性。至于热更新之类经常被人提起的优点,是因为lua在不开启luaJIT时纯粹解释执行,而脚本语言大多都有纯解释执行的模式,所以热更新并非使用lua的理由。

另外,对编程语言设计感兴趣也可以自己编译lua源代码,调试进去看看内部是怎么运转的,lua也是一个很适合新手学习的开源语言类项目,总共只有一万左右的代码。可以让开发者快速掌控全局或者魔改,这也是选择lua作为脚本模块的一个理由。

对于premake的使用,我们只需要学习简单的lua特性即可:

  • local声明的含义
  • 变量有哪些类型?
    • number
    • bool
      • nil也属于false
    • string
    • nil
      • 常见的函数参数会在函数定义最开始的时候写成是name = name or “Unknown”,怎么理解?
    • table
      • 增删改查操作
    • function
  • 变量命名使用什么命名法比较好?
    • 建议带类型前缀的方式,因为脚本语言变量是万能类型并且没有编译检查
  • if分支,for循环,while循环等控制结构的编写
  • 如何定义全局变量,全局变量可以跨不同的lua文件,是因为什么?( _G表 )
  • 如何使用多个lua文件,dofile和require是什么?
  • 自带库的简单API,比如获取当前时间,获取当前工作目录,文件操作等
  • 超出premake使用需求的部分
    • 脚本语言中,把function作为变量一样传递的好处是什么?( 动态绑定 )
    • 变量赋值哪些情况是引用,哪些情况是复制 (或者说弱引用/强引用)
    • table背后数据结构是什么?关联数组是什么?
    • table使用要注意什么?什么情况会引起空洞?
    • 引入OOP编程
    • 元表,metatable
    • 自定义数据类型,userdata
    • 闭包,在函数内定义新的函数,在声明范围内可以调用
    • luaJIT的使用,以及限制

Premake

Premake构建工程需要准备两样东西,一个是premake5.exe(数字5对应当前premake版本),它集成了一个lua解释器以及按premake api翻译到不同IDE,平台工程规则的逻辑,使用的时候它默认会查找premake5.lua作为程序入口点。

以在Visual Studio 2022中以单个main.cpp构建控制台程序为例,我们要做的步骤是:

  • 创建一个Solution,比如叫Tutorials
    • 在Solution下创建一个Project, 比如叫Tutorial0-HelloWorld
      • 设置为C++项目,指定C++版本,比如C++ 17
      • 设置成控制台应用程序,或者新的Visual Studio直接叫应用程序
      • 在这个Project的某个筛选器下(通常是Source),添加或者新建main.cpp代码文件
  • Build,运行

我们用Premake来实践一下上面的步骤:

  • 新建一个项目文件夹,比如叫awesome-premake
  • 从官网下载一个Windows版本的premake5.exe,丢在根目录
  • 新建一个叫premake5.lua的脚本,编写premake脚本(后面再谈怎么写)
  • 新建一个叫source的文件夹,里面新建一个叫main.cpp的文件,写好hello world
  • 在根目录打开命令行执行./premake5.exe vs2022,生产了.sln文件
  • 打开生成好的sln, Build,运行

目录结构是:

  • awesome-premake
    • premake5.exe
    • premake5.lua
    • source
      • main.cpp
    • 将在此出现Tutorials.sln

premake5.lua的内容是:

1
2
3
4
5
6
7
8
9
10
11
12
workspace("Tutorials")
configurations { "Debug", "Release" }

project("Tutorial0-HelloWorld")
kind("ConsoleApp")
language("C++")
cppdialect("C++17")

targetdir("bin")
files {
"source/main.cpp"
}

简单解释一下,workspace对应于solution的概念,是多个projects的容器;configurations是项目build的配置类型;project就是项目描述,kind可以指定不同类型的工程,比如ConsoleApp就是控制台应用程序;language和cppdialect用于语言是C++,并且使用版本是C++ 17;targetdir是项目build生成的二进制文件存放目录;files用于添加代码文件到project中。

premake脚本是通过从上往下依次调用premake api来描述项目工程,具体api可以查文档:https://premake.github.io/docs/

在跑完premake命令,并且打开sln运行过后,我们的目录变成了:

  • awesome-premake
    • premake5.exe
    • premake5.lua
    • source
      • main.cpp
    • bin
      • Tutorial0-HelloWorld.exe
      • Tutorial0-HelloWorld.pdb
    • obj
      • Debug
        • main.obj
        • 其他临时文件
    • Tutorial0-HelloWorld.vcxproj
    • Tutorials.sln

至此,我们完成了一个最简单的基于premake的C++项目框架。这里仍然可以衍生一些新改进项:

  • 把命令行写到Windows下的bat,或者Mac下的cmd里,双击即可执行
  • 打开sln后,没发现有筛选器,如何添加进来?
  • bin和obj,还有工程文件全部暴露在根目录,怎么让他们换个位置更整洁?
  • Debug和Release两个不同配置怎么填写?比如我想让Release也能生成pdb,让Debug也能开启简单的优化

欢迎提问更多的如何改进premake工程,后面会以问题+解决方案的形式进行更新。

游戏引擎工程目录设计

以下是项目整体目录结构的当前设计,以Visual Stuido为例:

  • Root
    • Projects : 引擎功能用例
      • Project A : 用例A
        • Asset
          • Audio
          • Materials
          • Textures
          • Particles
          • Shaders
          • Models
          • Maps
        • Config
          • Engine.ini
          • Editor.ini
          • Game.ini
    • Engine
      • Auto :存放makefile,bat之类的自动构建工具和脚本
      • Binaries :生成的二进制文件,.dll, .exe
        • Mac
        • Win64
        • ThirdParty
          • bgfx
          • sdl2
      • Intermediate :生成的临时文件
        • ProjectFiles : 项目文件
        • Configs : 引擎配置,游戏设置等
        • Build : 临时编译文件,.obj等
          • Mac
          • Win64
          • ThirdParty
      • Source : 引擎源代码
        • Editor
        • Tools或者Programs
          • AudioLoader
            • libsndfile
          • ModelLoader
            • assimp
        • Runtime : 要求区分好private和public的api
          • Core - 核心层,放一些基础库
          • Asset - 资源管理
          • Audio - 3D音效
          • Graphics - 图形API,RHI层
          • Rendering - 渲染功能实现
          • Physics - 物理
          • GUI - UI界面
          • Window或者Application - 窗口,应用程序
        • ThirdParty
          • bgfx
            • bimg
            • bx
          • sdl2
          • openal-soft
    • Engine.sln

游戏引擎工程的Premake编写

上述的工程目录基本上写完了,形成博客还需要花很多时间,TBD

参考资料