Tag Archives: devops

Defold Game architecture diagram connecting game engine, backend, cloud, database, and live operations

Building Defold HTML5 Games in GitHub Codespaces — A Complete Guide

How to set up a fully headless Defold development environment in the cloud, complete with unit testing, linting, and a live HTML5 demo — no desktop engine install required.

Why?

Defold is a fantastic free game engine, but its editor is a desktop application. If you want to develop Defold games on a Chromebook, an iPad, a locked-down work laptop, or just want your entire toolchain reproducible in the cloud, GitHub Codespaces is a great answer. This guide walks through everything I learned setting up a complete Defold → HTML5 pipeline from scratch: building with bob.jar, testing Lua with busted, linting with luacheck, formatting with StyLua, and serving a playable “Hello World” demo — all inside a browser tab.

What is Defold?

Defold is a free, open-source, cross-platform game engine developed by the Defold Foundation (originally by King). It’s designed for 2D games with first-class support for HTML5/WebAssembly, making it ideal for web games, instant games, and playable ads. Here’s what makes it stand out:

  • Zero cost, no royalties — completely free for commercial use, no revenue sharing, no seat licenses.
  • Lightweight & fast — the engine core is ~1-2 MB; HTML5 builds start around 2-3 MB gzipped. Cold starts are near-instant.
  • Lua + native extensions — game logic is written in Lua 5.1 (LuaJIT), with the ability to drop into C/C++/Rust for performance-critical code via native extensions.
  • Component-based architecture — game objects are composed of components (sprites, labels, collision objects, scripts, factories, etc.) attached to collections (scenes). No heavy inheritance hierarchies.
  • Built-in physics — Box2D for 2D physics with collision groups, raycasts, and joints.
  • First-class HTML5/WASM — one-click (or one-command) export to WebAssembly with asm.js fallback. Supports PWA, WebGL 1/2, and custom engine templates.
  • Live update — push content updates to live games without app store review (mobile) or re-deployment (web).
  • Headless CI-friendly build toolbob.jar is a standalone Java CLI that compiles, bundles, and packages projects without the editor. Perfect for automation, GitHub Actions, and Codespaces.
  • Small, helpful community — active forums, Discord, and extensive documentation. The engine has been battle-tested in production games like Plague Inc., Cosmic Top Secret, and many King titles.

Defold’s philosophy is “batteries included but not forced” — you get a complete 2D toolkit (rendering, physics, audio, input, GUI, particles, tilemaps, Spine/DragonBones animation) without being locked into a specific workflow or visual editor. The editor is there if you want it, but the project format is plain text, so you can version-control everything and even hand-author projects like we do in this guide.

The Architecture

Defold’s build system is Java-based, which is what makes this possible. The engine ships bob.jar — the same build tool the desktop editor uses internally. Given a project folder of text assets (collections, game objects, scripts), bob compiles them into a runnable bundle for any platform, including HTML5 via WebAssembly.

Our final layout:

defold/
├── bob.jar                      # Defold build tool
├── setup_environment.sh         # One-command environment restore
└── testproject/
    ├── Makefile                 # test / lint / fmt / build / bundle / serve
    ├── game.project             # Engine configuration
    ├── .luacheckrc              # Linter config with Defold globals
    ├── main/
    │   ├── main.collection      # Bootstrap collection
    │   ├── hello.go             # Game object (script + label components)
    │   ├── hello.script         # Game logic entry point
    │   ├── hello.label          # Text label definition
    │   └── game_logic.lua       # Pure-Lua module (unit tested!)
    ├── fonts/
    │   ├── RocknRollOne-Regular.ttf
    │   ├── rocknroll.font       # Defold font resource
    │   └── OFL.txt              # License (required by SIL OFL)
    └── spec/
        └── game_logic_spec.lua  # busted test suite

Step 1: Install the Toolchain

Note: for setting up the environment you can also copy the bash file at the bottom of the page and have an Ai agent execute it for you

Java + the system library nobody warns you about

bob.jar needs Java. Codespaces usually has it, but if not:

