Logo Signal From The Stars

Engine3

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

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

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

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.

There is also a lib-dev folder, this contains third party modules that are only needed for development.

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 test

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

βœ‰οΈ Stay informed!

Receive the latest news for free and motivate me to make this adventure a success!