Script Best Practices
Follow these guidelines to write effective, performant, and maintainable scripts for DDDBrowser.
Script Structure
Always Return a Table
Scripts must return a table with lifecycle methods:
local MyScript = {}
function MyScript:on_start()
-- Initialization
end
return MyScript
Don't:
-- Wrong: No return statement
local MyScript = {}
function MyScript:on_start()
end
Use self for Instance Variables
Store instance-specific data in self:
function MyScript:on_start()
self.counter = 0
self.position = {x = 0, y = 0, z = 0}
end
Don't use module-level variables (they're shared across instances):
-- Wrong: Shared across all instances
local counter = 0
function MyScript:on_start()
counter = counter + 1 -- All instances share this!
end
Initialize in on_start
Set up all variables in on_start, not at module level:
function MyScript:on_start()
self.time = 0
self.speed = 1.0
self.amplitude = 5.0
-- Load initial data
if self.data then
self.speed = self.data.speed or self.speed
end
end
Performance
Use Delta Time
Always use dt (delta time) for frame-rate independent updates:
function MyScript:on_update(dt)
self.time = self.time + dt -- Frame-rate independent
local offset = math.sin(self.time) * 5
end
Don't assume fixed frame rate:
-- Wrong: Frame-rate dependent
function MyScript:on_update(dt)
self.time = self.time + 1 -- Will vary with FPS!
end
Keep Updates Fast
Avoid expensive operations in on_update:
-- Good: Cache expensive calculations
function MyScript:on_start()
self.cachedValue = expensiveCalculation()
end
function MyScript:on_update(dt)
-- Use cached value
useValue(self.cachedValue)
end
Limit Update Frequency
Only update when necessary:
function MyScript:on_update(dt)
self.updateTimer = (self.updateTimer or 0) + dt
-- Update every 0.1 seconds instead of every frame
if self.updateTimer >= 0.1 then
self.updateTimer = 0
-- Do expensive update here
end
end
Error Handling
Check API Availability
Always check if APIs exist before using them:
if Engine and Engine.setEntityPosition then
Engine.setEntityPosition(self.entity, x, y, z)
end
Handle Missing Values
Check for nil values:
local value = localStorage.get("key")
if value then
self.data = value
else
self.data = "default"
end
Graceful Degradation
Provide fallbacks when features aren't available:
function MyScript:on_update(dt)
if Engine and Engine.setEntityPosition then
Engine.setEntityPosition(self.entity, x, y, z)
else
-- Fallback behavior
self.lastPosition = {x = x, y = y, z = z}
end
end
State Management
Save State Properly
Use on_save() to persist complex state:
function MyScript:on_save()
return {
counter = self.counter,
position = self.position,
state = self.state
}
end
Load State Properly
Restore state in on_load():
function MyScript:on_load(state)
if state then
self.counter = state.counter or 0
self.position = state.position or {x = 0, y = 0, z = 0}
end
end
Use localStorage for Simple Values
For simple string values, use localStorage:
-- Save
localStorage.set("playerName", "Alice")
-- Load
local name = localStorage.get("playerName")
Event-Driven Architecture
Use Events for Communication
Decouple scripts using events:
-- Gamemode triggers event
Gamemode.triggerEvent("playerDied", {reason = "fall"})
-- Entity listens
Gamemode.onEvent("playerDied", function(data)
-- Handle player death
end)
Namespace Event Names
Use prefixes to organize events:
-- Good: Namespaced
Gamemode.triggerEvent("game.levelComplete", data)
Gamemode.triggerEvent("player.died", data)
-- Avoid: Generic names
Gamemode.triggerEvent("event", data)
Code Organization
Keep Scripts Focused
Each script should have a single responsibility:
-- Good: Focused script
local Animation = {}
function Animation:on_update(dt)
-- Only handles animation
end
Modular Design
Break complex logic into functions:
local ComplexScript = {}
function ComplexScript:calculatePosition(time)
return math.sin(time) * 5
end
function ComplexScript:on_update(dt)
self.time = self.time + dt
local pos = self:calculatePosition(self.time)
Engine.setEntityPosition(self.entity, 0, pos, 0)
end
Common Patterns
Animation Pattern
local Animated = {}
function Animated:on_start()
self.time = 0
self.speed = 1.0
self.amplitude = 5.0
end
function Animated:on_update(dt)
self.time = self.time + dt
local offset = math.sin(self.time * self.speed) * self.amplitude
Engine.setEntityPosition(self.entity, 0, offset, 0)
end
return Animated
Interaction Pattern
local Interactive = {}
function Interactive:on_start()
self.interactionCount = 0
end
function Interactive:on_interact(actorId)
self.interactionCount = self.interactionCount + 1
if Engine and Engine.openTextBox then
Engine.openTextBox(
"interaction-" .. tostring(self.entity),
"Interaction",
"You've interacted " .. self.interactionCount .. " times!",
{"OK"}
)
end
end
return Interactive
State Persistence Pattern
local Persistent = {}
function Persistent:on_start()
local saved = localStorage.get("counter")
self.counter = tonumber(saved) or 0
end
function Persistent:on_update(dt)
self.counter = self.counter + 1
localStorage.set("counter", tostring(self.counter))
end
function Persistent:on_save()
return {
counter = self.counter
}
end
function Persistent:on_load(state)
if state and state.counter then
self.counter = state.counter
end
end
return Persistent
Debugging
Use print() for Debugging
Print statements help debug scripts:
function MyScript:on_start()
print("Script started for entity " .. tostring(self.entity))
end
function MyScript:on_update(dt)
if self.debug then
print("Time: " .. tostring(self.time))
end
end
Enable Script Tracing
Set environment variable for detailed logging:
DDDBROWSER_SCRIPT_TRACE=1
This enables detailed method call logging.
Security
Validate Input
Always validate user input and external data:
function MyScript:on_interact(actorId)
if actorId and actorId == 0 then
-- Only allow player interactions
end
end
Don't Trust External Data
Validate data from HTTP requests:
local response = Engine.httpRequest({url = "https://api.example.com/data"})
if response.success then
local data = response.body
-- Validate data before using
if isValid(data) then
useData(data)
end
end
Next Steps
- Script Examples - See these patterns in action
- Engine API - Complete API reference
- Troubleshooting - Fix common issues