Engine3
经过这么多年的概念尝试,现在轮到第3号了

Engine3
在关于开发工具的文章里,你已经读到了我会用哪些东西来制作游戏 Signal From The Stars。
由于 Love2d 是一个只提供一些简单基础函数的框架,我就想先做一个基础引擎,再在上面做游戏。因为我这辈子已经做过好几次这样的概念,这将是第三个引擎,所以叫 engine3。
这样做的好处是,我以后也许可以把 engine3 开源,也可以有一天用它做一款完全不同的游戏。这些都放在一个单独的 git 仓库里,游戏之后会用到它。
所以一开始我还不会具体做游戏,而是先写一些逻辑。当然,我是想着游戏里真正会用到什么来写的。更进一步,凡是我用不到的东西,都不会放进框架里(less is more)。
在文件层面,engine3 的结构如下:

engine3 的基本结构
库、模块与 Mod
有些东西你想自己做,有些则不想。原因是重复造轮子代价非常高。你要多花时间,还得自己测试和维护。
所以 engine3 里有一个 lib 文件夹。里面有一些不是我自己写的库,它们比如是以 MIT 许可证发布的。
- Binser https://github.com/bakpakin/binser
- 我用它来保存数据,比如存档。
- Json https://github.com/rxi/json.lua
- 用于读取 json 数据(备用)
- Msgpack https://framagit.org/fperrad/lua-MessagePack
- json 很好,但 msgpack 处理文件要快很多倍
- Tick https://github.com/rxi/tick
- 可以在设定的时间调用函数,比如3秒后
- Vector https://github.com/themousery/vector.lua
- 所有物体都用向量来移动
还有一个 lib-dev 文件夹,里面是只在开发时需要的第三方模块。
- ProFi https://gist.github.com/perky/2838755
- 可以查看你有没有占用太多内存
写代码
在类、函数、变量和文件的命名上保持一致非常重要,代码格式和注解的使用也一样。这样代码才能保持可读,以后也更容易看出哪里出了问题。别人也能更好地理解代码。为此我使用 https://luals.github.io/wiki/。
我个人觉得这个程序的名字 lua language server 有点让人摸不着头脑,但它做的正是我想要的。
安装之后,你就可以在代码里写 — 开头的行,如下所示。
---@param str string The string to check
---@param ending string The chararacter to check
local function endsWith(str, ending)
return ending == "" or str:sub(-(#ending)) == ending
end
这看起来不重要,但如果我写了比如下面这样的代码,Visual Studio Code 就会给出警告:
local something = endsWith({something = "a"}, "b") -- invalid str
和 GO 之类的语言相比,Lua 的一个缺点是没有这类检查,因此容易出错。
文档
没人读文档… 但我知道从长远来看它是必要的,尤其是引擎要开源的话。幸好 lua language server 里也有一个 .md 生成器,可以生成完整的文档。我用下面这个 shell 脚本 create_code_doc.sh 来做这件事
#!/bin/bash
echo "Generate code documentation"
cd ../
lua-language-server --logpath=temp --doc=src/ --doc_out_path=codedoc
额外检查
测试可能会非常碍事,也很花时间,尤其是从零开始做东西的时候。 你还在不停地试验和修改。我想 refactor 大概是 git log 里出现最多的词。
尽管如此,我还是使用了单元测试和集成测试。

engine3 测试的基本结构
shell 脚本 check_code.sh 现在是这样的,我可以随时运行它,比如每次 git push 之前,或者以后放进流水线里。
#!/bin/bash
cd ../
luacheck --no-cache --config scripts/.luacheckrc src
echo "UNIT TEST"
./unittest.sh
echo "INTEGRATION TEST"
./integrationtest.sh
代码检查
用 https://github.com/mpeterv/luacheck 这个程序,你可以检查(或者偏偏不检查)各种方面。
单元测试
为此我使用 https://github.com/bluebird75/luaunit,每个单元测试都有自己的文件,里面是对应的函数。
test-order.lua
local lu = require("luaunit")
TestClass = {}
local numKeyTable = {
[1] = {name = "a", order = 1},
[2] = {name = "b", order = 2},
[3] = {name = "d", order = 3}
}
function TestClass:test1Sort()
table.sort(
numKeyTable,
function(a, b)
return a.order < b.order
end
)
lu.assertEquals(
numKeyTable,
{
[1] = {name = "a", order = 1},
[2] = {name = "b", order = 2},
[3] = {name = "d", order = 3}
}
)
end
os.exit(lu.LuaUnit.run())
集成测试
让它跑起来要难一些,但很有用,因为它需要启动 love2d,并对输入输出做一些处理。这里我也用 luaunit,只不过是作为启动 love2d 的 main.lua,如下所示
local lu = require("luaunit")
local runner = lu.LuaUnit.new()
function love.load()
runner:runSuiteByInstances(
{
{"rect-helper", require "rect-helper"},
{"image-assets", require "image-assets"},
{"audio-assets", require "audio-assets"},
{"entity-manager", require "entity-manager"},
{"callback", require "callback"},
{"frameset", require "frameset"}
}
)
love.event.push("quit", 0)
end
建,建,建
我住的地方住房严重短缺,你常听到的口号就是:
建,建,建
engine3 当然也得负责构建游戏,不过幸好只有在游戏完成,或者我想在其他平台上测试时才需要这样做。
我目前为此写的 shell 脚本是 build.sh,以后还会调整。
#!/bin/bash
source config
osx_CFBundleName=${APP_NAME}
osx_CFBundleIdentifier="com.SuperCompany."${APP_NAME}
ios_CFBundleVersion="11.3"
src_dir="../src/"
osx_build_with=${ENGINE3_SRC_DIR}"/scripts/osx-love-build-source/love.app"
win64_build_with=${ENGINE3_SRC_DIR}"/scripts/win-love-build-source/love-win64/"
win32_build_with=${ENGINE3_SRC_DIR}"/scripts/win-love-build-source/love-win32/"
build_dir="../build/"
build_love_file=${build_dir}${APP_NAME}".love"
build_temp_dir=${build_dir}"temp/"
build_osx_dir=${build_dir}"osx/"
build_win64_dir=${build_dir}"win64/"
build_win32_dir=${build_dir}"win32/"
build_linux_dir=${build_dir}"linux/"
current_script_dir=$(pwd)
echo "[1/7] Remove old build ${build_dir}"
rm -Rf ${build_dir}
mkdir -p ${build_dir}
mkdir -p ${build_temp_dir}
echo "[2/7] Copy only the files that we actual need"
cd ${src_dir}
# We don't want/need all files in the actuale build, for the end-user.
rsync --relative -rpKL \
--exclude=lib/engine3/archive \
--exclude=lib/engine3/codedoc \
--exclude=lib/engine3/docs \
--exclude=lib/engine3/scripts \
--exclude=lib/engine3/temp \
--exclude=lib/engine3/README.MD \
--exclude=lib/engine3/src/framework/helper/dump.lua \
--exclude=lib/engine3/src/lib-dev \
--exclude=lib/engine3/src/test \
--exclude=assets/*/*/*.json \
--exclude=assets/*/*.json \
--exclude=unittest \
--exclude=*.DS_Store* \
--exclude=*.gitignore* \
* \
${build_temp_dir}
# some modules are exclude, check for example if the dump( function is in use
check1=$(grep -rnw ${build_temp_dir} -e 'dump(')
if [ ! -z "$check1" ] ; then
echo "dump() function found, remove it in the source"
echo ${check1}
exit 3
fi
echo "[3/7] Switch to the temp directory"
cd ${build_temp_dir}
echo "[4/7] Create love file: ${build_dir}${APP_NAME}.love"
zip -9 -r --quiet ${APP_NAME}".love" .
cd ${current_script_dir}
mv ${build_temp_dir}${APP_NAME}".love" ${build_love_file}
echo "[5/7] Create osx executable: ${build_osx_dir}"
mkdir -p ${build_osx_dir}
cp -R ${osx_build_with} ${build_osx_dir}
mv ${build_osx_dir}"love.app" ${build_osx_dir}${APP_NAME}".app"
cp ${build_love_file} ${build_osx_dir}${APP_NAME}".app/Contents/Resources"
plutil -replace CFBundleName -string ${osx_CFBundleName} ${build_osx_dir}${APP_NAME}".app/Contents/Info.plist"
plutil -replace CFBundleIdentifier -string ${osx_CFBundleIdentifier} ${build_osx_dir}${APP_NAME}".app/Contents/Info.plist"
plutil -remove UTExportedTypeDeclarations ${build_osx_dir}${APP_NAME}".app/Contents/Info.plist"
echo "[6/7] Create win 64 executable: ${build_win64_dir}"
mkdir -p ${build_win64_dir}
cp -R ${win64_build_with} ${build_win64_dir}
cat ${build_win64_dir}"love.exe" ${build_love_file} > ${build_win64_dir}${APP_NAME}".exe"
echo "[7/7] Create win 32 executable: ${build_win32_dir}"
mkdir -p ${build_win32_dir}
cp -R ${win32_build_with} ${build_win32_dir}
cat ${build_win32_dir}"love.exe" ${build_love_file} > ${build_win32_dir}${APP_NAME}".exe"
# don't need this file anymore
#rm ${build_love_file}
# comment this line below, to see/debug what files are used for the actual build.
rm -rf ${build_temp_dir}
exit 0
最后
如你所见,你必须先把基础打好。在那之前需要花上一段时间,但之后就会得到回报。以前我比如没有测试,也没有语法检查。一开始还没问题,但总会到一个地步,你写的代码多到一切都变得混乱不清。
你要假设会有别人来看这些代码,并且要能看懂。或者3年、10年后的你自己还能看懂。