Patterns & Best Practices¶
Working code patterns for common game mechanics, with tested examples.
Core Pattern: Event-Driven Design¶
SplashEdit games work best with fully event-driven architecture:
onInteractfor player-initiated actionsonButtonPressfor button-driven controlsonCollideWithPlayerfor collision responsesonTriggerEnter/onTriggerExitfor area triggersAnimation.Playwith callbacks for time-based actionsCutscene.Playwith callbacks for sequenced events
Core Pattern: Scene as Controller¶
The scene script is the central coordinator. Define shared functions there and publish them to _G so object scripts can access them:
-- scene.lua
local score = 0
local scoreText = -1
function onSceneCreationEnd()
local hud = UI.FindCanvas("HUD")
scoreText = UI.FindElement(hud, "ScoreText")
end
function addScore(amount)
score = score + amount
if scoreText >= 0 then
UI.SetText(scoreText, "Score: " .. score)
end
end
function setStatus(msg)
Debug.Log("[Status] " .. msg)
end
-- CRITICAL: Publish to _G so object scripts can call these
_G.addScore = addScore
_G.setStatus = setStatus
Object scripts call these directly:
Pattern: One-Time Action¶
Use a local flag to prevent repeated triggers:
local collected = false
function onCollideWithPlayer(self)
if collected then return end
collected = true
Audio.Play("collect", 127, 64)
Entity.SetActive(self, false)
addScore(100)
setStatus("Collected! +100 points")
end
Pattern: Animation Gating¶
Prevent overlapping animations by checking IsPlaying before starting:
local isOpen = false
function onInteract(self)
-- Don't start if either animation is running
if Animation.IsPlaying("door_open") or Animation.IsPlaying("door_close") then
return
end
-- Disable interaction during animation
Interact.SetEnabled(self, false)
if isOpen then
isOpen = false
Audio.Play("door_close", 100, 64)
Animation.Play("door_close", {
onComplete = function()
Interact.SetEnabled(self, true)
end
})
else
isOpen = true
Audio.Play("door_open", 100, 64)
Animation.Play("door_open", {
onComplete = function()
Interact.SetEnabled(self, true)
end
})
end
end
Pattern: Dialogue System¶
Build a dialogue system in the scene script, then call it from NPC scripts:
Scene Script (Dialogue System)¶
-- scene.lua
local dialogueCanvas = -1
local dialogueText = -1
local inDialogue = false
local dialogueLines = {}
local dialogueLine = 0
function onSceneCreationEnd()
dialogueCanvas = UI.FindCanvas("Dialogue")
if dialogueCanvas >= 0 then
dialogueText = UI.FindElement(dialogueCanvas, "DialogueText")
UI.SetCanvasVisible(dialogueCanvas, false)
end
end
function startDialogue(lines)
if inDialogue then return end
inDialogue = true
dialogueLines = lines
dialogueLine = 1
Controls.SetEnabled(false)
UI.SetCanvasVisible(dialogueCanvas, true)
UI.SetText(dialogueText, dialogueLines[1])
end
function advanceDialogue()
if not inDialogue then return end
dialogueLine = dialogueLine + 1
if dialogueLine > #dialogueLines then
endDialogue()
else
UI.SetText(dialogueText, dialogueLines[dialogueLine])
end
end
function endDialogue()
inDialogue = false
Controls.SetEnabled(true)
UI.SetCanvasVisible(dialogueCanvas, false)
end
function isInDialogue()
return inDialogue
end
-- Publish all
_G.startDialogue = startDialogue
_G.advanceDialogue = advanceDialogue
_G.endDialogue = endDialogue
_G.isInDialogue = isInDialogue
NPC Script¶
-- npc.lua
local talked = false
function onCreate(self)
talked = false
end
function onInteract(self)
if isInDialogue() then return end
Interact.SetEnabled(self, false)
if not talked then
talked = true
startDialogue({
"Hello, traveler!",
"Welcome to the world.",
"Press CROSS to continue."
})
else
startDialogue({
"Hello again!",
"Nothing new to say."
})
end
end
function onButtonPress(self, button)
if not isInDialogue() then return end
if button == Input.CROSS then
advanceDialogue()
if not isInDialogue() then
Interact.SetEnabled(self, true)
end
end
end
Pattern: Persistent Data Across Scenes¶
Use Persist to carry data between scene loads:
-- Before leaving the scene
Persist.Set("score", currentScore)
Persist.Set("health", currentHealth)
Persist.Set("came_from", Scene.GetIndex())
Scene.Load(1)
-- In the new scene
function onSceneCreationStart()
local score = Persist.Get("score") or 0
local health = Persist.Get("health") or 100
local prevScene = Persist.Get("came_from") or -1
Debug.Log("Arrived from scene " .. prevScene)
end
16 entry limit
Persist supports only 16 key-value pairs. Use it for important game state only.
Pattern: Toggle Switch¶
Control another object by name lookup:
local target = nil
local isOn = false
function onCreate(self)
target = Entity.Find("SwitchTarget")
if target then
Entity.SetActive(target, false)
else
Debug.Log("WARNING: SwitchTarget not found!")
end
end
function onInteract(self)
isOn = not isOn
if target then
Entity.SetActive(target, isOn)
end
Audio.Play(isOn and "switch_on" or "switch_off", 100, 64)
setStatus(isOn and "Switch ON" or "Switch OFF")
end
Pattern: Object Movement with Buttons¶
Move objects using button input with fixed-point step sizes:
local selected = false
function onInteract(self)
selected = not selected
if selected then
Controls.SetEnabled(false)
Interact.SetEnabled(self, false)
setStatus("D-pad to move, L1/R1 for height, Triangle to deselect")
else
Controls.SetEnabled(true)
Interact.SetEnabled(self, true)
end
end
function onButtonPress(self, button)
if not selected then return end
local pos = self.position
local step = FixedPoint.new(1) / 64
if button == Input.UP then
Entity.SetPosition(self, Vec3.new(pos.x, pos.y, pos.z + step))
elseif button == Input.DOWN then
Entity.SetPosition(self, Vec3.new(pos.x, pos.y, pos.z - step))
elseif button == Input.LEFT then
Entity.SetPosition(self, Vec3.new(pos.x - step, pos.y, pos.z))
elseif button == Input.RIGHT then
Entity.SetPosition(self, Vec3.new(pos.x + step, pos.y, pos.z))
elseif button == Input.L1 then
Entity.SetPosition(self, Vec3.new(pos.x, pos.y + step, pos.z))
elseif button == Input.R1 then
Entity.SetPosition(self, Vec3.new(pos.x, pos.y - step, pos.z))
elseif button == Input.SQUARE then
local rot = Entity.GetRotationY(self)
local one = FixedPoint.new(1)
Entity.SetRotationY(self, rot + one / 2) -- Rotate 90 degrees
elseif button == Input.TRIANGLE then
selected = false
Controls.SetEnabled(true)
Interact.SetEnabled(self, true)
end
end
Pattern: Cutscene Trigger Zone¶
One-time area trigger that plays a cutscene:
local played = false
function onTriggerEnter()
if played then return end
played = true
Controls.SetEnabled(false)
Cutscene.Play("camera_flyover", {
onComplete = function()
Controls.SetEnabled(true)
setStatus("Cutscene complete!")
end
})
end
Pattern: Health System¶
Color-coded health bar with damage and healing:
-- scene.lua
local health = 100
local healthBar = -1
function onSceneCreationEnd()
local hud = UI.FindCanvas("HUD")
healthBar = UI.FindElement(hud, "HealthBar")
updateHealthBar()
end
function updateHealthBar()
if healthBar < 0 then return end
UI.SetProgress(healthBar, health)
if health > 50 then
UI.SetProgressColors(healthBar, 0, 40, 0, 0, 255, 0) -- Green
elseif health > 25 then
UI.SetProgressColors(healthBar, 40, 40, 0, 255, 255, 0) -- Yellow
else
UI.SetProgressColors(healthBar, 40, 0, 0, 255, 0, 0) -- Red
end
end
function applyDamage(amount)
health = PSXMath.Clamp(health - amount, 0, 100)
updateHealthBar()
if health <= 0 then
setStatus("You died! Reloading...")
Controls.SetEnabled(false)
Persist.Set("score", 0)
Scene.Load(Scene.GetIndex()) -- Reload current scene
end
end
function applyHeal(amount)
health = PSXMath.Clamp(health + amount, 0, 100)
updateHealthBar()
end
_G.applyDamage = applyDamage
_G.applyHeal = applyHeal
Damage Trigger¶
Heal Trigger¶
Pattern: Scene Portal¶
Transition between scenes with data preservation:
-- portal_trigger.lua
function onTriggerEnter()
setStatus("Loading next scene...")
Persist.Set("came_from", Scene.GetIndex())
Scene.Load(0) -- Load scene 0
end
Pattern: Looping Animation Toggle¶
Start/stop a looping animation on interact:
function onInteract(self)
if Animation.IsPlaying("anim_spinner") then
Animation.Stop("anim_spinner")
setStatus("Spinner stopped!")
else
Animation.Play("anim_spinner", {loop = true})
setStatus("Spinner started!")
end
end
Pattern: Entity Scanning / Diagnostics¶
Enumerate all entities for debugging:
function onInteract(self)
Debug.Log("=== Entity Scan ===")
Debug.Log("Total entities: " .. Entity.GetCount())
Entity.ForEach(function(obj, index)
local pos = Entity.GetPosition(obj)
local active = Entity.IsActive(obj)
Debug.Log(" [" .. index .. "] pos=" .. pos.x .. "," .. pos.y .. "," .. pos.z
.. " active=" .. tostring(active))
end)
end