Engine3
After all those years of concepts, it's time for number 3

- Martin
- 7 min read

Engine3
In the article development tools you could read what I’m going to use to make the game Signal From The Stars
.
Since Love2d is a framework with only some simple basic functions, I got the idea to first make a basic engine and then the game on top of that. Since I’ve made these kinds of concepts before in my life, this will become the third engine, hence the name engine3
.
The advantage of this is that I can eventually release engine3 open source, but also to be able to make a completely different game at some point. This is all in a separate git repo, which the game will use later.
So I’m not going to make the game concretely for the time being, but I’m going to start writing some logic. Of course, I’m doing this with in mind what I’m actually going to use in the game. In fact, I’m not going to include anything I don’t need in the framework (less is more).
The structure of engine3 at file level looks like this:

Basis structuur engine3
Libraries / Modules / Mods
Some things you want to make yourself, other things you don’t, the reason for this is that if you reinvent the wheel, it is very expensive. You then lose extra time, you have to test and maintain it yourself.
That is why there is a folder lib
in the engine3. This contains a number of libraries that I did not make myself, but that are released under the MIT license, for example.
I use this to store data, such as save games.
Used to read json data (backup)
Json is so nice, msgpack processes the json files much faster
This allows you to call a function at a set time, e.g. after 3 seconds
All objects move with vector
There is also a lib-dev
folder, this contains third party modules that are only needed for development.
- ProFi https://gist.github.com/perky/2838755
- This lets you see if you are using too much memory
Writing code
It is very important to write the code consistently in naming the classes, functions, variables, file names. But also the use of code formatting and annotations. This keeps the code readable and you can see later more easily where something might go wrong. In addition, others can understand the code better. For this I use https://luals.github.io/wiki/.
I personally find the naming of that program a bit confusing lua language server
but it does exactly what I want.
After installation you can write — lines in the code like below.
---@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
This may seem unimportant, but it causes Visual Studio Code to give warnings if I do something like:
local something = endsWith({something = "a"}, "b") -- invalid str
One of the disadvantages of Lua compared to e.g. GO is that Lua does not have such control and is therefore error-prone.
Docs
Nobody reads the documentation… I just know that this will be necessary in the long term and especially if the engine would become open source. Fortunately, the lua language server
also contains a .md generator that creates the complete documentation, I do this using the following 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 control
Tests can be a huge hindrance and time-consuming, especially when you’re starting from scratch. You’re constantly testing and changing things. I think the word refactor is the most used word in git log.
I still use unit test and integration tests, though.

Basis structuur engine3 tests
The shell script check_code.sh now looks like this, and I can start it whenever I want or e.g. before every git push or later in a pipeline.
#!/bin/bash
cd ../
luacheck --no-cache --config scripts/.luacheckrc src
echo "UNIT TEST"
./unittest.sh
echo "INTEGRATION TEST"
./integrationtest.sh
Code check
With the program https://github.com/mpeterv/luacheck you can check (or not) at various points.
Unit test
For this I use https://github.com/bluebird75/luaunit and each unit test has its own file and the associated functions.
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
It was a bit more difficult but useful to get this working, this has to start love2d and do something with the input/output. I also use luaunit for this but it is a main.lua love2d starts as below
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
Build build build
Where I live there is a big shortage of houses, the cry you often hear is:
Build build build
Engine3 also has to build the game, but luckily I only have to do that when the game is finished, or when I want to test the game on another platform.
The shell script I have made for this at the moment is build.sh and will be adjusted later.
#!/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
Finally
As you can see, you first have to make sure that you have the basics right, before you do that, it will take some time and you will get the reward later. For example, I once had no tests or syntax checks and in the beginning that went well, but you reach a point where you have written so much code that it all becomes opaque.
Jet has to assume that someone else will look at the code and can understand it. Or that you will understand it yourself in 3 or 10 years.
Loading external comment system...