基于Gradio和JSON的互动叙事游戏开发:末日逃生列车Demo “末日逃生列车选择你的专属无限续美食车厢”——这个标题听起来像一个桌游设定也像一个末世题材的互动叙事游戏。玩家要做的事很直接在一条高速行驶的逃生列车上从多节美食车厢里选择一节进入之后获得无限续供的食物但每节车厢都有自己的规则和代价选择不同结局就不同。这篇文章不讨论美术和动画而是直接把这个创意做成一个可以本地运行的互动游戏 Demo。整体方案是 Python Gradio用一个 JSON 文件驱动全部剧情不依赖 GPU不装游戏引擎普通笔记本就能跑。文章会覆盖剧本数据结构、状态管理、Web 界面、本地功能验证、LLM 动态剧情扩展以及常见问题排查。适合的读者有三类想做互动游戏原型开发的人想低成本验证剧情玩法的内容策划以及想学习 Gradio 数据驱动交互应用的技术学习者。读完你可以亲手跑通一个“选择车厢 - 触发剧情 - 走向结局”的完整小游戏。1. 玩法定位与核心能力速览这个项目的本质不是写一个大型游戏而是用最少的技术成本把一个偏创意的互动玩法快速变成可运行的原型方便验证“无限续供美食车厢”这个设定是否真的能产生足够的选择张力。项目本身是纯本地的脚本应用通过 Gradio 提供网页交互界面不涉及客户端安装包也不需要额外的服务器。核心能力速览如下能力项说明项目类型互动叙事游戏 Demo玩法概念转可运行脚本运行环境本地 Python 环境CPU 即可运行无需 GPU主要功能剧情分支选择、车厢场景切换、结局判定、剧本重开启动方式命令行启动 Gradio Web 界面剧本扩展JSON 文件驱动可直接改剧情文本接口扩展预留兼容 OpenAI 格式的 LLM 接口可动态生成剧情批量能力可编写脚本批量生成车厢描述但需要自建生成流程适合场景游戏原型开发、互动式内容创作、写作辅助、玩法验证整体实现思路是数据驱动。游戏剧情全部放在story.json里Python 代码只负责读取 JSON、渲染文本、接收选择、跳转节点。这样剧情策划和技术实现可以完全解耦改内容不用改代码代码也不用为了内容频繁调整。2. 适用场景与使用边界这类互动叙事游戏最直接的适用场景有三个。第一个是游戏玩法原型验证。如果你脑子里有“逃生列车 资源选择 美食主题”的玩法但又不想一上来就碰 Unity 或 Unreal先用 Python 脚本把剧情分支、选择和结局跑通成本最低。玩法验证通常只需要解决一个问题这个选择是否让玩家产生继续玩下去的欲望而不是视觉表现。用本文的方法半小时内就能跑出第一个可交互的剧情分支。第二个是互动式内容创作。现在很多内容团队会做“互动图文”“沉浸式选择”类内容使用场景包括公众号互动推文、在线互动小说、展厅触摸屏小游戏。用 JSON 管理剧本文本用 Gradio 输出网页界面对内容团队来说修改成本很低不熟悉代码的人也能通过改 JSON 来调整剧情。内容文案、分支关系、结局文本都集中在一个文件里任何人都能上手。第三个是写作辅助和灵感测试。写作者可以先搭好分支结构再用不同的文案填充每个节点快速比较不同分支的阅读节奏。比如同一个“甜品车厢”主题可以写出“治愈结局”“反转结局”“黑色幽默结局”三种版本放到同一个剧本文件里测试读者反馈。使用边界也要讲清楚。这个 Demo 适合做原型并不适合直接做成面向大量玩家的在线产品因为 Gradio 默认界面偏工具化缺少商业游戏的动效、音乐和存档系统如果需要上线通常要结合前端引擎重写。另外一旦接入 LLM 动态生成剧情输出文本可能包含不稳定内容所有自动生成的对白在公开前必须人工审核。末日题材本身不算敏感但涉及求生、资源分配、群体行为等描写时创作者要注意把握情绪导向不要在内容中渲染过度消极或鼓励极端行为的表达。这个项目本身只是本地脚本工具不涉及隐私收集和外部付费链路风险很低。但如果后续把界面服务暴露到公网需要对访问来源做限制防止被恶意刷接口。推荐只绑定127.0.0.1只在本地访问。3. 环境准备与前置条件操作系统方面Windows、macOS、Linux 都可以只要 Python 能正常安装。推荐使用 Python 3.10 及以上版本因为 Gradio 新版对低版本 Python 适配不佳太老的 Python 可能会出现组件兼容问题。环境准备清单如下Python 3.10 或更高版本pip 包管理工具用于存放项目文件的目录例如doomsday_train/浏览器推荐 Chrome 或 Edge可选一个兼容 OpenAI 格式的 LLM 服务地址用于动态剧情扩展这个项目对硬件几乎不构成压力。Gradio 本身是 CPU 推理的 Web 框架渲染一层简单页面不需要独立显卡。后续如果做更复杂的动效才需要考虑 GPU 或前端重构。磁盘空间的占用主要来自 Gradio 及其依赖包体积在几百 MB 量级具体以 pip 安装时的实际输出为准。端口方面默认会使用 7860。如果端口被占用可以在启动参数中指定其他端口例如demo.launch(server_name127.0.0.1, server_port7861)。创建项目目录并安装依赖mkdir doomsday_train cd doomsday_train pip install gradio requests安装完成后可以用下面这条命令检查 Gradio 是否正常python -c import gradio; print(gradio.__version__)如果报错说明 gradio 没有正确安装先重启终端再试。如果重启后仍然失败优先检查 Python 版本和 pip 是否属于当前 Python 环境尤其是在 Windows 上安装了多个 Python 版本的情况。4. 游戏数据与状态设计互动叙事游戏的核心是数据驱动。把剧情从代码里抽出来放到 JSON 文件里定义好节点结构和选择关系后续就可以不碰代码直接改剧情。这也是这个 Demo 最重要的设计决定。节点结构推荐这样设计{ title: 末日逃生列车, start_node: enter_train, nodes: { enter_train: { type: scene, text: 末日第47天你挤进最后一班逃生列车。乘务员递来一张卡片你可以选择进入任一美食车厢之后每节车厢都会为你无限续供。问题是你只能选择一节车厢。, choices: [ { text: 进入无限速食面包车厢, next: bread_car }, { text: 进入无限火锅汤底车厢, next: hotpot_car }, { text: 进入无限甜品车厢, next: dessert_car } ] }, bread_car: { type: ending, text: 你选择了无限速食面包车厢。车厢里堆满面包足够吃几年。但三天后列车燃料耗尽所有人开始把面包做成燃料列车重新启动却也在浓烟中偏离了轨道。, choices: [ { text: 重新开始, next: enter_train } ] }, hotpot_car: { type: ending, text: 你选择了无限火锅汤底车厢。热辣的汤底让车厢温度不断升高列车触发高温警报但你带着火锅底料逃到了终点站成为队伍里的食物支柱。, choices: [ { text: 重新开始, next: enter_train } ] }, dessert_car: { type: ending, text: 你选择了无限甜品车厢。甜食让车厢里的人暂时忘记末日。列车停靠后你把甜品分给了沿途村镇换来了燃料和药品也换来了通往下一个安全区的机会。, choices: [ { text: 重新开始, next: enter_train } ] } } }这个文件包含了三个关键字段。title是游戏标题start_node标记初始节点nodes是全部剧情节点。每个节点有type、text、choices三个字段其中type用于标记scene中间场景和ending结局text是当前场景的文本描述choices是玩家可选的分支列表。每个 choice 由text和next组成next指向下一个节点 ID。这种设计的好处是所有剧情依赖关系都在 JSON 中可见改剧情只需要改文本和跳转关系不需要理解 Python 代码。后续如果要加“背包”或“资源点”只需要在节点里增加一个resources字段。状态管理也很简单。游戏运行时只需要记住当前节点 ID。因为路线相对线性没有复杂的状态堆栈用一个全局字典保存当前节点就够了。后续如果要做多结局解锁、资源统计再引入复杂状态管理也不迟最初版本不需要过度设计。5. 核心代码实现从剧本到可玩界面5.1 加载剧本并渲染节点先封装一个加载函数负责把story.json读入内存import json import gradio as gr def load_story(pathstory.json): with open(path, r, encodingutf-8) as f: return json.load(f) story load_story(story.json)接下来定义节点渲染函数。渲染函数要做三件事更新页面上的剧情文本刷新单选按钮的选择项并把当前节点 ID 保存到全局状态。state {node: story[start_node]} def render_node(node_id): node story[nodes][node_id] state[node] node_id choices [choice[text] for choice in node.get(choices, [])] return node[text], choices当玩家点击“确定”后把选择结果传入选择处理函数。处理函数根据当前节点找到对应的 choice再跳转到 next 节点。def on_choose(selected): node story[nodes][state[node]] for choice in node.get(choices, []): if choice[text] selected: return render_node(choice[next]) return 选择无效请重试, []这里需要注意Gradio 的 Radio 组件返回值是用户选中的选项文本所以直接用文本匹配 choice 里的 text 字段。5.2 构建 Gradio 界面界面部分使用 Gradio Blocks。页面包含标题、剧情文本、单选按钮、确定按钮和重新开始按钮。with gr.Blocks(title末日逃生列车) as demo: gr.Markdown(## 末日逃生列车选择你的专属无限续美食车厢) story_text gr.Markdown() choice_radio gr.Radio(label请做出你的选择) confirm_btn gr.Button(确定) restart_btn gr.Button(重新开始) def start(): return render_node(story[start_node]) confirm_btn.click(on_choose, inputschoice_radio, outputs[story_text, choice_radio]) restart_btn.click(start, inputs[], outputs[story_text, choice_radio]) demo.load(start, inputs[], outputs[story_text, choice_radio])启动入口if __name__ __main__: demo.launch(server_name127.0.0.1, server_port7860)把以上代码保存为app.py。最终的目录结构如下doomsday_train/ ├── app.py └── story.json完整的app.py代码如下import json import gradio as gr def load_story(pathstory.json): with open(path, r, encodingutf-8) as f: return json.load(f) story load_story(story.json) state {node: story[start_node]} def render_node(node_id): node story[nodes][node_id] state[node] node_id choices [choice[text] for choice in node.get(choices, [])] return node[text], choices def on_choose(selected): node story[nodes][state[node]] for choice in node.get(choices, []): if choice[text] selected: return render_node(choice[next]) return 选择无效请重试, [] with gr.Blocks(title末日逃生列车) as demo: gr.Markdown(## 末日逃生列车选择你的专属无限续美食车厢) story_text gr.Markdown() choice_radio gr.Radio(label请做出你的选择) confirm_btn gr.Button(确定) restart_btn gr.Button(重新开始) def start(): return render_node(story[start_node]) confirm_btn.click(on_choose, inputschoice_radio, outputs[story_text, choice_radio]) restart_btn.click(start, inputs[], outputs[story_text, choice_radio]) demo.load(start, inputs[], outputs[story_text, choice_radio]) if __name__ __main__: demo.launch(server_name127.0.0.1, server_port7860)这段代码不到 60 行已经把“选择 - 跳转”的核心循环跑通了。从工程角度看这个结构保留了很大的扩展空间后面加资源统计、加条件分支、加随机事件都不会伤筋动骨。6. 本地运行与功能验证6.1 启动游戏在项目目录下执行python app.py启动成功后终端会出现类似下面的地址Running on local URL: http://127.0.0.1:7860浏览器打开这个地址就能看到“末日逃生列车选择你的专属无限续美食车厢”的页面。整个页面只有三部分剧情文本、单选选项、操作按钮非常简洁但已经具备完整交互能力。6.2 验证一条完整剧情分支第一次验证建议走最短路径页面加载后看到初始剧情文本和三个选项。选择“进入无限速食面包车厢”。点击“确定”。页面切换到面包车厢结局文本选项变为“重新开始”。点击“重新开始”回到初始场景。如果第 2 步无法选中选项检查 radio 组件是否被正确渲染常见原因是 Gradio 版本不兼容升级或降级后重试。如果第 4 步选项没有刷新说明on_choose里返回值没有正确输出到 radio检查outputs是否同时包含了story_text和choice_radio。接下来跑一条带两个节点的分支验证。修改story.json把第二个场景做成中间节点再让玩家选择一次。例如把hotpot_car改成场景节点指向hotpot_endhotpot_car: { type: scene, text: 火锅汤底车厢温度过高排风系统满负荷运转。你注意到角落有一扇标注‘物资室’的小门。, choices: [ { text: 进入物资室, next: storage_room }, { text: 留在火锅车厢, next: hotpot_end } ] }加入storage_room节点后走一遍完整的两级分支流程确认每次跳转都正确。6.3 修改与扩展剧本改剧情只需要两步打开story.json修改或新增节点保存后刷新浏览器点击重新开始。不需要重启 Python 进程因为每次刷新都会从新页面加载但app.py里的story是在进程启动时加载的。如果希望不重启进程也能热更新可以把load_story放到start()函数里每次重开时重新读文件def start(): global story story load_story(story.json) return render_node(story[start_node])这样每次点击重新开始都会重新读取story.json适合内容调试阶段。内容策划可以一边改剧本一边刷新页面不需要技术人员介入。7. 扩展接入 LLM API 实现动态剧情当 JSON 剧本无法满足大量内容生成需求时可以接入兼容 OpenAI 格式的 LLM API把剧情生成从“手写”变成“按提示词生成”。这里给出一个通用调用模板假设你的本地 LLM 服务或远程 API 地址为base_url模型名为modelimport requests def call_llm(prompt, api_key, base_urlhttp://127.0.0.1:8000/v1, modelqwen2.5): url f{base_url}/chat/completions headers {Authorization: fBearer {api_key}} payload { model: model, messages: [ {role: system, content: 你是一个末日题材互动叙事游戏编剧。}, {role: user, content: prompt} ], temperature: 0.8, max_tokens: 300 } response requests.post(url, jsonpayload, headersheaders, timeout60) data response.json() return data[choices][0][message][content] if __name__ __main__: print(call_llm(请生成一节‘无限罐头车厢’的进入剧情包含三个选择分支返回 JSON 格式。))这个模板可以放到生成脚本里批量产出节点文本。但 LLM 生成的 JSON 不保证完全合法建议生成后先用一个校验脚本做 JSON 解析解析失败就重新生成或人工修正。def is_valid_json(text): try: json.loads(text) return True except json.JSONDecodeError: return False批量生成时准备一个节点 ID 列表和提示词模板逐条调用 API把结果写入新的故事文件。要注意控制请求频率避免接口超时。接入 LLM 后所有生成内容都要人工审核尤其涉及末日、资源分配、群众行为描写时要避免出现暴力美化或消极引导。8. 资源占用与运行性能观察这个 Demo 对资源的占用很低核心压力来自 Gradio 页面本身和 Python 进程而不是模型推理。没有 GPU 需求任何能运行 Python 的电脑都可以跑。观察性能时可以注意以下几点。终端会输出 Gradio 的启动日志包含当前 IP、端口和运行时间。如果页面响应慢先在终端看是否有请求超时日志。Gradio 默认使用 7860 端口如果电脑上有其他项目占用该端口启动时会直接报错换端口即可。剧情文本越长页面渲染越慢但实际上只要文本量在几千字以内基本无感。真正需要关注的是 LLM 接口的响应时间如果接口响应超过 30 秒前端会一直等待需要给 requests 增加超时参数并在页面侧增加 loading 状态。内存方面Python 进程通常占用几十到几百 MB取决于剧情节点数量。如果担心内存可以记录每次请求前后的占用再决定是否需要做进程内缓存。更稳妥的判断是这个脚本不是性能瓶颈瓶颈通常在 LLM 服务端或网络。对于原型项目性能优化可以放到后面先把玩法和内容确认好再说。9. 常见问题与排查方法问题现象可能原因排查方式解决方案启动时提示 gradio 未安装依赖没有安装成功检查 pip list执行 pip install gradio requests再重启终端浏览器打开 127.0.0.1:7860 打不开端口被占用或服务未启动查看终端日志和端口占用换端口或在启动参数中指定其他端口或关闭占用进程页面显示正常但选项无法点击Gradio 版本不兼容查看 gradio 版本升级或降级 Gradio 到兼容版本点击确定后剧情不跳转返回值没有传给 radio检查 on_choose 的 outputs确保 outputs 同时包含 story_text 和 choice_radio剧情文本出现乱码控制台编码或文件编码问题检查文件保存编码确保 story.json 以 UTF-8 保存读取时使用 encodingutf-8JSON 文件解析失败引号或逗号写错使用 JSON 校验工具排查修正 story.json 格式接入 LLM 后返回格式错误模型输出不是合法 JSON先打印原始返回文本增加 JSON 校验失败后重试或人工修正重新开始后还是旧剧情story 变量只加载了一次检查代码结构在 start() 里重新 load_story()如果出现 Python 模块装错环境的情况建议在项目目录下单独创建虚拟环境把依赖隔离在项目内部避免污染全局环境。虚拟环境创建方式python -m venv venvWindows 下激活venv\Scripts\activatemacOS 和 Linux 下激活source venv/bin/activate激活后再执行 pip install就能避免多个项目依赖冲突。10. 最佳实践与下一步先说最容易踩的坑。第一次运行前一定要确认story.json和app.py在同一个目录下否则load_story找不到文件。Gradio 安装完成后如果还是报模块不存在重启终端再试。再给一套稳定的开发流程。先在 JSON 里写两条分支跑通后再扩展成 6 到 8 个节点避免一开始写太多节点导致排错困难。每次改动story.json后用 JSON 校验工具检查格式再刷新页面验证。批量生成 LLM 内容时把生成结果保存到独立文件不要直接覆盖正式剧本审核通过后再合并。这个项目的下一步有三个方向。第一个方向是增加状态系统比如记录玩家的“能量值”“资源数”在节点里加入条件判断让不同状态值触发不同分支。第二个方向是把 Gradio 换成正向的 Web 前端后端保留节点引擎做成真正可发布的互动小说页面。第三个方向是接入更多 LLM 生成能力用批量脚本自动生成“早餐车厢”“午餐车厢”“夜宵车厢”等更多节点形成更完整的无限续美食主题剧本。回到标题本身“选择你的专属无限续美食车厢”这个玩法最值得验证的地方是“无限供应”这个设定能否在末日背景下制造出足够强的选择张力。先用本文的脚本跑通基础选择再逐步加入资源约束、角色和随机事件这个创意就能从一行标题变成可玩的交互原型。想要快速体验建议先收藏本文把第 5 节的app.py和第 4 节的故事 JSON 复制到本地跑通一条结局分支再考虑扩展功能。