sudo apt-get update && sudo apt-get install -y default-jdk

The first build failure you’ll likely hit is cryptic:

UnsatisfiedLinkError: .../libmodelc_shared.so: libXi.so.6:
cannot open shared object file: No such file or directory

Headless containers lack X11 libraries that Defold’s native asset tools link against. One line fixes it:

sudo apt-get install -y libxi6

Download bob.jar

Get it from the official release (pin your version for reproducibility):

mkdir -p defold && cd defold
curl -sL -o bob.jar \
  https://github.com/defold/defold/releases/download/1.13.1/bob.jar
java --enable-native-access=ALL-UNNAMED -jar bob.jar --version

Gotcha: On modern JDKs (17+), bob’s native loaders trigger restricted-access warnings and can fail outright. Always run it with --enable-native-access=ALL-UNNAMED.

Lua tooling

Defold embeds Lua 5.1 (technically LuaJIT), so match your local tools accordingly:

sudo apt-get install -y lua5.1 luarocks
sudo luarocks install luacheck   # linter
sudo luarocks install busted     # BDD test framework
sudo luarocks install luaunit    # xUnit alternative

# StyLua formatter (binary download)
curl -sL -o /tmp/stylua.zip \
  https://github.com/JohnnyMorganz/StyLua/releases/download/v2.5.2/stylua-linux-x86_64.zip
unzip /tmp/stylua.zip stylua -d /tmp && sudo mv /tmp/stylua /usr/local/bin/

# lua-language-server (IDE autocomplete/diagnostics)
LLS_URL=$(curl -s https://api.github.com/repos/LuaLS/lua-language-server/releases/latest \
  | grep browser_download_url | grep linux-x64 | grep -v musl | cut -d'"' -f4 | head -1)
curl -sL -o /tmp/lls.tar.gz "$LLS_URL"
sudo mkdir -p /opt/lls && sudo tar -xzf /tmp/lls.tar.gz -C /opt/lls
sudo ln -sf /opt/lls/bin/lua-language-server /usr/local/bin/

Step 2: Create a Minimal Project

Defold projects are folders of plain-text files — perfect for hand-authoring when you can’t use the editor.

game.project — the INI-style config. Note the bootstrap paths point at compiled extensions (.collectionc); this is how bob resolves them internally:

[bootstrap]
main_collection = /main/main.collectionc

[display]
width = 640
height = 480

[input]
game_binding = /input/game.input_bindingc

main/main.collection — the root scene. It instantiates one game object:

name: "main"
instances {
  id: "go"
  prototype: "/main/hello.go"
  position { x: 320.0 y: 240.0 z: 0.0 }
  rotation { x: 0.0 y: 0.0 z: 0.0 w: 1.0 }
}

Key lesson: collections reference game object files; they don’t hold components inline. Put your script and label on hello.go.

main/hello.go — the game object owns both components. Position the object at screen center so rotations pivot correctly later:

components {
  id: "hello"
  component: "/main/hello.script"
  position { x: 0.0 y: 0.0 z: 0.0 }
}
components {
  id: "label"
  component: "/main/hello.label"
  position { x: 0.0 y: 0.0 z: 0.1 }
}

input/game.input_binding — maps left mouse button to the action name touch:

mouse_trigger {
  input: MOUSE_BUTTON_LEFT
  action: "touch"
}

Step 3: Hello World Script

local game_logic = require("main.game_logic")

function init(self)
    self.game = game_logic.new()
    msg.post("@render:", "clear_color", {color = vmath.vector4(0.2, 0.5, 0.9, 1.0)})
    -- pulse animation
    go.animate("#label", "scale.x", go.PLAYBACK_LOOP_PINGPONG, 1.2, go.EASING_INOUTSINE, 1.2)
    go.animate("#label", "scale.y", go.PLAYBACK_LOOP_PINGPONG, 1.2, go.EASING_INOUTSINE, 1.2)
    msg.post(".", "acquire_input_focus")
end

