Engine3
Después de tantos años de prototipos, ahora toca el número 3

Engine3
En el artículo sobre las herramientas de desarrollo pudiste leer todo lo que voy a usar para hacer el juego Signal From The Stars.
Como Love2d es un framework con solo unas pocas funciones básicas sencillas, se me ocurrió hacer primero un motor base y encima el juego. Como a lo largo de mi vida ya he hecho varios conceptos así, este va a ser ya el tercer motor, de ahí el nombre engine3.
La ventaja es que quizá pueda publicar engine3 como open source, pero también hacer algún día un juego totalmente distinto. Todo esto está en un repositorio git aparte, que el juego usará más adelante.
Así que al principio todavía no voy a trabajar de verdad en el juego, sino que empiezo escribiendo algo de lógica. Claro que lo hago pensando en lo que de verdad voy a usar en el juego. Es más, todo lo que no vaya a necesitar no lo meto en el framework (less is more).
A nivel de archivos, la estructura de engine3 es así:

Estructura básica de engine3
Librerías, módulos y mods
Algunas cosas las quieres hacer tú y otras no. El motivo es que reinventar la rueda sale muy caro. Pierdes tiempo extra y tienes que probarlo y mantenerlo tú.
Por eso en engine3 hay una carpeta lib. En ella hay algunas librerías que no he hecho yo, pero que están publicadas, por ejemplo, con licencia MIT.
- Binser https://github.com/bakpakin/binser
- Lo uso para guardar datos, como las partidas guardadas.
- Json https://github.com/rxi/json.lua
- Se usa para leer datos json (copia de seguridad)
- Msgpack https://framagit.org/fperrad/lua-MessagePack
- Json está bien, pero msgpack procesa los archivos muchísimo más rápido
- Tick https://github.com/rxi/tick
- Con esto puedes llamar a una función en un momento dado, por ejemplo tras 3 segundos
- Vector https://github.com/themousery/vector.lua
- Todos los objetos se mueven con vectores
También hay una carpeta lib-dev, con módulos de terceros que solo se necesitan durante el desarrollo.
- ProFi https://gist.github.com/perky/2838755
- Con esto puedes ver si estás usando demasiada memoria
Escribir código
Es muy importante escribir el código de forma coherente en los nombres de clases, funciones, variables y archivos. Pero también en el formato del código y las anotaciones. Así el código sigue siendo legible y después es más fácil ver dónde falla algo. Además, otras personas también entienden mejor el código. Para eso uso https://luals.github.io/wiki/.
Personalmente el nombre de ese programa, lua language server, me parece un poco confuso, pero hace exactamente lo que quiero.
Tras instalarlo puedes escribir líneas con — en el código como aquí abajo.
---@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
Parece poco importante, pero hace que Visual Studio Code me avise si, por ejemplo, hago esto:
local something = endsWith({something = "a"}, "b") -- invalid str
Una de las desventajas de Lua frente, por ejemplo, a GO es que Lua no tiene este tipo de control y por eso es más propenso a errores.
Documentación
Nadie lee la documentación… Pero sé que a largo plazo va a hacer falta, sobre todo si el motor pasara a ser open source. Por suerte el lua language server también tiene un generador de .md que crea toda la documentación. Lo hago con el siguiente script de shell, create_code_doc.sh
#!/bin/bash
echo "Generate code documentation"
cd ../
lua-language-server --logpath=temp --doc=src/ --doc_out_path=codedoc
Control extra
Los tests pueden estorbar muchísimo y costar tiempo, sobre todo cuando empiezas algo desde cero. Todavía estás probando y cambiando cosas todo el rato. Creo que refactor es la palabra más usada en el git log.
Aun así uso unit tests e integration tests.

Estructura básica de los tests de engine3
El script de shell check_code.sh ahora es así, y puedo lanzarlo cuando quiera, por ejemplo antes de cada git push o más adelante en un pipeline.
#!/bin/bash
cd ../
luacheck --no-cache --config scripts/.luacheckrc src
echo "UNIT TEST"
./unittest.sh
echo "INTEGRATION TEST"
./integrationtest.sh
Control de código
Con el programa https://github.com/mpeterv/luacheck puedes comprobar (o no) distintos puntos.
Unit test
Para esto uso https://github.com/bluebird75/luaunit y cada unit test tiene su propio archivo con sus funciones correspondientes.
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
Fue algo más complicado, pero útil, hacer que funcionara, porque tiene que arrancar love2d y hacer algo con la entrada y la salida. Aquí también uso luaunit, pero como un main.lua de arranque de love2d, como abajo
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
Construir, construir, construir
Donde vivo hay una gran escasez de viviendas, y el lema que oyes a menudo es:
Construir, construir, construir
Engine3 también tiene que construir el juego, claro, pero por suerte solo tengo que hacerlo cuando el juego esté terminado o cuando quiera probarlo en otra plataforma.
El script de shell que he hecho para esto por ahora es build.sh, y lo cambiaré más adelante.
#!/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
Para terminar
Como ves, primero tienes que asegurarte de tener una buena base. Hasta llegar ahí tardas un tiempo, pero después llega la recompensa. En su día, por ejemplo, no tenía tests ni comprobación de sintaxis, y al principio todavía va bien, pero llegas a un punto en el que has escrito tanto código que todo se vuelve confuso.
Tienes que partir de que otra persona va a mirar el código y tiene que poder entenderlo. O de que tú mismo lo sigas entendiendo dentro de 3 o 10 años.