Logo Signal From The Stars

Engine3

Na al die jaren van concepten is het nu tijd voor nummer 3

Martin avatar
  • Martin
  • 7 min read
Engine nummer 3...

Engine3

In het artikel ontwikkeltools heb je kunnen lezen wat ik allemaal ga gebruiken om het spel Signal From The Stars te maken.

Aangezien Love2d een framework is met enkel wat eenvoudige basis functies, kreeg ik het idee om eerst een basis engine te maken en daar bovenop het spel. Aangezien ik in mijn leven al vaker van dit soort concepten heb gemaakt zal dit ondertussen de derde engine gaan worden vandaar de naam engine3.

Het voordeel hiervan is dat ik eventueel engine3 opensource kan uitbrengen maar ook om bijv. een totaal ander spel ooit te kunnen maken. Dit is staat allemaal in een aparte git repo, die het spel later gaat gebruiken.

Ik ga de eerste tijd dus nog niet concreet aan het spel maken maar begin met wat logica te schrijven. Natuurlijk doe ik dit met in het achterhoofd wat ik daadwerklijk ga straks ga gebruiken in het spel. Sterker nog, alles wat ik niet nodig ga hebben ga ik niet meenemen in het framework (less is more).

De structuur van engine3 zit er op bestandsniveau zo uit:

Basis structuur engine3

Basis structuur engine3

Libraries / Modules / Mods

Sommige dingen wil je zelf maken andere dingen weer niet, de rede hiervoor is dat wanneer je het wiel opnieuw gaat uitvinden dit zeer kostbaar is. Je bent dan extra tijd kwijt, je moet het zelf gaan testen en onderhouden.

Vandaar dat er in de engine3 een map lib staat. Hierin staan een aantal libraries die ik niet zelf gemaakt heb, maar die bijv. onder de MIT licentie zijn uitgegeven.

Er is ook een lib-dev map, hierin staan modules van derde die alleen voor het ontwikkelen nodig zijn.

Code schrijven

Het is zeer belangrijk om de code consequent te schrijven in naamgeving van de classes, functies, variables, bestandsnamen. Maar ook het gebruik van code formatting en annotations. Hierdoor blijft de code leesbaar en kan je later makkelijker zien waar eventueel iets fout gaat. Daarnaast kunnen andere dan ook de code beter begrijpen. Hiervoor gebruik ik https://luals.github.io/wiki/.

De naamgeving van dat programma vindt ik persoonlijk een beetje verwarrend lua language server maar het doet precies wat ik wil. Na installatie kun je in de code — regels schrijven zoals hieronder.

---@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

Dit lijkt onbelangrijk, maar zorgt ervoor dat Visual Studio Code warnings geeft indien ik nu bijv. zit doe:

local something = endsWith({something = "a"}, "b") -- invalid str

Een van de nadelen van Lua ten opzichte van bijv. GO is dat Lua dergelijk controle niet heeft en daardoor foutgevoelig is.

Docs

Niemand leest de documentatie… Alleen weet ik dat dit wel nodig gaat zijn op langer termijn en helemaal indien de engine opensource zou worden. In de lua language server zit gelukkig ook een .md generator die de complete documentatie maakt, dit doe ik middels het volgende shell script create_code_doc.sh

#!/bin/bash

echo "Generate code documentation"

cd ../
lua-language-server --logpath=temp --doc=src/ --doc_out_path=codedoc

Extra controle

Tests kunnen enorm in de weg zitten en tijd kosten, vooral wanneer je vanaf niets iets gaat maken. Je bent dan nog constant dingen aan het testen en aan het veranderen. Het woord refactor is denk ik het meest gebruikte woord in git log.

Deondanks gebruik ik toch unit test, en integration tests.

Basis structuur engine3 test

Basis structuur engine3 tests

Het shell script check_code.sh ziet er nu zo uit, en kan ik starten wanneer ik wil of bijv. voor elke git push of later in een pipeline.

#!/bin/bash

cd ../
luacheck --no-cache --config scripts/.luacheckrc src

echo "UNIT TEST"
./unittest.sh

echo "INTEGRATION TEST"
./integrationtest.sh

Code controle

Met het programma https://github.com/mpeterv/luacheck kan je op verschillende punten controleren (of juist niet).

Unit test

Hiervoor gebruik ik https://github.com/bluebird75/luaunit en elke unittest heeft zijn eigen bestand en daarin de bijbehoorden functies.

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

Het was wat lastiger maar wel handig om dit werkend te krijgen, dit moet namelijk love2d starten en iets met de input/output doen. Ook hiervoor gebruik ik luaunit maar is het een main.lua love2d start zoals hieronder

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

Bouwen bouwen bouwen

Waar ik woon is er een groot te kort aan huizen, de kreet die je dan ook vaak hoort is:

Bouwen bouwen bouwen

Engine3 moet natuurlijk ook het spel bouwen, alleen hoef ik dat gelukkig alleen maar te doen wanneer het spel af is, of wanneer ik het spel op een andere platform wil testen.

Het shell script wat ik hiervoor heb gemaakt op dit moment is build.sh en zal later worden aangepast.

#!/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

Tot slot

Zoals je ziet moet je eerst zorgen dat je de basis goed hebt staan, voordat je dat hebt ben je wel even bezig en zul je later de beloning krijgen. Ooit had ik bijv. geen tests of syntax checks en in het begin gaat dat nog goed, alleen kom je op een punt dat je zo veel code hebt geschreven het allemaal ondoorzichtig wordt.

Jet moet er vanuit gaan dat iemand anders de code gaat bekijken en kan begrijpen. Of dat je het zelf nog over 3 of 10 jaar begrijpt.