function on_input(self, action_id, action)
    if action_id == hash("touch") and action.pressed then
        self.game:add_score(1)
        go.animate(".", "euler.z", go.PLAYBACK_ONCE_FORWARD, 360,
            go.EASING_INOUTQUAD, 0.8, 0, function()
                go.set(".", "euler.z", 0)
            end)
    end
end

function update(self, dt)
    -- rainbow color cycling
    local t = (self.t or 0) + dt
    self.t = t
    local r = 0.5 + 0.5 * math.sin(t * 2.0)
    local g = 0.5 + 0.5 * math.sin(t * 2.0 + 2.0944)
    local b = 0.5 + 0.5 * math.sin(t * 2.0 + 4.1888)
    go.set("#label", "color", vmath.vector4(r, g, b, 1))
end

Three things worth noting:

  1. Rotate the game object ("."), not the component ("#label") — component rotation isn’t reliably animatable.
  2. Pivot matters — because the object sits at screen center and the label has pivot: PIVOT_CENTER, the spin rotates around the text’s visual center instead of flinging it off-screen.
  3. Game logic lives in a pure module — more on that below.

Step 4: Testable Architecture

Engine scripts can’t run under standard Lua interpreters — they depend on Defold globals (go, msg, vmath). The fix is classic hexagonal architecture: keep logic in pure modules and make scripts thin adapters.

-- main/game_logic.lua
local game = {}

function game.new()
    local self = setmetatable({}, { __index = game })
    self.score = 0
    return self
end

function game:add_score(points)
    assert(type(points) == "number" and points > 0, "points must be positive")
    self.score = self.score + points
    return self.score
end

return game
-- spec/game_logic_spec.lua
local game_logic = require("game_logic")

describe("game_logic", function()
    it("adds points to the score", function()
        local state = game_logic.new()
        assert.equals(10, state:add_score(10))
    end)
end)

Run with the module path pointing into main/:

busted --lpath="./main/?.lua" spec

Step 5: Build & Serve the HTML5 Bundle

This is where most of my debugging time went. The critical discovery: always clean-build when bundling, or bob silently reuses stale artifacts:

BOB := java --enable-native-access=ALL-UNNAMED -jar ../bob.jar

bundle:
	$(BOB) --platform wasm-web -a --bundle-output build_html5 clean build bundle

serve:
	cd build_html5/unnamed && python3 -m http.server 8000

Then open the forwarded port in your browser. That’s it — a real Defold game running in WASM, built entirely in the cloud.

Debugging tip: verify your deploy actually happened by comparing the archive size bob reports against what the server returns from archive/archive_files.json. Mismatch = stale bundle.

Step 6: Custom Fonts

Drop a TTF in the project and define a .font resource:

font: "/fonts/RocknRollOne-Regular.ttf"
material: "/builtins/fonts/font-df.material"
size: 48
output_format: TYPE_DISTANCE_FIELD

Critical pairing rule: distance-field fonts must use label-df.material; bitmap fonts need label-fnt.material. Mixing them produces ugly blocky outlines. Also remember labels require a size field — and it’s a Vector4 scale, not a number.

If you ship an OFL font, include its license file. It’s a legal requirement and takes two seconds.

Step 7: Disaster Recovery — The Setup Script

Everything above is captured in one idempotent script, setup_environment.sh. It checks each dependency before installing, pins versions, validates downloads, works under both root and non-root users, and finishes by running the full verification suite (tool versions + tests + lint). Run it in any fresh codespace and you’re back in business:

./setup_environment.sh
# ==> Environment fully restored and verified.

Commit it alongside the project. Your environment becomes as reproducible as your code.

Gotchas Cheat Sheet

