Engine3
Efter alla år av koncept är det nu dags för nummer 3

Engine3
I artikeln om utvecklingsverktygen kunde du läsa vad jag ska använda för att göra spelet Signal From The Stars.
Eftersom Love2d är ett ramverk med bara några enkla grundfunktioner fick jag idén att först göra en basmotor och bygga spelet ovanpå den. Eftersom jag redan har gjort sådana koncept flera gånger i livet blir det här vid det här laget den tredje motorn, därav namnet engine3.
Fördelen är att jag eventuellt kan släppa engine3 som open source, men också att jag någon gång kan göra ett helt annat spel. Allt det här ligger i ett separat git-repo som spelet ska använda senare.
I början kommer jag alltså inte att jobba konkret med spelet, utan börja med att skriva lite logik. Självklart gör jag det med tanke på vad jag faktiskt kommer att använda i spelet. Dessutom tar jag inte med något i ramverket som jag inte behöver (less is more).
På filnivå ser strukturen i engine3 ut så här:

Grundstruktur för engine3
Bibliotek, moduler och moddar
Vissa saker vill man göra själv, andra inte. Anledningen är att det är väldigt dyrt att uppfinna hjulet på nytt. Man lägger extra tid och måste själv testa och underhålla det.
Därför finns det en mapp lib i engine3. Där ligger några bibliotek som jag inte har gjort själv, men som till exempel är släppta under MIT-licensen.
- Binser https://github.com/bakpakin/binser
- Det använder jag för att spara data, som sparfiler.
- Json https://github.com/rxi/json.lua
- Används för att läsa in json-data (backup)
- Msgpack https://framagit.org/fperrad/lua-MessagePack
- Json är bra, men msgpack bearbetar filerna mångdubbelt snabbare
- Tick https://github.com/rxi/tick
- Med det kan man anropa en funktion vid en bestämd tid, till exempel efter 3 sekunder
- Vector https://github.com/themousery/vector.lua
- Alla objekt rör sig med vektorer
Det finns också en mapp lib-dev med moduler från tredje part som bara behövs under utvecklingen.
- ProFi https://gist.github.com/perky/2838755
- Med det kan man se om man använder för mycket minne
Skriva kod
Det är mycket viktigt att skriva koden konsekvent när det gäller namn på klasser, funktioner, variabler och filnamn. Men också när det gäller kodformatering och annoteringar. Då förblir koden läsbar och det är lättare att se senare var något går fel. Dessutom kan andra förstå koden bättre. För det använder jag https://luals.github.io/wiki/.
Programmets namn, lua language server, tycker jag personligen är lite förvirrande, men det gör precis det jag vill.
Efter installationen kan man skriva —rader i koden som nedan.
---@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
Det verkar oviktigt, men gör att Visual Studio Code varnar om jag till exempel gör så här:
local something = endsWith({something = "a"}, "b") -- invalid str
En av nackdelarna med Lua jämfört med till exempel GO är att Lua inte har den typen av kontroll och därför är felbenäget.
Dokumentation
Ingen läser dokumentationen… Men jag vet att den kommer att behövas på lång sikt, särskilt om motorn blir open source. Som tur är finns det också en .md-generator i lua language server som skapar hela dokumentationen. Det gör jag med följande shellskript, create_code_doc.sh
#!/bin/bash
echo "Generate code documentation"
cd ../
lua-language-server --logpath=temp --doc=src/ --doc_out_path=codedoc
Extra kontroll
Tester kan vara väldigt i vägen och kosta tid, särskilt när man bygger något från grunden. Man testar och ändrar fortfarande saker hela tiden. Ordet refactor är nog det mest använda ordet i git log.
Trots det använder jag enhetstester och integrationstester.

Grundstruktur för engine3-tester
Shellskriptet check_code.sh ser nu ut så här, och jag kan köra det när jag vill, till exempel före varje git push eller senare i en pipeline.
#!/bin/bash
cd ../
luacheck --no-cache --config scripts/.luacheckrc src
echo "UNIT TEST"
./unittest.sh
echo "INTEGRATION TEST"
./integrationtest.sh
Kodkontroll
Med programmet https://github.com/mpeterv/luacheck kan man kontrollera olika saker (eller just låta bli).
Enhetstest
För det använder jag https://github.com/bluebird75/luaunit, och varje enhetstest har sin egen fil med tillhörande funktioner.
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())
Integrationstest
Det var lite svårare, men praktiskt, att få det att fungera, eftersom det måste starta love2d och göra något med input och output. Även här använder jag luaunit, men som en main.lua-start för love2d, som nedan
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
Bygga, bygga, bygga
Där jag bor är det stor bostadsbrist, och parollen man ofta hör är:
Bygga, bygga, bygga
Engine3 måste förstås också bygga spelet, men som tur är behöver jag bara göra det när spelet är klart eller när jag vill testa spelet på en annan plattform.
Shellskriptet jag har gjort för det just nu heter build.sh och kommer att ändras senare.
#!/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
Till sist
Som du ser måste man först se till att grunden är stabil. Innan dess är man upptagen ett tag, men senare får man belöningen. Förr hade jag till exempel inga tester eller syntaxkontroller. I början går det bra, men man når en punkt där man har skrivit så mycket kod att allt blir oöverskådligt.
Man måste utgå från att någon annan ska titta på koden och kunna förstå den. Eller att man själv fortfarande förstår den om 3 eller 10 år.