Engine3
Etter alle disse årene med konsepter er det nå tid for nummer 3

Engine3
I artikkelen om utviklingsverktøyene kunne du lese hva jeg skal bruke for å lage spillet Signal From The Stars.
Siden Love2d er et rammeverk med bare noen få enkle grunnfunksjoner, fikk jeg idéen om først å lage en grunnmotor og så spillet oppå. Siden jeg allerede har laget slike konsepter flere ganger i livet, blir dette etter hvert den tredje motoren, derav navnet engine3.
Fordelen er at jeg eventuelt kan gi ut engine3 som open source, men også at jeg en dag kan lage et helt annet spill. Alt dette ligger i et eget git-repo som spillet skal bruke senere.
I starten skal jeg altså ikke jobbe konkret med spillet, men begynne med å skrive litt logikk. Selvfølgelig gjør jeg det med tanke på hva jeg faktisk skal bruke i spillet. Ja, alt jeg ikke trenger, tar jeg ikke med i rammeverket (less is more).
På filnivå ser strukturen til engine3 slik ut:

Grunnstruktur for engine3
Biblioteker, moduler og mods
Noen ting vil man lage selv, andre ikke. Grunnen er at det er svært kostbart å finne opp hjulet på nytt. Du bruker ekstra tid og må selv teste og vedlikeholde det.
Derfor finnes det en mappe lib i engine3. Der ligger noen biblioteker jeg ikke har laget selv, men som for eksempel er gitt ut under MIT-lisensen.
- Binser https://github.com/bakpakin/binser
- Dette bruker jeg til å lagre data, som lagrede spill.
- Json https://github.com/rxi/json.lua
- Brukes til å lese inn json-data (backup)
- Msgpack https://framagit.org/fperrad/lua-MessagePack
- Json er fint, men msgpack behandler filene mange ganger raskere
- Tick https://github.com/rxi/tick
- Med dette kan du kalle en funksjon på et bestemt tidspunkt, for eksempel etter 3 sekunder
- Vector https://github.com/themousery/vector.lua
- Alle objekter beveger seg med vektorer
Det finnes også en mappe lib-dev med moduler fra tredjeparter som bare trengs under utviklingen.
- ProFi https://gist.github.com/perky/2838755
- Med dette kan du se om du bruker for mye minne
Å skrive kode
Det er svært viktig å skrive koden konsekvent når det gjelder navngiving av klasser, funksjoner, variabler og filnavn. Men også når det gjelder kodeformatering og annotasjoner. Da forblir koden lesbar, og det er lettere å se senere hvor noe går galt. I tillegg kan andre forstå koden bedre. Til det bruker jeg https://luals.github.io/wiki/.
Navnet på programmet, lua language server, synes jeg personlig er litt forvirrende, men det gjør akkurat det jeg vil.
Etter installasjonen kan du skrive —linjer i koden som under.
---@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
Dette virker uviktig, men gjør at Visual Studio Code gir advarsler hvis jeg for eksempel gjør dette:
local something = endsWith({something = "a"}, "b") -- invalid str
En av ulempene med Lua sammenlignet med for eksempel GO er at Lua ikke har denne typen kontroll og derfor er feilutsatt.
Dokumentasjon
Ingen leser dokumentasjonen… Men jeg vet at den vil trengs på lengre sikt, særlig hvis motoren blir open source. Heldigvis har lua language server også en .md-generator som lager hele dokumentasjonen. Det gjør jeg med følgende shell-skript, create_code_doc.sh
#!/bin/bash
echo "Generate code documentation"
cd ../
lua-language-server --logpath=temp --doc=src/ --doc_out_path=codedoc
Ekstra kontroll
Tester kan være enormt i veien og koste tid, spesielt når du lager noe fra bunnen av. Du tester og endrer fortsatt ting hele tiden. Ordet refactor er nok det mest brukte ordet i git log.
Likevel bruker jeg enhetstester og integrasjonstester.

Grunnstruktur for engine3-tester
Shell-skriptet check_code.sh ser nå slik ut, og jeg kan starte det når jeg vil, for eksempel før hver git push eller senere i en pipeline.
#!/bin/bash
cd ../
luacheck --no-cache --config scripts/.luacheckrc src
echo "UNIT TEST"
./unittest.sh
echo "INTEGRATION TEST"
./integrationtest.sh
Kodekontroll
Med programmet https://github.com/mpeterv/luacheck kan du sjekke ulike punkter (eller nettopp la være).
Enhetstest
Til dette bruker jeg https://github.com/bluebird75/luaunit, og hver enhetstest har sin egen fil med tilhørende funksjoner.
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())
Integrasjonstest
Det var litt vanskeligere, men nyttig, å få dette til å fungere, for det må starte love2d og gjøre noe med input og output. Også her bruker jeg luaunit, men som en main.lua-oppstart av love2d, som under
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
Bygg, bygg, bygg
Der jeg bor, er det stor boligmangel, og slagordet du ofte hører, er:
Bygg, bygg, bygg
Engine3 må selvfølgelig også bygge spillet, men heldigvis trenger jeg bare å gjøre det når spillet er ferdig, eller når jeg vil teste spillet på en annen plattform.
Shell-skriptet jeg har laget for dette akkurat nå, heter build.sh og vil bli justert senere.
#!/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
Til slutt
Som du ser, må du først sørge for at grunnmuren er på plass. Det tar litt tid, men senere får du belønningen. Før hadde jeg for eksempel ingen tester eller syntakssjekker. I starten går det fint, men du kommer til et punkt der du har skrevet så mye kode at alt blir uoversiktlig.
Du må gå ut fra at noen andre skal se på koden og kunne forstå den. Eller at du selv fortsatt forstår den om 3 eller 10 år.