GotchaFix
Platform js-web not supportedUse wasm-web
libXi.so.6: cannot open shared object fileapt-get install libxi6
Native access errors on modern JDKjava --enable-native-access=ALL-UNNAMED
Changes don’t appear after rebuildBundle must include clean build — stale cache lies
Error shows truncated path (main.collectio)Display quirk; not the real problem
Label won’t compileNeeds text, size (Vector4!), font, AND material
Blocky font renderingDF font ↔ label-df.material, bitmap ↔ label-fnt.material
Spin animation does nothingAnimate "." not "#component" for rotation
Rotation swings off-screenPivot = game object position; center the object, zero the component offset
.luacheckrc syntax errorIt’s Lua: -- comments and {} tables, not # or []

Wrapping Up

What we ended up with: a cloud-native Defold workflow with version-pinned tooling, automated quality gates (make check runs lint + format validation + 8 unit tests), one-command bundling, live preview over a forwarded port, and full disaster recovery in a single script. Total cost: free tier Codespaces hours.

The same pattern extends naturally — add a GitHub Actions workflow that runs make check && make bundle on every push and publishes build_html5/unnamed/ to GitHub Pages, and you’ve got continuous deployment for a browser game. But that’s a post for another day.

#!/usr/bin/env bash
###############################################################################
# Defold HTML5 dev environment bootstrap
#
# Restores the complete toolchain needed to build/test/serve this project:
#   - bob.jar (Defold build tool) + libxi6 system dependency
#   - Lua 5.1, Luarocks, Luacheck, busted, luaunit (testing)
#   - StyLua (formatter), lua-language-server (LSP)
#
# Usage:
#   ./setup_environment.sh          # install everything + verify
#   ./setup_environment.sh --no-verify   # skip verification step
#
# Assumes it is run from the repository root or from defold/.
###############################################################################
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Resolve defold/ dir whether script lives in defold/ or repo root
if [[ -d "$SCRIPT_DIR/defold" ]]; then DEFOLD_DIR="$SCRIPT_DIR/defold"; else DEFOLD_DIR="$SCRIPT_DIR"; fi

BOB_VERSION="1.13.1"
BOB_URL="https://github.com/defold/defold/releases/download/${BOB_VERSION}/bob.jar"
STYLUA_VERSION="2.5.2"

log() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; }

# Run a command with sudo only if not already root (codespace/root containers)
maybe_sudo() {
    if [[ $(id -u) -eq 0 ]]; then "$@"; else sudo "$@"; fi
}

apt_install() {
    maybe_sudo apt-get update -qq
    maybe_sudo apt-get install -y "$@"
}

###############################################################################
log "1/7 Checking Java (needed for bob.jar)"
###############################################################################
if ! command -v java >/dev/null 2>&1; then
    echo "Java not found. Installing default-jdk..."
    apt_install default-jdk
fi
java -version 2>&1 | head -1

###############################################################################
log "2/7 Installing system dependency libxi6 (required by bob's native tools)"
###############################################################################
dpkg -s libxi6 >/dev/null 2>&1 || apt_install libxi6

###############################################################################
log "3/7 Downloading bob.jar ${BOB_VERSION}"
###############################################################################
if [[ -f "$DEFOLD_DIR/bob.jar" ]] && java -jar "$DEFOLD_DIR/bob.jar" --version 2>/dev/null | grep -q "$BOB_VERSION"; then
    echo "bob.jar ${BOB_VERSION} already present, skipping download."
else
    curl -sL -o "$DEFOLD_DIR/bob.jar" "$BOB_URL"
    [[ $(stat -c%s "$DEFOLD_DIR/bob.jar") -gt 1000000 ]] || { echo "ERROR: bob.jar download failed"; exit 1; }
fi

###############################################################################
log "4/7 Installing Lua 5.1 + Luarocks + testing frameworks"
###############################################################################
command -v lua5.1 >/dev/null 2>&1 || apt_install lua5.1
command -v luarocks >/dev/null 2>&1 || apt_install luarocks
luarocks show luacheck >/dev/null 2>&1 || maybe_sudo luarocks install luacheck
luarocks show busted >/dev/null 2>&1   || maybe_sudo luarocks install busted
luarocks show luaunit >/dev/null 2>&1  || maybe_sudo luarocks install luaunit

