Defold Integration
Reference for integrating plotknot into a Defold project. For a step-by-step walkthrough, see the Defold Integration Tutorial.
Project structure
A typical Defold + plotknot project:
my_game/
main/
plotknot.lua -- runtime library (copy from runtime/defold/)
stories/
main.plotknot -- source stories
compiled/
main.lua -- compiled output (generated)
dialogue.script -- Defold script driving the story
dialogue.gui -- GUI with text label + choice buttons
dialogue.gui_script -- GUI script handling button input
Compiling stories
Compile during development:
plotknot compile main/stories/main.plotknot --output main/compiled/
Or use watch mode to recompile on save:
plotknot watch main/stories/main.plotknot --output main/compiled/
The compiled .lua file is a Defold-compatible module. Add it to your game.project dependencies or require it directly.
Loading compiled data
local plotknot = require("main.plotknot")
local story_data = require("main.compiled.main")
local story = plotknot.load(story_data)
load validates the data structure and returns a story object. It does not start playback.
Driving the story from a script
local plotknot = require("main.plotknot")
local story_data = require("main.compiled.main")
function init(self)
self.story = plotknot.load(story_data)
self.story:on_call("play_sound", function(name)
msg.post("/sound#script", "play", { sound = name })
end)
self.story:on_stage("boss_room", function()
msg.post("/music#script", "play_track", { track = "boss" })
end)
self.story:start()
show_current(self)
end
function show_current(self)
local lines = self.story:current()
for _, line in ipairs(lines) do
if line.type == "text" then
gui.set_text(gui.get_node("story_text"), line.text)
elseif line.type == "choice" then
for i, choice in ipairs(line.choices) do
local btn = gui.get_node("choice_" .. i)
gui.set_text(btn, choice.text)
gui.set_enabled(btn, true)
end
-- Hide unused buttons
for i = #line.choices + 1, 4 do
gui.set_enabled(gui.get_node("choice_" .. i), false)
end
end
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("choice_pressed") then
self.story:advance(message.index)
show_current(self)
end
end
GUI choice buttons
Connect GUI buttons to the story script:
-- dialogue.gui_script
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed then
for i = 1, 4 do
local btn = gui.get_node("choice_" .. i)
if gui.is_enabled(btn) and gui.pick_node(btn, action.x, action.y) then
msg.post("/dialogue#script", "choice_pressed", { index = i })
return true
end
end
end
end
Saving and loading story state
The story’s state lives in two tables: variables and character properties.
-- Save
local function save_story(story)
local state = {
section = story:stage(),
vars = story:variables(),
chars = story:chars(),
}
-- Serialize to JSON or Defold's sys.serialize
local path = sys.get_save_file("game", "story_save")
sys.save(path, state)
end
-- Load
local function load_story(story)
local path = sys.get_save_file("game", "story_save")
local state = sys.load(path)
if state then
-- Restore variables (live reference — mutations apply directly)
local vars = story:variables()
for k, v in pairs(state.vars) do
vars[k] = v
end
local chars = story:chars()
for k, v in pairs(state.chars) do
chars[k] = v
end
-- Navigate to saved section
story:goto_section(state.section)
end
end
The variables() and chars() methods return live references. Mutations are visible to the story engine immediately.
Common patterns
Dialogue trigger from collision
Start a conversation when the player enters a trigger zone:
-- player.script
function on_message(self, message_id, message, sender)
if message_id == hash("trigger_response") and message.enter then
if message.other_group == hash("npc") then
msg.post("/dialogue#script", "start_story", { npc = "blacksmith" })
end
end
end
Quest state tracking
Use story variables to track quest progress, then read them from game code:
// In the story
set quest.crypt_accepted = true
set quest.crypt_reward = 500
-- In game code
local vars = story:variables()
if vars.quest and vars.quest.crypt_accepted then
-- Update quest log UI
end
Conditional NPC dialogue
Register a function that reads game state:
story:register_function("npc_mood", function(name)
local npc = game_state.npcs[name]
if not npc then return "unknown" end
return npc.mood
end)
// In the story
if npc_mood("blacksmith") == "friendly":
<friendly> Good to see you again! What can I forge for you?
else:
The blacksmith eyes you warily. "What do you want?"
Audio triggers via on_stage
story:on_stage("tavern", function()
msg.post("/music#script", "play_track", { track = "tavern_theme" })
end)
story:on_stage("crypt", function()
msg.post("/music#script", "play_track", { track = "crypt_ambient" })
end)
Error handling
The runtime is defensive:
- Unknown function calls produce a warning and return
nil - Invalid choice indices are ignored
- Goto cycles are detected and broken (with a warning)
- Missing sections produce a warning and halt advancement
Check story:warnings() during development to catch integration issues early.
Next steps
- Defold Integration Tutorial — step-by-step from empty project
- Runtime API — full Lua runtime reference
- Extensibility — custom functions, call handlers, line types