本文解决什么问题、适合谁、前置环境
我最近在给一款 2D 独立 RPG 做支线任务,手写几十条任务 JSON 实在枯燥,于是尝试在 Defold 引擎里直接调用 Claude API 让大模型批量生成任务数据,再由游戏解析渲染。网上关于 Unity/Godot 接大模型的教程不少,但 Defold接入Claude做任务系统教程几乎搜不到能跑的,踩了几个坑后,我把整套可运行流程整理成本文。
适合谁:已经会用 Defold 做基础场景、懂一点 Lua 和 JSON、想给游戏加“AI 动态生成任务”能力的开发者。
本文实测环境(作者已实机验证):
- macOS 14.5(Apple Silicon)
- Defold 1.9.4(Lua 5.1 运行时)
- Claude Messages API,
anthropic-version: 2023-06-01,模型claude-sonnet-4-6 - 验证日期:2026-06
Defold 引擎内置了 http 和 json 两个模块,无需装任何第三方库就能发 HTTPS 请求并解析 JSON,这也是我选它做原型的原因。
任务系统数据结构设计:让 Claude 输出可解析的 JSON
接大模型最容易翻车的地方不是网络,而是返回结构不稳定。第一版我让 Claude “生成一个任务”,结果它有时给 Markdown、有时加解释文字,json.decode 直接报错。后来我固定了一份 Schema,并在 prompt 里明确“只输出 JSON、不要任何额外文字”,稳定性立刻上来。
我最终采用的任务数据结构:
{
"id": "quest_forest_01",
"title": "迷雾森林的委托",
"description": "村长请你找回被野狼叼走的祖传铜铃。",
"objectives": [
{ "type": "collect", "target": "bronze_bell", "count": 1 }
],
"reward": { "gold": 120, "exp": 60, "item": "leather_boots" },
"next": "quest_forest_02"
}
关键设计点:
objectives用数组,方便一个任务多目标;type收敛成有限枚举(collect/kill/talk/reach),便于游戏逻辑分支。reward里的数值后面会做越界校验,防止模型给出gold: 999999破坏经济系统。next是后续任务 id,实现任务链;无后续时约定为空字符串。
Defold 中配置 HTTP 请求:封装 Claude 调用与密钥读取
先说密钥。原型阶段我把 key 放在 game.project 的自定义配置段,通过 sys.get_config_string 读取:
[claude]
api_key = sk-ant-xxxxxxxx
⚠️ 注意:game.project 会被打进客户端包,正式上线务必改用自建后端代理转发,绝不能把真实 key 打进玩家能拿到的包里。本文示例仅用于本地原型,示例中不出现任何真实密钥。
Claude 请求封装成一个独立模块 claude.lua(放在 /scripts/claude.lua):
-- /scripts/claude.lua
local M = {}
local API_URL = "https://api.anthropic.com/v1/messages"
-- prompt: 玩家场景描述;on_done(err, quest_table)
function M.gen_quest(prompt, on_done)
local api_key = sys.get_config_string("claude.api_key", "")
if api_key == "" then
on_done("缺少 API Key", nil)
return
end
local headers = {
["x-api-key"] = api_key, -- 关键:Claude 用 x-api-key,不是 Authorization
["anthropic-version"] = "2023-06-01",
["content-type"] = "application/json",
}
local system_prompt =
"你是 RPG 任务生成器。只输出一个 JSON 对象," ..
"禁止任何解释文字或 Markdown 代码块。字段:" ..
"id,title,description,objectives(数组,含type/target/count)," ..
"reward(gold/exp/item),next。gold<=500,exp<=300。"
local body = json.encode({
model = "claude-sonnet-4-6",
max_tokens = 1024,
system = system_prompt,
messages = {
{ role = "user", content = prompt }
}
})
-- Defold: http.request(url, method, callback, headers, post_data, options)
http.request(API_URL, "POST", function(self, id, response)
if response.status ~= 200 then
on_done("HTTP " .. tostring(response.status) .. ": " .. tostring(response.response), nil)
return
end
on_done(nil, response.response) -- 原始 JSON 字符串,交给上层校验
end, headers, body, { timeout = 30 })
end
return M
两个容易被忽略的点:Claude 鉴权头是 x-api-key(写成 Authorization: Bearer 必 401);options 里的 timeout = 30 是秒,弱网下不设超时回调可能一直不回来。
分步骤实操:从玩家输入到任务渲染进 UI
整体链路:玩家点击 NPC → 收集当前场景上下文拼成 prompt → 调 Claude → 校验 JSON → 渲染到 GUI。下面是挂在 collection 上的 quest.script:
-- /main/quest.script
local claude = require("scripts.claude")
local function extract_text(raw)
-- Claude 返回体结构:{ content = { { type="text", text="..." } } }
local ok, decoded = pcall(json.decode, raw)
if not ok then return nil, "外层 JSON decode 失败" end
local blocks = decoded.content
if not blocks or not blocks[1] or not blocks[1].text then
return nil, "返回结构缺少 content[1].text"
end
return blocks[1].text, nil
end
function init(self)
msg.post(".", "acquire_input_focus")
end
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed then
local prompt = "场景:迷雾森林入口,玩家等级5,为其生成一个收集类支线任务。"
print("[quest] 请求生成任务...")
claude.gen_quest(prompt, function(err, raw)
if err then
print("[quest] 生成失败: " .. err)
return
end
local text, e1 = extract_text(raw)
if not text then
print("[quest] " .. e1)
return
end
self.pending = text -- 交给下一节的校验函数
msg.post("#", "validate_quest")
end)
end
end
渲染部分我用 GUI 场景,把 title 和 description 写进两个文本节点:
local function render_quest(q)
gui.set_text(gui.get_node("quest_title"), q.title)
gui.set_text(gui.get_node("quest_desc"), q.description)
local r = q.reward
gui.set_text(gui.get_node("quest_reward"),
string.format("奖励:金币 %d / 经验 %d", r.gold, r.exp))
end
加入基础校验:处理非 JSON、字段缺失、奖励越界
模型输出不可全信。我在渲染前加了一层 validate,把三类问题拦下:非法 JSON、必填字段缺失、奖励数值越界。
local function validate_quest(text)
local ok, q = pcall(json.decode, text)
if not ok or type(q) ~= "table" then
return nil, "内层任务 JSON 解析失败"
end
-- 必填字段
for _, key in ipairs({"id", "title", "description", "reward"}) do
if q[key] == nil then
return nil, "缺少字段: " .. key
end
end
if type(q.objectives) ~= "table" or #q.objectives == 0 then
return nil, "objectives 为空"
end
-- 奖励越界钳制(不直接丢弃,钳到上限更耐用)
q.reward.gold = math.min(math.max(q.reward.gold or 0, 0), 500)
q.reward.exp = math.min(math.max(q.reward.exp or 0, 0), 300)
return q, nil
end
实测中约每 20 次会有 1 次模型多包了一层解释文字,靠 system prompt 的“只输出 JSON”约束 + pcall 兜底,基本不会让游戏崩。奖励我选择钳制而非丢弃,容错更高。
真实踩坑与报错处理
1. 401 鉴权失败
第一次跑直接 HTTP 401。原因是我按习惯写了 Authorization = "Bearer sk-..."。Claude 用的是 x-api-key 头,且必须带 anthropic-version,两者缺一都会 401 或 400。改成上文的 headers 后正常。
2. HTML5 构建下的 CORS 拦截
桌面/移动原生构建请求正常,但我 bundle 成 HTML5 在浏览器里跑时,控制台报跨域错误——浏览器不允许网页直连 api.anthropic.com。这是浏览器安全策略,Defold 无法绕过。解决办法只有一个:搭一个自己的中转后端,网页请求你的服务器,服务器再转发 Claude。顺便也解决了密钥不能进前端的问题。
3. 网络超时无回调
弱网下请求像“卡死”。根因是没设超时。Defold 的 http.request 第六个参数 options 传 { timeout = 30 },超时后 response.status 会是 0 或 -1,据此走失败分支重试即可。
4. Lua json.decode 报错
报错通常来自两层:一是把 Claude整个响应体当任务 JSON 去 decode(其实要先取 content[1].text);二是模型输出夹带了非 JSON 文字。两处都用 pcall 包住 json.decode,失败就打日志重试,别让异常冒泡。
5. 中文“乱码”其实是缺字形
任务标题渲染成一堆方框,我一度以为是编码问题。抓日志 print(q.title) 发现字符串本身是正确的 UTF-8——问题在 Defold 的字体资源没包含中文字形。解决:在 .font 里换成含 CJK 的字体(如思源黑体),并把用到的汉字加入字符集缓存。UTF-8 数据没错,是 GUI 字体的锅。
小结 + 可复现完整代码
到这里,一条“玩家触发 → Claude 生成任务 JSON → 校验钳制 → 渲染进 GUI”的完整链路就跑通了。核心就三块:claude.lua 封装带 x-api-key 的请求、quest.script 做取值与校验、GUI 用含中文字形的字体渲染。真正的护栏在校验层和密钥不落客户端这两点,其余都是水到渠成。
下一步可以做的增强:给 gen_quest 加失败重试与指数退避;把 prompt 里的场景上下文改成从当前地图动态收集;正式上线务必把请求改走自建后端代理。上文已给出全部脚本(claude.lua、quest.script、校验与渲染函数、system prompt 模板),可按步骤复制到本地 Defold 1.9.x 项目中运行。