###############################################################################
log "5/7 Installing StyLua formatter"
###############################################################################
if ! command -v stylua >/dev/null 2>&1; then
    TMPZIP="$(mktemp /tmp/stylua.XXXX.zip)"
    curl -sL -o "$TMPZIP" "https://github.com/JohnnyMorganz/StyLua/releases/download/v${STYLUA_VERSION}/stylua-linux-x86_64.zip"
    INSTALL_DIR="$(mktemp -d /tmp/stylua_install.XXXX)"
    unzip -o "$TMPZIP" stylua -d "$INSTALL_DIR" >/dev/null
    maybe_sudo mv "$INSTALL_DIR/stylua" /usr/local/bin/
    rm -rf "$TMPZIP" "$INSTALL_DIR"
fi

###############################################################################
log "6/7 Installing lua-language-server"
###############################################################################
if ! command -v lua-language-server >/dev/null 2>&1; then
    LLS_URL=$(curl -s https://api.github.com/repos/LuaLS/lua-language-server/releases/latest \
        | grep browser_download_url | grep linux-x64 | grep -v musl | cut -d'"' -f4 | head -1)
    [[ -n "$LLS_URL" ]] || { echo "ERROR: could not resolve lua-language-server download URL"; exit 1; }
    TARGZ="$(mktemp /tmp/lls.XXXX.tar.gz)"
    curl -sL -o "$TARGZ" "$LLS_URL"
    maybe_sudo mkdir -p /opt/lua-language-server
    maybe_sudo tar -xzf "$TARGZ" -C /opt/lua-language-server
    maybe_sudo ln -sf /opt/lua-language-server/bin/lua-language-server /usr/local/bin/lua-language-server
    rm -f "$TARGZ"
fi

###############################################################################
log "7/7 Verification"
###############################################################################
if [[ " $* " == *" --no-verify "* ]]; then
    echo "Skipping verification."
    exit 0
fi

cd "$DEFOLD_DIR/testproject"
FAIL=0

# run_check <label> <command...> — records failure without aborting (set -e safe)
run_check() {
    local label="$1"; shift
    echo "--- $label ---"
    if ! "$@"; then
        FAIL=1
    fi
}

run_check "bob.jar" java --enable-native-access=ALL-UNNAMED -jar ../bob.jar --version
run_check "luacheck version" luacheck --version
run_check "stylua" stylua --version
run_check "lua-language-server" lua-language-server --version
run_check "busted tests" busted --lpath="./main/?.lua" spec
run_check "lint" luacheck main spec

echo ""
if [[ $FAIL -eq 0 ]]; then
    echo -e "\033[1;32mEnvironment fully restored and verified.\033[0m"
    echo "Next steps:"
    echo "  cd $DEFOLD_DIR/testproject"
    echo "  make bundle        # build HTML5 bundle into build_html5/"
    echo "  make serve         # serve at http://localhost:8000"
else
    echo -e "\033[1;31mSome checks failed — review output above.\033[0m" >&2
    exit 1
fi

How to fix an Azure Data Factory Pull Request Merge Conflict

Typically most pipeline development use cases can be handled directly within Data Factory through the Azure Web Portal. However where the line can get blurred sometimes between working in the cloud and working locally is with DevOps GIT.

If a GIT based deployment gets tangled there is an expectation you will be able to work through the desktop interface for GIT or worse fall back to using command line.

This is necessary because before a GIT pull request can complete, any conflicts with the target branch must be resolved and this usually involves issuing a few commands to put the matter right. The options for resolving conflicts through the web portal by default are limited to nonexistent which is at odds with the very high level, low code approach of developing pipelines in Data Factory.

Luckily if a merge conflict occurs there is an extension you can try.

https://marketplace.visualstudio.com/items?itemName=ms-devlabs.conflicts-tab

A conflict might occur because the master branch is no longer in sync with the development branch for example i.e. the master branch was changed after a development branch was created from it. When a pull request is created this may throw a merge conflict error blocking the merge from proceeding. Without resorting to code the extension above will allow you to choose between the source and target branch and specify which has the correct file version.