标签: Claude API

  • Defold接入Claude做任务系统教程

    Defold接入Claude做任务系统教程

    本文解决什么问题、适合谁、前置环境

    我最近在给一款 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 引擎内置了 httpjson 两个模块,无需装任何第三方库就能发 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 场景,把 titledescription 写进两个文本节点:

    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.luaquest.script、校验与渲染函数、system prompt 模板),可按步骤复制到本地 Defold 1.9.x 项目中运行。

    🤖 本文部分内容由 AI 辅助生成,已经作者人工审核、实测与校订。实测环境:macOS 14.5 / Defold 1.9.4 / Claude API(anthropic-version 2023-06-01,claude-sonnet-4-6)/ 2026-06;文中 Lua 代码均在本地 Defold 项目实机跑通,401、CORS、超时、JSON decode、中文缺字形等报错为真实复现并给出处理。如发现事实或代码错误,欢迎在评论区指正。