Engine3
Après toutes ces années de prototypes, place au numéro 3

Engine3
Dans l’article sur les outils de développement, vous avez pu lire tout ce que je vais utiliser pour créer le jeu Signal From The Stars.
Comme Love2d est un framework avec seulement quelques fonctions de base simples, j’ai eu l’idée de créer d’abord un moteur de base, puis le jeu par-dessus. Comme j’ai déjà créé plusieurs concepts de ce genre dans ma vie, ce sera désormais le troisième moteur, d’où le nom engine3.
L’avantage, c’est que je pourrais éventuellement publier engine3 en open source, mais aussi créer un jour un jeu complètement différent. Tout cela se trouve dans un dépôt git séparé, que le jeu utilisera plus tard.
Au début, je ne vais donc pas encore vraiment travailler sur le jeu, mais commencer par écrire un peu de logique. Bien sûr, je le fais en pensant à ce que j’utiliserai réellement dans le jeu. Mieux encore, tout ce dont je n’aurai pas besoin, je ne le mets pas dans le framework (less is more).
Au niveau des fichiers, la structure d’engine3 ressemble à ceci :

Structure de base d’engine3
Bibliothèques, modules et mods
Certaines choses, on veut les faire soi-même, d’autres non. La raison, c’est que réinventer la roue coûte très cher. On perd du temps et il faut tout tester et maintenir soi-même.
C’est pourquoi engine3 contient un dossier lib. On y trouve quelques bibliothèques que je n’ai pas créées moi-même, mais qui sont publiées par exemple sous licence MIT.
- Binser https://github.com/bakpakin/binser
- Je l’utilise pour enregistrer des données, comme les sauvegardes.
- Json https://github.com/rxi/json.lua
- Utilisé pour lire des données json (sauvegarde de secours)
- Msgpack https://framagit.org/fperrad/lua-MessagePack
- Json, c’est bien, mais msgpack traite les fichiers bien plus vite
- Tick https://github.com/rxi/tick
- Permet d’appeler une fonction à un moment donné, par exemple après 3 secondes
- Vector https://github.com/themousery/vector.lua
- Tous les objets se déplacent avec des vecteurs
Il y a aussi un dossier lib-dev, avec des modules tiers qui ne servent que pendant le développement.
- ProFi https://gist.github.com/perky/2838755
- Permet de voir si on n’utilise pas trop de mémoire
Écrire du code
Il est très important d’écrire le code de façon cohérente dans le nommage des classes, fonctions, variables et fichiers. Mais aussi dans la mise en forme et les annotations. Le code reste ainsi lisible, et il est plus facile de voir ensuite où quelque chose ne va pas. D’autres personnes peuvent aussi mieux comprendre le code. Pour cela, j’utilise https://luals.github.io/wiki/.
Personnellement, je trouve le nom de ce programme, lua language server, un peu déroutant, mais il fait exactement ce que je veux.
Après l’installation, on peut écrire des lignes — dans le code, comme ci-dessous.
---@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
Ça paraît sans importance, mais Visual Studio Code m’affiche alors des avertissements si, par exemple, je fais ceci :
local something = endsWith({something = "a"}, "b") -- invalid str
L’un des inconvénients de Lua par rapport, par exemple, à GO, c’est que Lua n’a pas ce genre de contrôle et est donc plus sujet aux erreurs.
Documentation
Personne ne lit la documentation… Mais je sais qu’elle sera nécessaire à long terme, surtout si le moteur devenait open source. Heureusement, le lua language server contient aussi un générateur .md qui crée toute la documentation. Je le fais avec le script shell suivant, create_code_doc.sh
#!/bin/bash
echo "Generate code documentation"
cd ../
lua-language-server --logpath=temp --doc=src/ --doc_out_path=codedoc
Contrôles supplémentaires
Les tests peuvent être très gênants et prendre du temps, surtout quand on part de rien. On est encore en train de tester et de changer des choses en permanence. Je pense que refactor est le mot le plus utilisé dans le git log.
Malgré tout, j’utilise des tests unitaires et des tests d’intégration.

Structure de base des tests d’engine3
Le script shell check_code.sh ressemble maintenant à ceci, et je peux le lancer quand je veux, par exemple avant chaque git push ou plus tard dans un pipeline.
#!/bin/bash
cd ../
luacheck --no-cache --config scripts/.luacheckrc src
echo "UNIT TEST"
./unittest.sh
echo "INTEGRATION TEST"
./integrationtest.sh
Contrôle du code
Avec le programme https://github.com/mpeterv/luacheck, on peut contrôler (ou justement ne pas contrôler) différents points.
Test unitaire
J’utilise pour cela https://github.com/bluebird75/luaunit, et chaque test unitaire a son propre fichier avec les fonctions correspondantes.
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())
Test d’intégration
C’était un peu plus compliqué, mais utile, de le faire fonctionner, car il faut démarrer love2d et faire quelque chose avec les entrées et sorties. J’utilise aussi luaunit ici, mais sous forme de main.lua de démarrage love2d, comme ci-dessous
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
Construire, construire, construire
Là où je vis, il y a une grave pénurie de logements, et le slogan qu’on entend souvent est :
Construire, construire, construire
Engine3 doit bien sûr aussi construire le jeu, mais heureusement je n’ai à le faire que quand le jeu est terminé, ou quand je veux le tester sur une autre plateforme.
Le script shell que j’ai créé pour cela pour l’instant s’appelle build.sh, et il sera modifié plus tard.
#!/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
Pour finir
Comme vous le voyez, il faut d’abord s’assurer d’avoir de bonnes bases. Ça prend un peu de temps, mais la récompense vient ensuite. Autrefois, par exemple, je n’avais ni tests ni vérification de syntaxe. Au début ça va encore, mais on arrive à un point où l’on a écrit tellement de code que tout devient opaque.
Il faut partir du principe que quelqu’un d’autre va lire le code et doit pouvoir le comprendre. Ou que vous-même le compreniez encore dans 3 ou 10 ans.