Engine3

Selepas bertahun-tahun membuat konsep, kini tiba masa untuk nombor 3

Enjin nombor 3...

Engine3

Dalam artikel tentang alat pembangunan anda sudah boleh membaca apa yang akan saya gunakan untuk membuat permainan Signal From The Stars.

Oleh sebab Love2d ialah rangka kerja yang hanya mempunyai beberapa fungsi asas yang ringkas, saya mendapat idea untuk membuat enjin asas dahulu, kemudian permainan di atasnya. Oleh sebab saya sudah beberapa kali membuat konsep seperti ini dalam hidup saya, ini akan menjadi enjin ketiga, sebab itu namanya engine3.

Kelebihannya, saya mungkin boleh melancarkan engine3 sebagai open source, dan juga suatu hari nanti membuat permainan yang sama sekali berbeza. Semua ini berada dalam repositori git yang berasingan, yang akan digunakan oleh permainan kemudian.

Jadi pada awalnya saya belum benar-benar mengerjakan permainan, tetapi bermula dengan menulis sedikit logik. Sudah tentu saya melakukannya dengan memikirkan apa yang sebenarnya akan digunakan dalam permainan. Malah, semua yang tidak saya perlukan tidak akan saya masukkan ke dalam rangka kerja (less is more).

Dari segi fail, struktur engine3 kelihatan seperti ini:

Struktur asas engine3

Struktur asas engine3

Pustaka, modul dan mod

Ada perkara yang anda mahu buat sendiri, ada yang tidak. Sebabnya, mencipta semula roda sangat mahal. Anda kehilangan masa tambahan dan perlu menguji serta menyelenggaranya sendiri.

Sebab itu ada folder lib dalam engine3. Di dalamnya ada beberapa pustaka yang bukan saya buat sendiri, tetapi diterbitkan contohnya di bawah lesen MIT.

Ada juga folder lib-dev yang mengandungi modul pihak ketiga yang hanya diperlukan semasa pembangunan.

Sangat penting untuk menulis kod secara konsisten dalam penamaan kelas, fungsi, pemboleh ubah dan nama fail. Begitu juga dalam penggunaan format kod dan anotasi. Dengan itu kod kekal mudah dibaca dan kemudian lebih mudah melihat di mana sesuatu tidak kena. Selain itu, orang lain juga lebih mudah memahami kod. Untuk itu saya menggunakan https://luals.github.io/wiki/.

Secara peribadi saya rasa nama program itu, lua language server, agak mengelirukan, tetapi ia melakukan tepat apa yang saya mahu. Selepas pemasangan, anda boleh menulis baris — dalam kod 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 nampak tidak penting, tetapi membuatkan Visual Studio Code memberi amaran jika saya contohnya melakukan ini:

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

Salah satu kelemahan Lua berbanding contohnya GO ialah Lua tidak mempunyai semakan sebegini dan oleh itu mudah berlaku ralat.

Dokumentasi

Tiada siapa membaca dokumentasi… Tetapi saya tahu ia akan diperlukan dalam jangka panjang, terutamanya jika enjin ini menjadi open source. Nasib baik lua language server juga ada penjana .md yang menghasilkan keseluruhan dokumentasi. Saya melakukannya dengan skrip shell berikut, create_code_doc.sh

#!/bin/bash

echo "Generate code documentation"

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

Semakan tambahan

Ujian boleh sangat mengganggu dan memakan masa, terutamanya apabila anda membina sesuatu dari kosong. Anda masih sentiasa menguji dan mengubah banyak perkara. Saya rasa perkataan refactor ialah perkataan yang paling banyak digunakan dalam git log.

Walaupun begitu saya tetap menggunakan unit test dan integration test.

Struktur asas ujian engine3

Struktur asas ujian engine3

Skrip shell check_code.sh kini kelihatan begini, dan saya boleh menjalankannya bila-bila masa, contohnya sebelum setiap git push atau kemudian dalam pipeline.

#!/bin/bash

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

echo "UNIT TEST"
./unittest.sh

echo "INTEGRATION TEST"
./integrationtest.sh

Semakan kod

Dengan program https://github.com/mpeterv/luacheck anda boleh menyemak (atau sengaja tidak menyemak) pelbagai perkara.

Unit test

Untuk ini saya menggunakan https://github.com/bluebird75/luaunit, dan setiap unit test mempunyai fail sendiri dengan fungsi yang berkaitan.

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

Menjadikannya berfungsi agak lebih sukar tetapi berguna, kerana ia perlu memulakan love2d dan melakukan sesuatu dengan input dan output. Di sini juga saya menggunakan luaunit, tetapi sebagai main.lua untuk memulakan 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

Bina, bina, bina

Di tempat saya tinggal ada kekurangan rumah yang besar, dan slogan yang sering didengar ialah:

Bina, bina, bina

Engine3 sudah tentu juga perlu membina permainan, tetapi nasib baik saya hanya perlu melakukannya apabila permainan sudah siap atau apabila saya mahu mengujinya pada platform lain.

Skrip shell yang saya buat untuk ini buat masa ini ialah build.sh, dan ia akan diubah kemudian.

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

Akhir kata

Seperti yang anda lihat, anda perlu pastikan asasnya kukuh dahulu. Sebelum sampai ke tahap itu memang mengambil masa, tetapi kemudian anda akan mendapat ganjarannya. Dahulu misalnya saya tiada ujian atau semakan sintaks. Pada mulanya masih okey, tetapi anda akan sampai ke satu tahap di mana anda sudah menulis begitu banyak kod sehingga semuanya menjadi kabur.

Anda perlu menganggap bahawa orang lain akan melihat kod itu dan perlu dapat memahaminya. Atau bahawa anda sendiri masih memahaminya 3 atau 10 tahun lagi.