Engine3
Dopo tutti quegli anni di prototipi, ora è il momento del numero 3

Engine3
Nell’articolo sugli strumenti di sviluppo hai potuto leggere tutto ciò che userò per creare il gioco Signal From The Stars.
Visto che Love2d è un framework con solo alcune funzioni di base semplici, mi è venuta l’idea di creare prima un motore di base e poi il gioco sopra di esso. Dato che nella mia vita ho già creato più volte cose del genere, questo sarà ormai il terzo motore, da qui il nome engine3.
Il vantaggio è che potrei pubblicare engine3 come open source, ma anche un giorno creare un gioco completamente diverso. Tutto questo è in un repository git separato, che il gioco userà più avanti.
All’inizio quindi non lavorerò ancora davvero al gioco, ma comincerò a scrivere un po’ di logica. Ovviamente lo faccio pensando a ciò che userò davvero nel gioco. Anzi, tutto ciò che non mi servirà non lo metterò nel framework (less is more).
A livello di file, la struttura di engine3 è così:

Struttura di base di engine3
Librerie, moduli e mod
Alcune cose vuoi farle da solo, altre no. Il motivo è che reinventare la ruota costa moltissimo. Perdi tempo in più e devi testarla e mantenerla da solo.
Per questo in engine3 c’è una cartella lib. Contiene alcune librerie che non ho creato io, ma che sono pubblicate per esempio con licenza MIT.
- Binser https://github.com/bakpakin/binser
- Lo uso per salvare dati, come i salvataggi.
- Json https://github.com/rxi/json.lua
- Usato per leggere dati json (backup)
- Msgpack https://framagit.org/fperrad/lua-MessagePack
- Json va bene, ma msgpack elabora i file molte volte più velocemente
- Tick https://github.com/rxi/tick
- Con questo puoi chiamare una funzione a un orario impostato, per esempio dopo 3 secondi
- Vector https://github.com/themousery/vector.lua
- Tutti gli oggetti si muovono con i vettori
C’è anche una cartella lib-dev, con moduli di terzi che servono solo durante lo sviluppo.
- ProFi https://gist.github.com/perky/2838755
- Con questo puoi vedere se stai usando troppa memoria
Scrivere codice
È molto importante scrivere il codice in modo coerente nei nomi di classi, funzioni, variabili e file. Ma anche nell’uso della formattazione e delle annotazioni. Così il codice resta leggibile e dopo è più facile capire dove qualcosa va storto. Inoltre anche altri possono capire meglio il codice. Per questo uso https://luals.github.io/wiki/.
Personalmente trovo il nome di quel programma un po’ confuso, lua language server, ma fa esattamente ciò che voglio.
Dopo l’installazione puoi scrivere nel codice righe con — come qui sotto.
---@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
Sembra poco importante, ma fa sì che Visual Studio Code mi dia degli avvisi se per esempio faccio così:
local something = endsWith({something = "a"}, "b") -- invalid str
Uno degli svantaggi di Lua rispetto per esempio a GO è che Lua non ha questo tipo di controllo ed è quindi soggetto a errori.
Documentazione
Nessuno legge la documentazione… Però so che a lungo termine servirà, soprattutto se il motore diventasse open source. Per fortuna nel lua language server c’è anche un generatore .md che crea tutta la documentazione. Lo faccio con il seguente script shell, create_code_doc.sh
#!/bin/bash
echo "Generate code documentation"
cd ../
lua-language-server --logpath=temp --doc=src/ --doc_out_path=codedoc
Controlli extra
I test possono essere un enorme intralcio e costare tempo, soprattutto quando crei qualcosa da zero. Stai ancora continuamente provando e cambiando cose. Credo che la parola refactor sia la più usata nel git log.
Nonostante questo uso comunque unit test e integration test.

Struttura di base dei test di engine3
Lo script shell check_code.sh ora è così, e posso avviarlo quando voglio, per esempio prima di ogni git push o più avanti in una pipeline.
#!/bin/bash
cd ../
luacheck --no-cache --config scripts/.luacheckrc src
echo "UNIT TEST"
./unittest.sh
echo "INTEGRATION TEST"
./integrationtest.sh
Controllo del codice
Con il programma https://github.com/mpeterv/luacheck puoi controllare (o proprio non controllare) diversi aspetti.
Unit test
Per questo uso https://github.com/bluebird75/luaunit e ogni unit test ha il suo file con le funzioni corrispondenti.
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())
Integration test
È stato un po’ più difficile, ma utile, farlo funzionare, perché deve avviare love2d e fare qualcosa con input e output. Anche qui uso luaunit, ma come main.lua di avvio di love2d, come qui sotto
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
Costruire, costruire, costruire
Dove vivo c’è una grande carenza di case, e lo slogan che senti spesso è:
Costruire, costruire, costruire
Ovviamente engine3 deve anche fare la build del gioco, ma per fortuna devo farlo solo quando il gioco è finito o quando voglio provarlo su un’altra piattaforma.
Lo script shell che ho creato per questo al momento è build.sh, e verrà modificato più avanti.
#!/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
Per finire
Come vedi, prima devi assicurarti che le basi siano solide. Prima di arrivarci ci vuole un po’, ma dopo arriva la ricompensa. Un tempo per esempio non avevo test né controlli di sintassi, e all’inizio va ancora bene, ma arrivi a un punto in cui hai scritto così tanto codice che tutto diventa poco chiaro.
Devi partire dal presupposto che qualcun altro guarderà il codice e dovrà capirlo. O che tu stesso lo capisca ancora tra 3 o 10 anni.