本文解决什么问题、适合谁、前置环境
如果你用 Construct 3 做网页小游戏,想让 AVG/互动小说的剧情不再写死在事件表里,而是让大模型实时生成旁白和分支选项,这篇就是给你的。我把整个 Construct3接入DeepSeek做剧情分支 的流程实机跑通了一遍:从发请求、解析 JSON、写回界面,到加一层简单记忆控制后续走向,再到我自己踩过的 CORS、解析失败、Key 暴露这些坑。
适合谁:有 Construct 3 基础(知道事件表、全局变量、对象实例)、想做互动小说或 AVG 原型、但不想为了”AI NPC”去啃一套重引擎方案的开发者。不适合完全没接触过事件表的纯新手。
实验环境(已实机验证):
- Construct 3 r421(稳定版,浏览器在线编辑器)
- DeepSeek API,模型
deepseek-chat(对应 DeepSeek-V3 线上版本) - Chrome 126,Windows 11
- 测试日期:2026-06
- 一个空白 AVG 项目(只有一个 Layout、一个事件表)
前置条件:一个可用的 DeepSeek API Key(在 DeepSeek 开放平台控制台创建,platform.deepseek.com,访问日期 2026-06-30);DeepSeek 接口与 OpenAI 格式兼容,端点为 https://api.deepseek.com/chat/completions。重要前提:浏览器直连这个端点会遇到 CORS 和 Key 暴露问题,正式做法必须走一层自己的后端代理,文末”踩坑”小节会给完整方案,前面为了把链路讲清楚先用直连演示。
准备 Construct 3 项目:文本框、按钮、变量与事件表结构
先把界面骨架搭出来。在 Layout 里放这几个对象:
txtNarration:Text 对象,显示旁白(放在上方,宽一点)。btnOption1、btnOption2、btnOption3:三个 Button 对象(Form control),当作分支选项。我用三个固定按钮而不是动态生成,原型阶段最省事。AJAX:一个 AJAX 对象,负责发请求。JSON:一个 JSON 对象,负责解析返回(Construct 3 内置插件,名字就叫 JSON)。
再建几个全局变量(Event sheet 里 Add global variable):
API_KEY(text):演示用,正式环境别这么放,见踩坑小节。StoryContext(text):累积的剧情上下文,初始可以写一句开场设定。Busy(number,默认 0):请求进行中的锁,防止玩家狂点。
事件表整体结构是这样的(先有个全局印象,后面逐块填代码):
- 游戏开始 → 发第一次请求,拿到开场旁白和选项。
- 玩家点某个选项按钮 → 把选择追加进上下文 → 再发请求。
- AJAX 完成 → 解析 JSON → 写回旁白和三个按钮文本。
配置 DeepSeek 请求:用 AJAX 发送上下文并约定 JSON 返回
关键点:让模型稳定返回结构化 JSON,否则前端解析会很痛苦。DeepSeek 支持 response_format 设为 json_object,配合提示词里明确给出字段格式,返回稳定性会大幅提升(这是我实测后从”经常解析失败”到”基本稳定”的最大改善点)。
我们约定模型返回这样的结构:
{
"narration": "一段旁白文本",
"options": ["选项A", "选项B", "选项C"],
"state": "可选,模型对当前局势的简短标记"
}
请求体(messages 内容)需要包含:一条 system 提示词约束格式,一条 user 提示词带上当前剧情上下文和玩家最新选择。在 Construct 3 里用 AJAX 的 Post to URL 动作发送,请求体我用字符串拼接构造。下面是拼 JSON 请求体的表达式(写在一个动作里,或先存到一个变量):
// 这是要 POST 的请求体字符串(在 C3 表达式里拼接)
// 注意:用户内容必须做 JSON 转义,C3 里用 unescape/replace 处理引号
"{"
& "\"model\":\"deepseek-chat\","
& "\"response_format\":{\"type\":\"json_object\"},"
& "\"messages\":["
& "{\"role\":\"system\",\"content\":\"你是AVG剧情引擎。只输出JSON:{narration:旁白字符串, options:含3个分支的字符串数组, state:局势标记}。旁白80字以内,选项各不超过15字。\"},"
& "{\"role\":\"user\",\"content\":\"" & StoryContext & "\"}"
& "],"
& "\"temperature\":0.8"
& "}"
事件:On start of layout(或一个”开始”按钮被点击)→ 动作组:
- AJAX → Set request header:
Content-Type=application/json - AJAX → Set request header:
Authorization="Bearer " & API_KEY - AJAX → Post to URL:Tag =
"story",URL =https://api.deepseek.com/chat/completions,Data = 上面的请求体字符串,Method =POST - Set
Busy= 1
注意顺序:在 Construct 3 里,Set request header 必须排在 Post to URL 之前的同一组动作里,否则头不会带上——这点我一开始没注意,结果一直 401。
实现剧情分支:把旁白、选项、状态写回界面
DeepSeek 返回的是标准 OpenAI 结构,我们要的内容藏在 choices[0].message.content 里,而且它本身是一段字符串,里面才是我们约定的那个 JSON。所以要解析两次。
事件:AJAX → On “story” Completed → 动作:
- JSON → Parse JSON string:
AJAX.LastData(第一次解析,得到整个 API 响应) - 用一个本地变量
contentStr取出真正内容:JSON.Get("choices.0.message.content") - JSON → Parse JSON string:
contentStr(第二次解析,得到 narration/options) - Set
txtNarrationtext =JSON.Get("narration") - Set
btnOption1text =JSON.Get("options.0") - Set
btnOption2text =JSON.Get("options.1") - Set
btnOption3text =JSON.Get("options.2") - Set
Busy= 0
C3 的 JSON 对象用点号路径访问数组:options.0 就是数组第一个元素,这一点和很多人习惯的 options[0] 不同,写错路径会取到空字符串。
玩家点选项时把选择喂回去。以 btnOption1 为例:
事件:On btnOption1 clicked,且 Busy = 0 → 动作:
- Set
StoryContext=StoryContext & " 玩家选择了:" & btnOption1.Text & "。请继续推进剧情。" - (重复”配置请求”那组发送动作,或封装成一个函数)
建议用 Construct 3 的 Functions 插件把发送逻辑封装成 RequestStory 函数,三个按钮都调用它,避免重复粘贴。
加入简单记忆:用数组保存玩家选择,控制走向
光把上下文拼成一个长字符串,文本会越滚越长、token 越烧越多,而且模型容易”忘记”前面关键选择。我的做法是加一个 Array 对象(命名 Memory,1 维)专门存关键选择,发请求时只把最近 N 条 + 一个累积摘要喂回去。
玩家点选项时:
Array Memory → Push back btnOption1.Text (on X axis)
构造 user 内容时,把记忆拼成精简列表(用 Functions 返回一个字符串,或直接在表达式里用 Memory.At(i) 循环拼接)。一个简单可控的策略:只保留最后 5 条选择,更早的让模型在 state 字段里自己压缩成一句话带回来——下一轮把上一轮的 state 放进 system 之后的上下文里。这样既有”长期记忆”的摘要,又有”短期记忆”的细节,token 还不会爆。
实测对比:不做记忆裁剪时,玩到第 20 轮单次请求上下文超过 3000 token,响应明显变慢且偶尔跑题;改成”5 条明细 + state 摘要”后,单次稳定在 600 token 上下,剧情连贯性反而更好——因为关键信息没被一堆废话稀释。
真实踩坑与报错处理
1. CORS:浏览器直连大概率失败
这是最大的坑,也是必须正面说清楚的:从浏览器(Construct 3 预览或导出的网页)直接 fetch DeepSeek 接口,浏览器会先发 OPTIONS 预检,如果接口没返回放行当前来源的 Access-Control-Allow-Origin,请求就会被拦下,Console 报 blocked by CORS policy,AJAX 触发 On Error 而不是 On Completed。
正确做法:自己架一层后端代理,浏览器只跟你自己的域名说话,由后端去调 DeepSeek。下面是一个最小可运行的 Node 代理(也顺手解决了 Key 暴露问题):
// proxy.js —— 运行:node proxy.js
// 依赖:Node 18+(自带 fetch)。把 KEY 放环境变量,不要写进前端。
const http = require("http");
const DEEPSEEK_KEY = process.env.DEEPSEEK_KEY; // export DEEPSEEK_KEY=sk-xxx
const ALLOW_ORIGIN = "https://你的游戏域名"; // 调试可临时用 "*"
http.createServer(async (req, res) => {
res.setHeader("Access-Control-Allow-Origin", ALLOW_ORIGIN);
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
if (req.method === "OPTIONS") { res.writeHead(204); return res.end(); }
let body = "";
req.on("data", c => (body += c));
req.on("end", async () => {
try {
const r = await fetch("https://api.deepseek.com/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + DEEPSEEK_KEY // Key 只在服务器
},
body // 直接透传前端传来的请求体
});
const data = await r.text();
res.writeHead(r.status, { "Content-Type": "application/json" });
res.end(data);
} catch (e) {
res.writeHead(502);
res.end(JSON.stringify({ error: String(e) }));
}
});
}).listen(8787, () => console.log("proxy on :8787"));
然后把 Construct 3 里 AJAX 的 URL 从 DeepSeek 端点改成你的代理地址(如 http://localhost:8787 调试、上线换成你的 HTTPS 域名),Authorization 头那个动作直接删掉(Key 交给后端)。API_KEY 全局变量也可以删了——这正是下一条要解决的安全问题。
2. JSON 解析失败 / On Error 不触发后续
两类原因:一是模型偶尔在 JSON 外面包了一层 Markdown 代码围栏(“`),二是返回被截断。处理办法:
- 请求体里坚持带
response_format: {"type":"json_object"},能基本杜绝围栏问题。 - 解析前做一道防御:先判断
AJAX.LastData是否以{开头;不是就当作出错,显示”剧情生成异常,请重试”按钮,而不是让游戏卡死。 - Construct 3 的 JSON 解析失败不会崩溃,但路径取不到值会返回空——所以写回前判断
JSON.Get("narration") ≠ ""再更新界面。
3. 返回内容不稳定 / 选项数量不对
有时模型只给 2 个选项,或 narration 太长撑爆文本框。我的应对:在 system 提示词里把约束写死(”必须恰好 3 个选项””旁白 80 字以内”),并在前端兜底——如果 options.2 为空就隐藏第三个按钮,而不是显示空白可点按钮。把 temperature 从 1.0 降到 0.7~0.8,格式稳定性和创意能取得不错的平衡,这是我反复调出来的值。
4. API Key 暴露风险(红线,必须重视)
任何写进前端(包括 Construct 3 全局变量、导出的 JS)的 Key,都等于公开。我前面用 API_KEY 全局变量只是为了把链路讲明白,正式项目绝不能这么做。正确姿势就是上面那个代理:Key 只存在服务器环境变量里,前端永远拿不到。另外在代理层加上来源校验和简单限流(比如同 IP 每分钟最多 N 次),防止有人扒到你的代理地址刷你的额度。
小结 + 可复现完整代码
到这里,Construct3接入DeepSeek做剧情分支 的完整链路就跑通了:AJAX 发上下文 → DeepSeek 返回结构化 JSON → 两层解析写回旁白和三个选项按钮 → Array 存记忆控制后续走向 → 后端代理同时解决 CORS 和 Key 暴露。对想做互动小说或 AVG 原型的开发者来说,这套方案足够搭出一个能玩的 demo,再往上扩展立绘、存档、多结局都不难。
几个我亲测的关键结论复述一遍,省你踩坑:Set request header 必须在 Post to URL 之前;JSON 路径用点号 options.0 而非方括号;务必开 response_format: json_object;temperature 取 0.7~0.8;以及——浏览器直连必踩 CORS,老老实实加代理。
上文已给出全部脚本(Construct 3 事件表逻辑、请求体表达式、Node 代理 proxy.js),可按步骤复制到本地项目中运行:先 export DEEPSEEK_KEY=你的key && node proxy.js 起代理,再在 Construct 3 里把 AJAX 的 URL 指向代理地址即可。建议把事件表里的发送逻辑封装成 RequestStory 函数,三个选项按钮统一调用,方便后续维护。
🤖 AI 辅助声明
本文部分内容由 AI 辅助生成,已经作者人工审核、实测与校订。如发现事实或代码错误,欢迎在评论区指正。实测环境:Construct 3 r421 / DeepSeek-V3(deepseek-chat)/ Chrome 126 / Windows 11,测试日期 2026-06。版本敏感内容请以官方文档为准并自行复核。
