Engine3

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

Motor 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

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.

Det finnes også en mappe lib-dev med moduler fra tredjeparter som bare trengs under utviklingen.

Å 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

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.