Engine3
После стольких лет концептов пришло время номера 3

Engine3
В статье об инструментах разработки вы могли прочитать, что я буду использовать, чтобы сделать игру Signal From The Stars.
Поскольку Love2d это фреймворк лишь с несколькими простыми базовыми функциями, у меня появилась идея сначала сделать базовый движок, а поверх него игру. Так как подобные концепты я в жизни делал уже не раз, это будет уже третий движок, отсюда и название engine3.
Плюс в том, что я, возможно, смогу выпустить engine3 как open source, а когда-нибудь сделать на нём совсем другую игру. Всё это лежит в отдельном git-репозитории, который игра потом будет использовать.
Так что поначалу я ещё не буду конкретно заниматься игрой, а начну с написания логики. Конечно, я делаю это с оглядкой на то, что действительно буду использовать в игре. Более того, всё, что мне не понадобится, в фреймворк не попадёт (less is more).
На уровне файлов структура engine3 выглядит так:

Базовая структура engine3
Библиотеки, модули и моды
Одни вещи хочется сделать самому, другие нет. Причина в том, что изобретать велосипед заново очень дорого. Тратишь лишнее время, и всё приходится тестировать и поддерживать самому.
Поэтому в 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
Один из недостатков Lua по сравнению, например, с GO в том, что в Lua нет такой проверки, поэтому легко ошибиться.
Документация
Никто не читает документацию… Но я знаю, что в долгосрочной перспективе она понадобится, особенно если движок станет open source. К счастью, в 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.
И всё же я использую unit тесты и интеграционные тесты.

Базовая структура тестов engine3
Shell скрипт check_code.sh теперь выглядит так, и я могу запускать его когда угодно, например перед каждым git push или позже в pipeline.
#!/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 можно проверять разные вещи (или как раз не проверять).
Unit тест
Для этого я использую https://github.com/bluebird75/luaunit, и у каждого unit теста свой файл с соответствующими функциями.
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, но в виде main.lua для запуска love2d, как ниже
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 лет.