自制游戏引擎 - 工程构建
作为一个跨平台的复杂项目,我们首先要支持在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出来的一个分支版本
- Github链接 https://github.com/bkaradzic/GENie 的介绍里有一个GDC 2016的宣传视频
- xmake
- 借鉴premake的思路,又额外做了一些新功能
- 国内做的make工具,也支持lua,暂时没试过
- bazel
- 谷歌内部开源出来的make工具,目前很多开源库都开始转向bazel,暂时没试过
- scons
- 使用方便,支持用python编写makefile,开源游戏引擎Godot使用了scons
- ninja
- 国内云风团队选择的make工具 : https://blog.codingnow.com/2021/05/make_to_ninja.html
- 他们很奇怪,因为也不喜欢写ninja脚本,喜欢lua,所以又做了一个luamake转ninja脚本的工具,我觉得这带来不必要的麻烦
- 国内云风团队选择的make工具 : https://blog.codingnow.com/2021/05/make_to_ninja.html
- autoconf
- …
- make
从这些工具里挑选,我倾向于使用非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代码文件
- 在Solution下创建一个Project, 比如叫Tutorial0-HelloWorld
- 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 | workspace("Tutorials") |
简单解释一下,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
- 其他临时文件
- Debug
- 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
- …
- Asset
- Project A : 用例A
- 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
- AudioLoader
- Runtime : 要求区分好private和public的api
- Core - 核心层,放一些基础库
- Asset - 资源管理
- Audio - 3D音效
- Graphics - 图形API,RHI层
- Rendering - 渲染功能实现
- Physics - 物理
- GUI - UI界面
- Window或者Application - 窗口,应用程序
- ThirdParty
- bgfx
- bimg
- bx
- sdl2
- openal-soft
- bgfx
- Engine.sln
- Projects : 引擎功能用例
游戏引擎工程的Premake编写
上述的工程目录基本上写完了,形成博客还需要花很多时间,TBD
参考资料
- 更多了解Premake的资料 - https://premake.github.io/community/support
- 遇到不支持的功能,改进源代码! - https://github.com/premake/premake-core