在2025年这个时间点上,“AI游戏”已经不是一个蹭热度的概念,而是真正能落地、能玩起来的东西。我这一篇不讲虚的,直接把我从零开始、用开源引擎配合大模型接口做出一款可运行AI游戏的全过程拆开,从选型、环境配置、核心代码、踩坑链路到体验优化,全部过一遍。文章会很长,但每一步都可以照着抄,适合有基础编程概念、但没碰过AI游戏开发的人,也适合已经在做传统游戏、想把手头项目接入AI能力的开发者。
1. 先想清楚:AI游戏到底玩的是什么
1.1 我理解的“AI原生游戏”
大家常说的AI游戏大概可以分两类。一类是把AI当作辅助工具,比如用AI生成美术素材、写剧情文本、做程序代码,游戏本身还是传统的玩法驱动。另一类是AI作为游戏的核心运行时机制,NPC不是读预设脚本,而是每次都用大模型的推理能力现场生成对话和行动;剧情不是写死的分支树,而是根据玩家输入实时演化;甚至游戏里的系统规则都会由AI动态解释。
我做的是后者,因为后者才配叫“AI游戏开发”。一个角色能不能记住你半小时前的决定,一个剧情能不能在你连续挑衅NPC之后真的改变世界走向,这些靠传统脚本要写死几百个分支,而用大模型只需要给它一个“人设”和“记忆池”,剩下交给你和它之间的对话。
这里要泼一盆冷水:AI游戏开发的门槛不在于“调用AI接口”,而在于“把AI输出变成稳定的游戏体验”。大模型返回的是文本,但游戏需要的是状态变更、数值增减、场景切换、任务更新。你要设计一套机制,把这些文本翻译成游戏世界里的“真实事件”,同时还要处理它偶尔跑偏、回复超时、输出格式不合法这些破事。
1.2 这条开发路线,天然适配这三类人
- 独立开发者:一个人就是一支团队,AI能帮你把文案、配音、部分美术、甚至玩法策划都包了,你专注系统架构和游戏性。
- 传统游戏开发者转型:Unity、Godot、Cocos的功力都还在,只是多学一个“跟大模型对话”的模块,边际成本很低,但产品想象力空间一下子打开了。
- 想拿AI游戏做作品集的新人:市面上大量AI玩法Demo都是套壳聊天,你如果能做出“AI行为真正影响游戏世界状态”的成品,在面试和社区里的辨识度完全不一样。
如果你连编程基础都没有,那这篇文章里的代码会让你吃力,建议你先补一遍任意一门编程语言的语法,再回来。
2. 引擎选型:Godot、Unity、Cocos,我最后选了谁
2.1 “免费商用”这一条,先帮我排掉了一半选项
做AI游戏大概率要走长线迭代,引擎的授权模式直接决定你以后会不会被“收割”。所以我在对比之前先设了两条硬门槛:必须免费商用,必须能导出到主流平台。
拿这三个引擎分别过一遍:
- Unity:个人版在收入低于10万美元时可以免费商用,但超过之后要订阅Pro,而且最近几年它的收费政策改来改去,社区怨气不小。它的优点是生态成熟,AI相关插件、资产商店资源最多;缺点是引擎本体越来越重,打开编辑器做小项目有种杀鸡用牛刀的感觉。
- Cocos:国内中小团队和微信小游戏的首选,免费商用政策清晰,对Web和小程序的支持是三个里最顺的,Creator编辑器上手也不难。但如果你要做的是3D PC游戏,或者要上Steam,Cocos的社区和生态就要弱一些了。
- Godot:完全开源,MIT协议,意思是你拿它做的游戏随便卖,一分钱授权费都不用交。编辑器轻量,GDScript语法像Python,对从零学引擎的人来说非常友好。它的短板是3D能力不如Unreal,商业化案例不如Unity多,但这两年在社区推动下进步非常快。
2.2 我实测下来的对比数据
我自己用同一个“AI对话+场景切换”的Demo在这三个引擎里各跑了一遍,感受如下:
| 对比维度 | Godot 4.x | Unity 2022+ | Cocos Creator 3.x |
|---|---|---|---|
| 安装包体量 | 约70MB,启动快 | 动辄几个GB | 中等,Web构建小 |
| 学习曲线 | 低,脚本像Python | 中高,C# + 复杂面板 | 低,TS/JS熟悉即可 |
| 导出目标 | PC/移动/Web全平台 | PC/移动/Web/主机 | Web/微信小游戏最强 |
| HTTP请求支持 | 内置HTTPClient | 需用UnityWebRequest | 内置XMLHttpRequest封装 |
| AI文本处理 | 字符串/GDScript字典操作顺手 | C#处理Json麻烦但可控 | 前端思维处理Json很舒服 |
| 开源与免费 | 完全开源,协议最宽松 | 有收入门槛 | 免费商用,政策清晰 |
2.3 我的选择:Godot,以及为什么
我最终选的Godot,核心原因就一句话:AI游戏的代码量集中在“逻辑和状态管理”上,而GDScript写这类代码的流畅度是三个引擎里最高的。
举个例子,AI返回的JSON里有一项action: "attack_guard",游戏要根据这个字符串去改变NPC的对玩家的好感度、卫兵的警戒值、场景BGM。在GDScript里,我可以直接写:
var action: String = ai_response.action match action: "attack_guard": player_state.add_reputation(-10) guard_state.alert_level = "hostile" bgm_player.switch_track("battle") "bribe_guard": player_state.add_gold(-50) guard_state.alert_level = "neutral" bgm_player.switch_track("ambient")这段逻辑在Unity里当然也能写,但要写一个匹配规则类、序列化类,还得处理MonoBehaviour生命周期。Godot的节点体系和_process回调让我能更快把“AI返回值”和“游戏世界状态”粘在一起,这就是选型上最大的效率优势。
另一个让我确定的点,是Godot 4.x的Web导出越来越靠谱。AI游戏天然适合放网页上给别人即点即玩,Godot导出HTML5的兼容性已经够用,给我省掉了大量平台适配工作。
3. AI能力接入:不要一上来就追Agent框架
3.1 三种常见的接入方式,我的一句话评价
现在打开知乎B站,满屏都是“AI Agent”“多智能体协作”的概念。但做游戏不是做企业级应用,我不建议新人一上来就上框架,先搞清楚三种接入方式的成本:
| 接入方式 | 实现难度 | 适用场景 | 我的评价 |
|---|---|---|---|
| 直接HTTP调用大模型API | 低 | 原型验证、小游戏 | 首选,所有复杂功能都从这里长出来 |
| 用现成Agent框架(如LangChain) | 中 | 需要工具调用、多步推理 | 先别碰,框架会替你抽象掉很多细节,但出了问题你根本不知道在哪层 |
| 自建Agent壳子 | 高 | 游戏需要长期记忆、复杂规划 | 等基础版跑通之后再用,属于进阶玩法 |
3.2 我在Godot里封装的大模型请求模块
我的项目并没有在引擎里塞任何Agent框架,而是自己写了一个AIClient单例,负责三件事:组装请求、发起HTTP、解析流式输出。
Godot的HTTP请求是异步的,不能和主线程的游戏循环同步等待。所以我用HTTPRequest节点发请求,然后用信号把结果传回来:
# AIClient.gd extends Node signal ai_response_ready(response_text: String) signal ai_response_error(error_msg: String) var api_url := "https://api.your-llm-provider.com/v1/chat/completions" var api_key := "your-api-key-here" var model_name := "your-model-name" var temperature := 0.7 var max_tokens := 1024 var http_request: HTTPRequest func _ready(): http_request = HTTPRequest.new() add_child(http_request) http_request.request_completed.connect(_on_request_completed) func send_prompt(system_prompt: String, user_prompt: String, history: Array = []): var messages := [] messages.append({"role": "system", "content": system_prompt}) for msg in history: messages.append(msg) messages.append({"role": "user", "content": user_prompt}) var body := JSON.stringify({ "model": model_name, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, "stream": false # 第一版我关掉流式,先保证稳定 }) var headers := [ "Content-Type: application/json", "Authorization: Bearer " + api_key ] var err := http_request.request(api_url, headers, HTTPClient.METHOD_POST, body) if err != OK: ai_response_error.emit("HTTP request failed: " + error_string(err))这里有几个我调试了很久才明白的细节:
- 别把
api_key硬编码在导出的游戏包里。如果你发布的是Web版,任何人的浏览器开发者工具都能把你的密钥抓出来。我的做法是:游戏里调用一个自己的后端转发接口,由后端保存密钥,游戏只发业务内容,后端再拼装大模型请求。这部分代码不难,但属于“上线前必须做”的安全动作。 temperature参数很关键。做游戏剧情时我开到0.8,这样角色回复比较有创造性;但解析“玩家意图”的时候我会降到0.2,避免AI发挥过头产生不可控的判定。- 第一版别开
stream: true。流式响应体验更好,但会显著增加游戏内处理的复杂度,后面我会专门讲这个坑。
3.3 游戏内的AI抽象层:对外只暴露一个函数
有了AIClient之后,我又包了一层GameAI.gd,让游戏逻辑根本不用管“HTTP”“JSON”“大模型”这些事。所有玩法代码只调它暴露的方法,比如:
# GameAI.gd extends Node enum AI_TASK { TALK, ACT, NARRATE } func request_npc_reply(npc_name: String, npc_persona: String, player_input: String) -> void: var sys_prompt = "你是游戏里的角色%s,性格设定:%s。请用中文以角色口吻回复玩家。" % [npc_name, npc_persona] _client.send_prompt(sys_prompt, player_input, _get_recent_memory()) func request_player_action(player_input: String) -> void: var sys_prompt = """ 你是一个游戏判定的AI。请根据玩家输入,推断玩家想做什么。 只输出JSON,格式如下: { "action": "移动|攻击|交谈|拾取|使用|等待", "target": "目标对象", "intent": "一句话说明你想做什么" } 不允许输出JSON之外的内容。 """ _client.send_prompt(sys_prompt, player_input)我把AI请求分成两种任务:对话型任务让AI自由发挥,判定型任务让AI必须输出结构化JSON。这两种模式用同一个AIClient,但用不同的system prompt和temperature,这个设计帮我避免了后面90%的解析问题。
4. 从空项目到可玩原型:我的一步步搭建过程
4.1 新建Godot项目,配置好目录
我用的是Godot 4.2,创建项目时选了“Blank”模板。目录结构提前规划好:
res:// ├── scenes/ # 场景文件 │ ├── main.tscn │ └── npc.tscn ├── scripts/ # 脚本 │ ├── AIClient.gd │ ├── GameAI.gd │ └── npc.gd ├── data/ # 角色人设、剧情设定 │ └── npc_personas.json └── ui/ # 界面相关 └── chat_panel.tscn这个规划很普通,但有个好处:AI相关逻辑、角色数据、界面三层分离。后期调参、换模型、换人物设定,都不用动到玩法核心。
4.2 把“角色”设计成数据驱动
做AI游戏最大的思维转变是:角色不是图片、不是动画状态机,而是一段可被大模型理解的人设文本 + 一组游戏世界状态值。
我的npc.gd大概长这样:
# npc.gd extends CharacterBody2D @export var npc_id: String @export var display_name: String = "神秘老者" @export var persona_prompt: String = "你是一位隐居深山的睿智老者,说话慢条斯理,喜欢引用山野典故。你对玩家有着隐秘的好奇心,但不会轻易信任陌生人。" @export var trust_level: int = 40 @export var memory: Array = []persona_prompt就是AI的人设,trust_level是影响对话走向的数值,memory是“这个角色记得的事”。每次AI请求时,我都会把相关状态拼进system prompt里:
你是一位隐居深山的睿智老者……(人设) 当前你对玩家的信任度是40(满分100)。 你记得的最近几件事: 1. 玩家在村口帮过你找回丢失的药草。 2. 玩家昨夜在酒馆打听过你的下落。这样AI的回复就不是瞎聊天,而是真正嵌在游戏状态里的。
4.3 核心玩法闭环:输入 → 判定 → 执行 → 反馈
我的Demo玩法很简单,概括成一句话:玩家在对话框里输入任意文字,AI判断玩家想做什么,游戏执行对应的状态变更,然后AI根据新状态继续扮演NPC回应。
这个闭环的代码逻辑是这样的:
func _on_send_button_pressed(): var player_input = input_field.text input_field.clear() _game_ai.request_player_action(player_input) func _on_action_ready(action_data: Dictionary): match action_data.action: "移动": player.position = _find_target_position(action_data.target) _game_ai.request_npc_reply("神秘老者", _persona_prompt, "玩家朝你走了过来,他说道:" + action_data.intent) "攻击": npc.trust_level -= 10 _game_ai.request_npc_reply("神秘老者", _persona_prompt, "玩家突然攻击你!") "交谈": _game_ai.request_npc_reply("神秘老者", _persona_prompt, action_data.intent) _: _game_ai.request_npc_reply("神秘老者", _persona_prompt, "玩家做了一件你一时看不懂的事情:" + action_data.intent)关键点在于:AI负责“理解意图”,游戏逻辑负责“执行改变”,AI再根据“变化后的世界”继续反馈。这个循环一旦转起来,玩家会感觉到自己说的话真的在改变游戏世界,而不是在跟一个聊天机器人对话。
4.4 把AI输出的JSON安全地变成游戏状态
这里有一个新手最容易爆的雷:AI返回的JSON不能被信任。它多了逗号、少了大括号、自说自话加字段,都会让你的JSON.parse直接垮掉。
我写了一个_safe_parse_json函数,做了三层防御:
func _safe_parse_json(raw_text: String) -> Dictionary: # 第一层:直接尝试解析 var result = JSON.parse_string(raw_text) if result != null and typeof(result) == TYPE_DICTIONARY: return result # 第二层:尝试抠出花括号里的部分 var regex = RegEx.new() regex.compile(r"\{.*\}", RegEx.MULTILINE) var match = regex.search(raw_text) if match: result = JSON.parse_string(match.get_string()) if result != null and typeof(result) == TYPE_DICTIONARY: return result # 第三层:放弃解析,返回一个安全的默认值 return { "action": "等待", "target": "", "intent": raw_text.left(50) }很多教程不会告诉你:大模型经常会在JSON前面加一句“好的,下面是我的回答:”。你如果直接JSON.parse整个字符串,必然报错。用正则把花括号内容抠出来是最稳的降级方案。
5. 实测里最折磨人的踩坑链路
跑通第一版只花了一个晚上,但真正让它“像能玩的游戏”我调了整整两周。下面这几个坑,按我踩的时间顺序写。
5.1 流式响应一开,游戏直接“假死”
初期测试我为了省事关掉了流式,接上stream: true的当晚,我的游戏画面就卡住了。原因其实不复杂:Godot的HTTPRequest虽然本身是异步的,但如果你用_process或者同步等待去阻塞主线程循环,整个游戏就会僵在那里。
排查链路是这样的:
- 我先在请求发出前打了print,确认AIClient发请求正常;
- 请求发出后,游戏立刻卡死,连UI的点击都失效;
- 我以为是
await用错了,翻查后确认HTTPRequest的request_completed信号会自动在空闲帧触发,不需要手动await; - 后来发现我为了“处理流式”,把解析逻辑放在了信号回调里做字符串累积和正则清洗,这个操作本身不阻塞,但传给信号的数据量一大,回调里连续做几十次字符串截取和拼接,就拖垮了帧循环;
- 最终解法:流式数据单独走一个后台线程,每帧只取“当前已累积的内容”显示到UI,解析JSON和清洗文本全在另一个线程做。
说实话,如果你做的是对话类AI游戏,流式真的不是必需品。我到现在很多正式场景都关流式,换来的是稳定性和更简洁的代码。流式只留给“长篇剧情演出”这种需要逐字效果的时刻。
5.2 上下文膨胀:聊到第20句,AI开始“失忆”
我的对话系统一开始把所有历史消息一股脑都塞给大模型,结果就是:
- 第10句后,响应速度肉眼可见变慢;
- 第20句后,AI开始重复说“你说得对”;
- 第30句后,它忘了最初的人设,开始说自己是AI助手。
根因在于:大模型的上下文窗口有限,你不控制历史长度,它就会被无关信息淹没。而且游戏里一个NPC的记忆只需要保留“对当前决策有影响”的部分,不需要逐字记住所有对话。
我的方案是引入滑动窗口 + 关键记忆抽取:
const MAX_HISTORY := 12 # 最多保留最近6轮对话 func _trim_history(history: Array) -> Array: if history.size() > MAX_HISTORY: return history.slice(history.size() - MAX_HISTORY, history.size()) return history func _compress_memory(npc_memory: Array) -> String: if npc_memory.size() <= 5: return "\\n".join(npc_memory) # 超过5条时,让AI把旧记忆压缩成摘要 _game_ai.request_memory_compression(npc_memory)再往后我加了一个“睡前总结”机制:每10轮对话后,让大模型把历史对话浓缩成几条摘要,存进npc.memory,旧消息直接丢。这样NPC既能长期记住你跟它的交集,又不会让上下文无限变胖。
5.3 AI胡说八道怎么办:给“幻觉”建一个兜底层
最让人崩溃的是AI在某次判定时,明明玩家输入的是“小心地推开门”,它给你判定成“大力踹门”,让整个剧情往离谱的方向狂奔。
面对这个,我的策略分三层:
第一层:对判定型任务,把可选的action枚举写死在prompt里,并且用few-shot示例约束它。比如:
请把玩家的行为归类到以下枚举值之一: - open_door(开门) - lock_door(锁门) - break_door(破门) - leave(离开) 如果无法归类,选择最接近的一项。第二层:在游戏逻辑里给每个枚举值写“最小可处理结果”。哪怕AI判定错了,游戏也要能正常继续。比如break_door就真的让门碎掉,但奖励惩罚照常计算,让玩家觉得这是“游戏设计如此”,而不是AI出Bug。
第三层:加一个玩家侧的手动修正入口。如果AI判定得离谱,玩家可以长按“重试”按钮,重新生成一次判定。这个操作很反直觉,但实际体验中玩家非常喜欢,因为他们能感觉到自己在跟AI共同创作剧情,而不是在忍受一个不靠谱的导演。
6. 性能、成本、体验:上线前必须补的功课
6.1 请求缓存:同样的对话,别让玩家花两遍钱
大模型API按token计费,虽然单次不贵,但游戏是长期运行的产品,玩家每点击一次都调一遍接口,成本会失控。我做的第一个优化是结果缓存:
- 把“system prompt的哈希 + 用户输入”作为key,把AI回复存到本地
File或者登录后的云存档; - 同一玩家再次输入相同内容时,直接读缓存,不再请求API。
这个优化对重复体验剧情的玩家尤其重要。我的Demo里,有玩家会把同一句话发给三个不同的NPC,其中两句命中缓存,响应时间从3秒降到0.1秒,体验提升非常明显。
6.2 预生成与后台预热:消灭“等待转圈”
AI响应再快也有网络延迟,纯同步等待会毁掉游戏节奏。我做了三件事:
- NPC出场前预生成开场白:玩家接近NPC的瞬间,就先在后台请求一轮“初次见面台词”,走完触发交互时直接展示,实现“秒回”;
- 回合制思维:设计上刻意让AI交互都发生在“玩家主动输入”之后,然后进入短等待状态,配合转圈UI,避免“玩家干等但无事可做”;
- 低俗场景用规则+AI混合:高频的移动、捡物品、简单交互绝不走大模型,只有“对话”“复杂行为判定”才走API,这样大部分游戏时间完全无网络等待。
6.3 提示词工程:别写小作文,要写测试用例
很多开发者把system prompt写成了长篇世界观小说,结果AI表现平平。我的经验是:人设适量,功能边界清晰,范例比描述更重要。比如我重新设计“神秘老者”的system prompt后,性能提升不是因为人设字数变多,而是加了这样一段:
示例对话: 玩家:“我听说山里有宝藏。” 老者:(眼中精光一闪,但没有直接答话)“……你来的路上,是不是踩断了一根结香树枝?”这一小段示例让AI明显更懂“这个老者角色该有的节奏”,比我在人设里写三十句“他是睿智的、沉稳的……”都管用。
另外,温度参数也值得定期调。我的项目里,对话任务温度0.75,判定任务温度0.2,记忆摘要温度0.3。调参的过程很漫长,但效果立竿见影。
6.4 兜底方案:AI服务挂了你不能跟着挂
最后一个必须提的:AI服务宕机或超时,不等于游戏崩溃。我的游戏里有一条完整的降级链路:
- 请求超过15秒无响应,UI提示“剧情服务暂时繁忙”;
- 自动切换到本地规则引擎,按关键词匹配返回预设回应;
- 玩家可以继续探索场景、拾取物品、查看NPC资料,游戏主体流程不受影响。
这意味着即使我依赖的模型服务临时停机,游戏也不会变成一块死屏。很多玩家会因此对你多一份信任,因为“AI游戏卡住了”跟“游戏死机了”是两回事。
最后再分享一个我这半个月最有成就感的小设计
我的Demo里有位“酒馆老板娘”,她会在玩家赢得小镇掷骰子比赛后,主动提及玩家三天前在河边给流浪猫喂鱼这件事。玩家完全懵了——因为那是他三小时前在一个毫不相关的场景里做的事。实际上,我的实现非常土:每个NPC的memory共享到同一个全局事件池,AI在抽记忆时按“相关性 + 全局且私密”的权重捞出来。但玩家不知道这层设计,他们会觉得“这个游戏里的角色真的在互相信任地传递信息”。
这种“虚假的深度”恰恰是AI游戏最迷人的地方。技术上它只是一个记忆池加一次向量检索,但体验上它完成了很多3A大作想做而不敢做的事:让玩家相信每个角色都有自己的生活。
如果你也准备从0开始做AI游戏,我的建议是:别一上来就纠结“我的AI逻辑够不够智能”,先把“输入 → 判定 → 执行 → 反馈”这个循环用最简单的方式跑通,哪怕你的角色就只有一个酒馆老板娘。跑通之后,你自然会知道下一步该往哪里加东西。这条路我已经替你踩过一遍了,接下来该轮到你进场了。