Engine3

Setelah bertahun-tahun membuat konsep, kini saatnya nomor 3

Engine nomor 3...

Engine3

Di artikel tentang alat pengembangan kamu sudah bisa membaca apa saja yang akan saya pakai untuk membuat game Signal From The Stars.

Karena Love2d adalah framework yang hanya punya beberapa fungsi dasar sederhana, saya mendapat ide untuk membuat engine dasar dulu, lalu game di atasnya. Karena dalam hidup saya sudah beberapa kali membuat konsep seperti ini, ini akan menjadi engine ketiga, karena itu namanya engine3.

Keuntungannya, suatu saat saya mungkin bisa merilis engine3 sebagai open source, tapi juga bisa membuat game yang sama sekali berbeda. Semua ini ada di repo git terpisah yang nanti akan dipakai oleh game.

Jadi di awal saya belum benar-benar mengerjakan game-nya, tapi mulai dengan menulis sedikit logika. Tentu saja saya melakukannya sambil memikirkan apa yang benar-benar akan saya pakai di game. Bahkan, semua yang tidak saya butuhkan tidak akan saya masukkan ke framework (less is more).

Di tingkat file, struktur engine3 seperti ini:

Struktur dasar engine3

Struktur dasar engine3

Library, modul, dan mod

Ada hal yang ingin kamu buat sendiri, ada yang tidak. Alasannya, menciptakan ulang roda itu sangat mahal. Kamu butuh waktu ekstra dan harus menguji serta merawatnya sendiri.

Karena itu di engine3 ada folder lib. Isinya beberapa library yang tidak saya buat sendiri, tapi dirilis misalnya dengan lisensi MIT.

Ada juga folder lib-dev berisi modul pihak ketiga yang hanya dibutuhkan selama pengembangan.

Sangat penting untuk menulis kode secara konsisten dalam penamaan class, fungsi, variabel, dan nama file. Juga dalam pemakaian format kode dan anotasi. Dengan begitu kode tetap mudah dibaca dan nanti lebih mudah melihat di mana ada yang salah. Selain itu orang lain juga lebih mudah memahami kodenya. Untuk itu saya memakai https://luals.github.io/wiki/.

Secara pribadi saya merasa nama program itu, lua language server, agak membingungkan, tapi program ini melakukan persis yang saya mau. Setelah terpasang, kamu bisa menulis baris — di dalam kode seperti di bawah.

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

Ini terlihat tidak penting, tapi membuat Visual Studio Code memberi peringatan kalau saya misalnya melakukan ini:

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

Salah satu kekurangan Lua dibanding misalnya GO adalah Lua tidak punya pengecekan seperti itu sehingga rawan kesalahan.

Dokumentasi

Tidak ada yang membaca dokumentasi… Tapi saya tahu ini akan dibutuhkan dalam jangka panjang, apalagi kalau engine-nya jadi open source. Untungnya lua language server juga punya generator .md yang membuat seluruh dokumentasi. Ini saya jalankan dengan script shell berikut, create_code_doc.sh

#!/bin/bash

echo "Generate code documentation"

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

Pengecekan tambahan

Test bisa sangat mengganggu dan makan waktu, terutama saat kamu membuat sesuatu dari nol. Kamu masih terus menguji dan mengubah banyak hal. Saya rasa kata refactor adalah kata yang paling sering muncul di git log.

Meski begitu saya tetap memakai unit test dan integration test.

Struktur dasar test engine3

Struktur dasar test engine3

Script shell check_code.sh sekarang seperti ini, dan bisa saya jalankan kapan saja, misalnya sebelum setiap git push atau nanti di sebuah pipeline.

#!/bin/bash

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

echo "UNIT TEST"
./unittest.sh

echo "INTEGRATION TEST"
./integrationtest.sh

Pengecekan kode

Dengan program https://github.com/mpeterv/luacheck kamu bisa mengecek (atau justru tidak mengecek) berbagai hal.

Unit test

Untuk ini saya memakai https://github.com/bluebird75/luaunit, dan setiap unit test punya file sendiri dengan fungsi-fungsinya.

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

Membuat ini berjalan agak lebih sulit tapi berguna, karena harus menjalankan love2d dan melakukan sesuatu dengan input dan output. Di sini saya juga memakai luaunit, tapi sebagai main.lua untuk menjalankan love2d, seperti di bawah

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

Bangun, bangun, bangun

Di tempat saya tinggal ada kekurangan rumah yang besar, dan slogan yang sering terdengar adalah:

Bangun, bangun, bangun

Engine3 tentu juga harus mem-build game, tapi untungnya itu hanya perlu saya lakukan saat game sudah selesai atau saat saya ingin mengujinya di platform lain.

Script shell yang saya buat untuk ini sekarang adalah build.sh, dan nanti akan diubah.

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

Penutup

Seperti yang kamu lihat, pertama kamu harus memastikan fondasinya kuat. Sebelum sampai di sana memang butuh waktu, tapi nanti kamu akan menuai hasilnya. Dulu misalnya saya tidak punya test atau pengecekan sintaks. Awalnya masih baik-baik saja, tapi kamu akan sampai di titik di mana kodenya sudah begitu banyak sehingga semuanya jadi tidak jelas.

Kamu harus berasumsi bahwa orang lain akan melihat kode itu dan harus bisa memahaminya. Atau bahwa kamu sendiri masih memahaminya 3 atau 10 tahun lagi.