第十五章:大模型打造 AI 桌面宠物
本章导言
人工智能正迅速改变游戏开发的方式,而大语言模型正是这一变革的核心技术之一。从生成角色对白、设计关卡,到自动编写脚本、绘制场景草图,大模型正在成为游戏设计师、程序员和美术师的得力助手。
在本章中,我们将带你走进大模型的世界,了解它的基本原理和能力边界,探索它如何在游戏创意、代码开发与素材生成等环节中发挥作用。更重要的是,我们将通过一个有趣而实用的项目,“AI桌面宠物”,来学习如何在 Godot 引擎中调用大模型接口,打造一个能与用户对话、互动、拖动、隐藏的智能虚拟角色。
你将学到:
- 大语言模型的工作机制;
- 如何使用提示词生成游戏设计文档、代码片段、美术素材;
- 如何通过 HTTPRequest 节点调用大模型接口;
- 如何使用 Godot 构建一个具备透明窗口、鼠标穿透、多边形区域检测和AI对话能力的桌面宠物。
这不仅是一节关于AI的入门课程,更是一次激发创造力的旅程。本章将帮助你打开新的思路,AI不仅能玩游戏,更能参与“做游戏”。
1 大模型简明入门
在过去的几年中,大语言模型(Large Language Model,简称 LLM)迅速成为人工智能领域的明星技术。它不仅能写文章、编程、翻译语言,还能与人类进行自然对话。在游戏开发中,它也正展现出惊人的潜力。
语言模型
语言模型的核心任务是:预测接下来最合理的词语。比如你在输入“我要去”,模型会根据语言规律预测下一个词可能是“超市”、“上学”或“旅行”。传统语言模型主要依靠规则或小规模数据进行预测。而大语言模型则使用深度学习,从数以亿计的文本中学习语言的结构、语法和上下文。
大语言模型
大语言模型的“大”不仅体现在体积,更体现在能力:
- 参数数量多:模型中包含数十亿甚至上千亿个参数,用于捕捉复杂的语言模式。
- 数据范围广:训练数据来自互联网上的大量网页、书籍、代码和对话内容。
- 任务通用性强:无需专门训练,它就能处理多种任务:写文案、生成代码、回答问题、模拟角色对话等。
- 理解上下文:通过一种叫“自注意力机制(Self-Attention)”的技术,它能同时考虑整段话中的所有信息,理解句意更全面。
多模态大模型
很多游戏数据并不仅是文字,还包含图片、音频、视频等。多模态大模型就是能够同时理解和处理多种类型输入的模型。例如:
- 输入一句文字,生成角色或场景图片(如 Stable Diffusion)
- 输入图片,回答“这张图中有几个人?”
- 输入语音,识别内容并生成回复
- 输入视频片段,进行情感分析或字幕生成
这类模型让我们可以通过自然语言来控制图像、声音等资源,进一步简化游戏开发流程。
大语言模型的核心是“理解与生成语言”,而多模态模型则进一步扩展到图像、声音和视频。它们正在为游戏开发者提供新的工具,提升效率、激发创意,也降低了开发门槛。
2 大模型在游戏开发中的应用
随着大语言模型(LLM)的飞速发展,游戏开发者迎来了全新的智能工具。你可以把它看作一位“懂游戏的超级助理”,只要用自然语言告诉它你的想法,它就能帮你写文档、写代码、画图,甚至模拟NPC与玩家对话。
2.1 游戏设计助手
对于游戏设计师而言,大模型是一个快速构思和文案生成工具:
- 游戏创意生成:大模型可以根据输入的简单描述生成游戏的创意构思,涵盖故事背景、世界观设定、角色设定等。例如“科技和魔法并存的沙漠文明”,就能生成完整的历史背景、种族划分和地理风貌等相关游戏设定;
- 关卡设计:大模型能够根据游戏的主题、玩法类型自动生成不同难度和创意的关卡布局,帮助设计师节省时间并提供新颖的设计思路。
- GDD(游戏设计文档)草稿:生成包含玩法循环、系统结构、角色系统、成长机制等内容的标准设计文档。
通过大模型来生成完整的游戏设计文档,参考提示词如下:
"请作为资深游戏设计师,根据以下创意框架逐步构建完整的游戏设计文档。要求包含:核心玩法循环、世界观设定、角色系统、关卡设计、成长体系、技术需求、商业模式等完整模块。文档需符合现代游戏设计规范,包含可行性分析和差异化设计。"
大模型可能不会一次性完成一个详细的游戏设计文档,一种更实用的做法是先让大模型生成设计文档的大纲,然后再针对每个章节让大模型进行完善和修改。这是一种更有效的多次调用大模型的工作模式。
游戏开发的另一个重要设计文档就是技术设计文档。我们也可以让大模型来帮助我们完成初稿。参考提示词下:
"你是一个资深的游戏系统架构师,需要根据我提供的游戏设计文档(GDD)进行面向对象技术分析与设计。请输出详细的需求分析和类图设计。"大模型可以帮助我们完成技术设计的初稿,然后再进行进一步的修改和优化。
2.2 游戏代码自动编写
程序员可以借助大模型完成大量繁琐工作,包括初始脚本生成、逻辑修复、算法优化等:
- 自动生成代码:开发者可以通过描述所需功能或任务,大模型会自动生成相应的代码段。比如,“生成一个控制角色跳跃的代码”,模型会输出合适的代码逻辑,如移动、攻击、UI控制、动画播放等。
- 优化游戏算法:大模型能够分析已有的游戏代码并提供优化建议,帮助开发者提高游戏的性能。例如,优化图形渲染算法、碰撞检测等,提升游戏运行效率。
- Bug修复和代码审查:大模型也能帮助开发者在代码中找到潜在的错误或逻辑漏洞,提供修复建议,甚至通过自动修复脚本直接修正问题。开发者可以通过与大模型的互动,加速调试过程。
使用大模型辅助代码开发有很多工具和方法,你可以直接在大模型平台上进行提问和代码生成,也可以借助第三方工具和插件。如果你偏好使用 VSCode 进行游戏开发,GitHub Copilot 是一个非常实用的 AI 编程插件。它可以根据你当前编写的代码自动补全整行或整段逻辑,也能根据注释或自然语言描述生成函数、类或脚本框架。例如当你在 GDScript 脚本中输入一段注释时(# 控制角色移动的函数),Copilot 会立即尝试生成对应的函数,并补全输入处理、方向判断、移动代码等内容。它不仅支持 Godot 的 GDScript,也支持 Python、C#、JavaScript 等多种语言,非常适合用于加速日常脚本编写、重构已有代码或参考最佳实践,让编程过程更加高效流畅。
你也可以从头开始生成游戏脚本,参考提示词包括:
"生成一个简单的角色控制脚本,支持玩家通过键盘控制角色的移动,使用 W, A, S, D 键进行上下左右移动,按空格键进行跳跃,角色跳跃时具有重力影响,且支持双跳。"
"编写一个游戏的血条 UI,当玩家受伤时,血条会相应地减少,且血条背景为红色,显示玩家的当前健康状态。"
2.3 游戏美术素材生成
在游戏美术早期阶段,大模型可协助制作概念图、角色草图、UI风格参考等:
- 自动生成场景和角色设计:大模型能够根据输入的关键词自动生成场景设计、角色外观、服饰、道具等元素。开发者可以输入“一个未来城市的街道场景,夜晚,有霓虹灯”,模型便能生成符合描述的图像素材。这些素材可以作为设计灵感或直接用于游戏开发中。
- 风格转换和迭代:对于美术风格的切换和迭代,模型也能提供帮助。例如从写实风格改为卡通风格,模型可以根据现有素材自动转换风格,极大节省手动绘制的时间和精力。
- 动画生成:大模型还能够根据角色的动作设计,自动生成流畅的2D或3D动画。例如,输入“角色跳跃并做出攻击动作”,模型可以生成相关的帧动画或骨骼动画,简化动画制作过程。
我们可以使用文生图大模型来生成游戏美术素材。同样的,你可以在大模型平台上直接提问,也可以使用第三方工具。比较流行的工具是ComfyUI,ComfyUI 是一个强大、直观的图像生成工具,它基于 Stable Diffusion,采用“拼图式”的工作流界面。用户可以像搭积木一样组合不同的图像生成模块,包括文本提示输入、模型选择、风格调整、图像处理等。
另一个推荐的工具是Krita AI,Krita 是一款免费开源的数字绘画软件,类似 Photoshop,适用于插画、动画与概念设计。通过安装 AI 插件,你可以在 Krita 中直接使用 Stable Diffusion 的图像生成功能。它能与传统绘图工具无缝结合,适合美术师直接使用。
在前面章节的坦克大战游戏中,我们就使用了文生图大模型来制作了主菜单的背景图,其提示词如下:
“a cute red tank, cartoon style, black background”
2.4 NPC对话生成器
游戏中的非玩家角色(NPC)往往扮演着引导、陪伴、推动剧情的关键角色。传统的NPC对话往往依赖编写大量脚本和对话树,耗时费力且不易拓展。而大语言模型(LLM)的加入,为游戏对话系统带来了革命性的改变:
大语言模型可以根据简单的提示生成自然、连贯、富有个性的对白,还能保持对话上下文,模拟情绪波动,甚至生成多选项分支。你不再需要为每一位NPC写死对白脚本,而是为他们设置一段“角色设定”,大模型就能自动完成对话生成。
-
设定角色:设定一个NPC的“人设”提示词(system prompt)是关键。你需要明确这个角色的身份、性格、说话语气、背景故事,以及他对哪些话题敏感或回避。比如:“你是中世纪王国里的盲眼守门人,说话缓慢而神秘,常用比喻,避免直接回答问题。” 这样的提示可以让生成的对话风格统一且可信。
-
控制输出:你可以进一步在提示中添加限制,比如“每次回答不超过50字”、“语气中带有愤怒与讽刺”、“以古文风格回答”等。大模型会在生成时自动遵循这些规则,从而更好地贴合游戏语境与世界观。
-
多轮互动与分支对话:大模型支持上下文记忆,可以与玩家进行多轮连续对话,也能根据提示生成多个玩家可选回应(如三个选项),并为每个选项指定不同的剧情后果(如触发战斗、开启支线、提高好感度)。如果你希望与游戏引擎集成得更紧密,还可以要求模型以结构化方式输出结果,便于直接接入对话管理系统。
-
加入情绪表达与动态变化:除了语义,大模型还能表达不同情绪的语言风格,如平静、愤怒、哀伤、激动等。你只需在提示词中注明角色当前状态(如“他刚刚得知了同伴的死亡”),模型就能自动生成符合情绪的回应。更进一步,结合游戏变量(如角色心情值或剧情事件)实时更新提示词,就能构建出“情绪会变”的NPC。
大模型可以用于设计剧情角色的主线对话、商人的交易对白、图书馆员的知识介绍、宠物的拟人表达、解谜过程中的引导提示等。
为了更好地与 Godot 或其他游戏引擎结合,建议将大模型的输出统一为对话数据格式(如 JSON),并设置合理的上下文长度控制。你可以在每次对话时动态构造提示词(包含玩家选择、NPC状态等),再用 HTTPRequest 调用大模型接口获取回应,绑定到 UI 界面显示。
3 Godot 调用大模型的方法
为了让大语言模型成为游戏的一部分,我们需要学会如何在 Godot 引擎中通过网络请求调用它的 API。本节将带你完成这个过程,包括:申请密钥、配置请求、封装脚本组件、构建编辑器插件,并以阿里云通义千问为例,演示完整的接入流程。
3.1 申请密钥
通过网络调用大模型通常需要具备两个核心要素:
- 一个合法的 API 密钥(API Key),用于验证身份;
- 一个 HTTP 接口地址(URL),用于发送请求和获取模型返回内容。
你可以选择任意支持 HTTP 请求的大模型平台,如:OpenAI (ChatGPT)或者阿里云通义千问(QWen)。你也可以选择本地部署开源大模型,例如安装Ollama工具,它也可以提供API接口。本节的代码以通义千问为例,其它平台的操作类似。登录阿里云通义千问官网后即可申请到 API Key,并获得对应的接口地址。
3.2 使用配置文件保存密钥
为了安全起见,我们不会将密钥写死在代码中,而是将其保存在项目目录下的配置文件中,如 res://config/api_config.cfg,内容格式如下:
3.3 封装大模型接口组件
我们将创建一个名为 LLMAPI 的脚本组件,专门用于调用大语言模型服务。该脚本可以作为任何场景的独立节点使用,也可以嵌入插件或游戏主逻辑中。代码会依赖Godot的两个类:HTTPRequest 和 JSON,先分别作简单介绍。
HTTPRequest 是一个用于执行 HTTP 网络请求的内置节点,支持与 Web 服务进行数据通信。它是我们与大模型进行 API 调用的核心工具。你可以使用 HTTPRequest 向服务器发送 GET、POST、PUT 等请求,并在请求完成后通过信号接收返回数据。网络请求完成后,HTTPRequest 节点会自动发出 request_completed 信号,表示服务器已经返回响应内容(无论成功或失败)。你可以通过连接该信号,获取返回的状态码与数据内容。
JSON 是一个用于处理 JSON 数据的内置工具类,常用于与 Web API 通信时解析和生成数据格式。它提供了两个主要方法:JSON.parse(json_string) 用于将 JSON 格式的字符串解析为字典或数组对象;JSON.stringify(data) 用于将字典或数组转换为 JSON 字符串,便于通过网络发送。配合 HTTPRequest 使用时,你可以轻松地构造请求体或读取服务器返回的数据,是连接游戏与外部服务的桥梁。
需要开发的LLMAPI组件的关键功能包括:
- 载入API密钥:从配置文件中读取;
- 维护对话历史:支持多轮上下文交互;
- 构造请求消息:生成带有
system prompt和历史信息的 JSON 请求体; - 发送POST请求:通过 Godot 内置的
request()方法; - 接收与解析响应:解析 JSON 数据,提取模型回答内容;
- 信号通知机制:通过
request_finished信号将结果发送给其他脚本或界面。
在文件系统中新创建一个名为 llm_api.gd 的脚本文件,代码如下:
extends HTTPRequest
class_name LLMAPI
signal request_finished
var output: String
var history: Array = []
var history_count: int = 3
var api_key :String = ""
var header :Array
var url = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"
var model_name = "qwen-max"
var cfg_path = "res://config/api_config.cfg"
var system_prompt: String = "你是一个精通游戏设计和Godot游戏开发的智能助手,你的回答字数不超过200个字"
request_finished信号是为了让LLMAPI组件能和其它场景进行信息传送。ouput变量用于存放大模型的输出结果history变量用于存放对话历史,history_count变量用于控制对话历史的长度。api_key变量用于存放API密钥header变量用于存放请求头,在一个HTTP请求中,Header(头部)是请求的元数据,用来传递关于请求的附加信息。url变量用于存放请求的URL地址,model_name变量用于存放模型名称cfg_path变量用于存放配置文件的路径system_prompt变量用于定义大模型的系统提示词,也可以认为是定义了大模型的人设。
func _ready() -> void:
load_api_key()
header = ["Authorization: Bearer " + api_key, "Content-Type: application/json"]
request_completed.connect(on_request_completed)
_ready()函数中,首先加载API密钥- 然后定义了请求头,其中包括用于验证用户身份的
Authorization,api_key拼接在其中,Content-Type指定请求体的内容类型为application/json,意味着发送的数据是JSON格式。 - 最后连接了
request_completed信号,用于处理API请求完成后的响应。
func call_llm(prompt):
var new_message = {"role": "user", "content": prompt}
history.append(new_message)
var sys_message = {"role": "system", "content": system_prompt}
var messages = [sys_message]
messages.append_array(history.slice(-history_count))
var body = JSON.stringify({
"model": model_name,
"messages" : messages,
"stream" : false
})
var request_result = request(url,
header,
HTTPClient.METHOD_POST,
body)
if request_result != OK:
push_error("LLM 请求失败,错误码:%s" % request_result)
call_llm函数负责和大模型服务端进行交互- 接受
prompt字符串参数,用于作为大模型的提示词输入,prompt会构成一个名为new_message的字典,因为所有传送给大模型的信息,需要是一个字典格式,其中role表示信息的主体,content表示信息的内容。 - 为了让大模型记得对话上下文,我们需要所有的往来信息都保存在一个对话历史中,也就是保存在
hitory中。 - 然后将系统提示词作为消息列表
messages的第一条信息,其它历史信息跟随在后面, - 使用
JSON.stringify函数将数据转换为JSON格式,保存为变量body。body变量用于存放请求体,其中主要包括模型名称,消息列表,以及是否需要流式响应。 - 通过request函数将所有需要的信息发送给大模型,其中包括了API密钥、请求的URL地址、请求的方法以及请求体。
func on_request_completed(result, response_code, headers, body):
var response = JSON.parse_string(body.get_string_from_utf8())
output = response['choices'][0].message.content
history.append({"role": "assistant", "content":output})
request_finished.emit(output)
on_request_completed函数负责处理API请求完成后的响应- 函数中使用
JSON.parse_string函数来解析返回的字符串 - 将解析后的结果赋值给
output变量 - 将输出结果添加到历史记录中
- 最后通过自定义的
request_finished信号将结果传递出去
func load_api_key():
var config := ConfigFile.new()
var err := config.load(cfg_path)
if err == OK:
api_key = config.get_value("api", "key", "")
else:
push_error("无法加载 API 密钥配置文件")
load_api_key函数负责加载API密钥- 使用
ConfigFile类来加载配置文件,并获取API密钥
这样就完成LLMAPI组件的开发,它可以灵活嵌入任何使用场景中,比如游戏角色、桌面宠物、任务助手等。
3.4 创建编辑器中的对话插件
除了在游戏中调用大模型,我们还可以在Godot 编辑器中创建一个辅助插件,直接输入内容并与大模型互动,例如生成代码片段、查询设计建议等。通过开发这个编辑器插件,可以测试验证之前开发的大模型接口组件,同时也可以进一步掌握插件的开发流程。
在前一章中我们已经开发一个简单按钮插件,点击会显示当前日期时间。本节将开发的插件是一个对话插件,会基于 EditorPlugin 编写插件逻辑,向编辑器底部添加一个 LineEdit 输入框。当用户输入文字并回车时,插件调用 LLMAPI 组件进行对话,响应结果将打印到输出窗口。
根据之前学到的编辑器插件开发流程,我们需要完成以下步骤:
- 在文件系统中创建目录 addons/llm_chat
- 创建插件配置文件 addons/llm_chat/plugin.cfg,并在其中配置插件信息,name 为 llm_chat,scipt 为 addons/llm_chat/llm_chat.gd
- 将之前创建的 LLMAPI 组件移动到 addons/llm_chat目录中
- 创建插件脚本文件 addons/llm_chat/llm_chat.gd
在llm_chat.gd文件中,编写代码如下:
代码解释:@tool关键词表示这是一个工具脚本,也需要继承EditorPlugin表示此脚本是一个编辑器插件- 定义变量
input_line是一个LineEdit控件,用于显示输入框 - 定义变量
llmapi,它是一个LLMAPI组件,用于调用大模型接口 - 目前这两个变量都只进行了声明,还没有赋值,所以是
null值。
func _enter_tree():
input_line = LineEdit.new()
input_line.text = "输入问题和大模型聊天"
input_line.text_submitted.connect(on_submitted)
add_control_to_container(CONTAINER_INSPECTOR_BOTTOM, input_line)
llmapi = LLMAPI.new()
add_child(llmapi)
llmapi.request_finished.connect(on_request_finished)
_enter_tree函数是插件进入编辑器时的回调函数- 创建一个LineEdit控件input_line,设置默认文本为"输入问题和大模型聊天"
- 将
text_submitted信号连接到on_submitted函数,用于处理用户输入 - 使用
add_control_to_container函数将input_line添加到编辑器的底部容器 - 创建一个LLMAPI组件
llmapi对象,并使用add_child函数将其添加到插件的子节点中,因为LLMAPI组件需要成为节点后才能正常工作 - 将
request_finished信号连接到on_request_finished函数,用于处理大模型返回的结果
func _exit_tree():
remove_control_from_container(CONTAINER_INSPECTOR_BOTTOM, input_line)
input_line.queue_free()
input_line = null
llmapi.queue_free()
llmapi = null
_exit_tree函数是插件退出编辑器时的回调函数- 使用
remove_control_from_container函数将input_line从编辑器的底部容器中移除 - 使用
queue_free函数释放input_line对象和llmapi对象 - 并将
input_line和llmapi设置为null
func on_submitted(new_text):
llmapi.call_llm(new_text)
input_line.text = "思考中..."
input_line.editable = false
print("User:"+new_text)
on_submitted函数是处理用户输入的回调函数- 调用
llmapi.call_llm函数来调用大模型接口,传入用户输入的文本 - 当用户输入完成后,设置
input_line的文本为"思考中...",禁用编辑功能,避免用户继续输入 - 然后打印用户输入的文本到控制台
func on_request_finished(chat_text):
input_line.editable = true
input_line.text = ""
print("AI:" +chat_text)
on_request_finished函数是处理大模型返回的结果的回调函数- 在大模型返回结果后,启用编辑功能,清空
input_line的文本, - 最终打印大模型返回的结果到控制台
这样就完成了编辑器中的对话插件的开发。在项目设置中启动该组件,然后可以在编辑器右下角看到一个输入框,输入问题后,点击回车,就可以在控制台看到大模型的回答了。完成后的效果如下图所示:

该插件可以作为你自己的“Godot AI 助手”,也可以作为桌面宠物组件测试的一部分,快速验证模型调用是否成功。
4 实战案例:AI桌面宠物
本节我们将结合前面学到的大模型调用技术,动手实现一个轻量级的 AI 桌面宠物。它不仅能常驻在用户的屏幕上,还能与用户进行对话互动,展现出智能生命的陪伴感。我们要实现的 AI 桌面宠物具备以下核心特性:
- 显示在桌面任意位置,背景透明,支持点击穿透,不干扰其他操作;
- 用户点击宠物时会弹出聊天窗口,可输入问题与 AI 互动;
- 聊天内容通过调用大语言模型生成,具备上下文记忆;
- 可拖动窗口移动宠物位置,提供贴心与陪伴感。
4.1 项目设置准备
在 Godot 中,我们需要对窗口和渲染做以下配置来支持“透明、无边框、最前显示”的桌面体验。打开 Project > Project Settings,进行以下修改:
-
Display/Window
Size/Viewport Width:1 初始窗口设置尺寸为1个像素大小,这是为了避免窗口启动时的闪烁现象,等窗口启动后会在代码中调整窗口大小。Size/Viewport Height:1Resizable:false,不需要调整窗口大小Borderless:true,窗口不要有边框Always On Top:true,窗口总是在最前面Transparent:true,窗口透明设置Per Pixel Transparency/Allowed:true,窗口透明设置
-
Rendering/Viewport
Transparent Background: true,窗口透明设置
-
Rendering/Textures
Default Texture Filter: Nearest(像素风格图像更清晰)
-
Rendering/Environment
Default Clear Color:设置为透明颜色
然后还需要在Input Map中设置三个动作,分别是:
- "click":映射鼠标左键,用于点击宠物,呼出聊天窗口
- "drag": 映射鼠标右键,用于拖动宠物
- "quit": 映射Esc按键,用于退出聊天窗口
4.2 大模型调用场景搭建
首先来搭建用于大模型调用的场景。在之前的示例中,我们是使用LLMAPI.new()来创建对象,然后add_child()将其添加到节点树中。本节将换用场景的方式,效果是一样的,只不过更为直观一些。
新建一个场景,根节点为HTTPRequest,重命名为LLMAPI,给它挂载脚本,脚本选择使用之前开发的llm_api.gd,将场景保存为llm_api.tscn。
考虑到它的功能是作为一个宠物的身份,可以修改代码中的system_prompt变量如下:
你也可以设置自己想要的的system_prompt,让宠物更有个性。4.3 宠物场景搭建
新建一个场景,根节点为 Node2D,命名为 Pet,下设两个主要子节点:
Player:Node2D节点类型,主要负责界面显示部分,其子节点会用于显示宠物图像、动画和对话 UI;LLMAPI:挂载我们前面创建的大模型调用场景。
将 Player 节点的位置设置为 (310, 382),便于初始展示。
Player节点的界面部分需要负责如下功能:
- 需要有节点来负责宠物的图形显示和动画,所以需要Sprite2D和AnimationPlayer节点
- 需要有节点来负责当用户鼠标点击宠物时,弹出聊天界面,这种交互可以用button,也可以用Area2D。
- 显示利聊天界面需要一个文本输入框和文本显示框,所以需要LineEdit和RichTextLabel。
- 最重要的一点是需要一个Polygon2D用来定义一个多边形区域,在这个区域内,用户的鼠标点击是有效的。尽管我们在项目设置中,让整个窗口背景透明,但它仍然会阻挡用户的鼠标操作,从而阻碍了用户电脑上的其它操作。
界面部分搭建
首先给Player节点增加一个Sprite2D节点,找到资源中的player.png图片,将它放到Texture属性中。
这个图片资源是一只小狐狸的不同姿态图片组合在一起。这些图片一共有19张,编号从0开始,观察发现空闲站立姿态的动画是从编号13到16这四张图片。在Animation属性中将Hframes设置为19,Vframes设置为1,Frame设置为13,也就是说让引擎知道,这张图片是由19张图片拼接而成,而我们先暂时需要其中编号为13的那张。最后是将scale设置为4,将图片放大4倍使用。
如果只有Sprite2D的话,宠物是一个静态的,我们为了给它加入动态性,在Player节点下新建AnimationPlayer节点。用它来控制Sprite2D的Frame属性值。
新建一个idle动画,动画时间为0.4秒,将Sprite2D的frame属性作为动画轨道,在0秒时frame设置为13,在0.1秒时frame设置为14,0.2秒时frame设置为15,0.3秒时frame设置为16。将动画设置为自动播放和循环播放的。这样就完成了动画部分。
动画设置完成后如下图所示:

然后增加Area2D节点用于实现鼠标可点击。属性中找到Pickable设置为True,以便接收点击事件。在Area2D节点下增加CollisionShape2D节点,在Shape属性中选择CircleShape2D,设置半径为40,可以适当调用它的位置,让它正好可以和狐狸图片重合。
聊天界面搭建
在Player节点下新建一个Control节点,因为聊天窗口的位置应该位于宠物的上方,而默认的位置是从左上角开始的,所以需要将Control节点的position属性设置为负值,例如(-257,-347)。另外要将pivot offset设置为(250,300)。这样使中心点放在中部下方,方便在后续设置正常的缩放动画。
然后在Control节点下设置一个PanelContainer,用于显示界面的面板。具体参数设置如下:
- layout_mode:Anchors
- anchors preset:Full Rect
- custom_minimum_size:(500, 300)
需要将Theme overrides中进行设置,选择new styleboxtexture,新建一个主题风格资源,将tile05.png文件拖拽到texture属性中,然后将Texture Margins设置为(10,10,10,10)。这样面板主题设置完成。完成面板设置后,如下图所示:

在面板中我们需要放两个UI组件,一个是用于文本显示,一个用于文本输入,所以先在PanelContainer下放置一下VBoxContainer节点,然后在其节点下放置RichTextLabel和LineEdit节点。
RichTextLabel的设置如下:
- visible_characters:0
- visible_ratio:0.0 这样设置可以让控件在一开始的时候保持空白,不显示任何内容。
LineEdit的设置如下:
- PlaceHolder Text:你想聊些啥,用于占位显示以提示用户。
- Max Lenth:40
多边形区域设置
为了定义鼠标可以点击操作的区域,我们需要在场景中设置多边形区域。桌面宠物的操作区域有两种可能,一种是聊天界面未显示时,此时操作区域是只需要框住小狐狸,一种是聊天界面显示时的时候,此时操作区域需要框住小狐狸以及聊天界面。因此我们需要定义两个多边形。
在Player节点下新建两个Polygon2D节点,分别重命名为Polygon2DPlayer和Polygon2DMenu。设置如下:
- Color属性:设置为透明色,因为它仅用于传递多边形数据信息,不需要显示出来。
先绘制Polygon2DMenu的多边形,使用操作面板上的Create Points,创建一个正方形的形状,让其面积正好覆盖聊天界面和宠物图形。Polygon2DMenu多边形的绘制示意图如下:

绘制Polygon2DPlayer是类似的,仍然是创建一个正方形的形状,让其面积正好覆盖宠物图形,但是不包括聊天界面的区域。Polygon2DPlayer多边形的绘制示意图如下:

这样宠物场景基本完成,下面我们继续来给它编写代码。
4.4 宠物场景的代码逻辑
下面需要给宠物场景增加代码,用于构建交互式的界面逻辑,集成了窗口拖动、菜单切换、透明窗口点击穿透、用户输入文本、调用大语言模型 API、输出对话等功能。
给Pet根节点附加代码如下:
extends Node2D
@onready var polygon_2d_player: Polygon2D = $Player/Polygon2DPlayer
@onready var polygon_2d_menu: Polygon2D = $Player/Polygon2DMenu
@onready var area_2d: Area2D = $Player/Area2D
@onready var llmapi: LLMAPI = $LLMAPI
@onready var control: Control = $Player/Control
@onready var rich_text_label: RichTextLabel = $Player/Control/PanelContainer/VBoxContainer/RichTextLabel
@onready var line_edit: LineEdit = $Player/Control/PanelContainer/VBoxContainer/LineEdit
var window: Window
var show_menu:bool = false
var dragging:bool = false
var drag_offset: Vector2i
var current_polygon : Polygon2D
var polygons :Array = []
- 使用
@onready关键字来引用子节点 window用于保存操作窗口对象show_menu用于判断是否显示聊天界面dragging用于判断是否拖拽中drag_offset用于保存在屏幕坐标系中,鼠标相对于窗口左上角的偏移量current_polygon用于保存当前操作的多边形polygons用于保存两种不同的多边形的数组。
func _ready() -> void:
setup_window()
line_edit.text_submitted.connect(on_submitted)
llmapi.request_finished.connect(on_finished)
area_2d.input_event.connect(on_input_event)
control.scale = Vector2.ZERO
polygons = [polygon_2d_player, polygon_2d_menu]
current_polygon = polygons[int(show_menu)]
setup_window函数用于初始化操作窗口text_submitted信号用于输入框文本提交事件。request_finished信号用于大模型组件返回数据。input_event信号用于鼠标点击操作事件。control节点的缩放属性设置为Vector2.ZERO,这是为了让聊天界面一开始处于隐藏状态- 将
Polygon2DPlayer和Polygon2DMenu的多边形放入数组中 - 最后赋值
current_polygon,此时show_menu为false,整数转换后的值为0,所以current_polygon的值此时等于Polygon2DPlayer。
setup_window函数用于设置窗口对象的尺寸,让窗口从一个像素大小到覆盖整个屏幕get_window函数是一个内置函数,用于获取当前游戏的窗口对象,然后使用DisplayServer.screen_get_size函数获取屏幕尺寸,并将其设置为当前窗口尺寸。但因为我们的游戏背景透明,所以并不会看到这个窗口。
func update_click_through():
var click_polygon: PackedVector2Array = current_polygon.polygon
for vec_i in range(click_polygon.size()):
click_polygon[vec_i] = current_polygon.to_global(click_polygon[vec_i])
window.mouse_passthrough_polygon = click_polygon
func _physics_process(delta: float) -> void:
update_click_through()
update_click_through函数负责更新窗口对象的点击穿透多边形- 首先将Polygon2D的多边形的每个顶点转换为全局坐标。之所以需要转换全局坐标,是因为窗口对象需要知道的是屏幕坐标系下的多边形形状,而 Polygon2D.polygon 默认是局部坐标系,也就是相对于它自己的节点原点的坐标,所以必须转换为全局坐标。
- 然后将其赋值给Window的
mouse_passthrough_polygon属性,相当于告诉操作系统,这个窗口只有某些区域是可交互的,其他部分请穿透到底层桌面。这样就可以实现点击穿透效果。 - 因为我们会移动拖拽这个宠物,所以需要在
_physics_process函数中调用update_click_through函数,以便实时更新点击穿透多边形。目的是确保窗口鼠标穿透区域实时更新。
func _input(event):
if event.is_action_pressed("drag") and not dragging:
dragging = true
drag_offset = DisplayServer.mouse_get_position() - DisplayServer.window_get_position()
elif event.is_action_released("drag") and dragging:
dragging = false
elif event is InputEventMouseMotion and dragging:
var new_pos = DisplayServer.mouse_get_position() - drag_offset
DisplayServer.window_set_position(new_pos)
if event.is_action_pressed("quit"):
get_tree().quit()
_input函数负责处理用户的输入事件,包括拖拽宠物,退出游戏等操作。- 第一个条件判断是当用户第一次按下鼠标右键,将
dragging设置为true,并赋值drag_offset变量,它是在屏幕坐标系中,鼠标的当前位置减去窗口左上角的当前位置。 - 第二个条件判断是当用户放开鼠标右键时,将
dragging设置为false。 - 第三个条件判断是当用户移动鼠标时,更新窗口的位置。新的窗口坐标是鼠标当前位置减去拖拽偏移量。
- 最后一个条件判断是当用户按下退出游戏的快捷键时,退出游戏
DisplayServer是Godot引擎中一个核心单例类,它代表的是Godot 与操作系统底层“显示系统”的桥梁。它让你可以直接控制窗口、显示器、鼠标、键盘、剪贴板等系统级别的显示和输入设备功能。之所以需要使用DisplayServer,是因为我们在拖拽宠物时,实际是在屏幕中移动一个看不见的窗口,而非在移动宠物场景。
之所以需要计算偏移量drag_offset,是实现窗口拖动时的标准写法。因为在屏幕坐标系中,鼠标坐标通常不等于窗口坐标(窗口左上角),但拖动时,需要让窗口的位移等于鼠标的位移,所以需要计算偏移量这个相对距离。用一个类比就好像是你要搬一个箱子,但你的手不是抓住箱子的左上角,而是抓住了箱子中间的位置。如果你每次移动时都让箱子的左上角直接跳到你手的位置,箱子就会“抖动”或“跳动”。而计算偏移量 drag_offset,就像是你记录了手相对于箱子的位置,确保每次拖动时,箱子整体平滑地跟着手移动。
func toggle_menu(show:bool):
var tw = create_tween()
if show:
current_polygon = polygons[int(show_menu)]
update_click_through()
tw.tween_property(control,"scale", Vector2.ONE, 0.5).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_IN_OUT)
await tw.finished
show_text("你好,我是小狐狸",0.5)
else:
tw.tween_property(control,"scale", Vector2.ZERO, 0.5).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_IN_OUT)
rich_text_label.visible_ratio=0
await tw.finished
current_polygon = polygons[int(show_menu)]
update_click_through()
toggle_menu函数负责切换聊天界面的不同显示状态。- 如果
show参数为true,则显示聊天界面,否则隐藏聊天界面。 - 切换界面会通过一个tween动画实现,当需要显示界面时,更新点击穿透多边形,将聊天界面的scale设置为1,聊天窗口会从0缩放到1,动画完成后显示一段打招呼的文本。如果需要隐藏聊天界面则是相反的操作。
func on_input_event(viewport, event,shapeidx):
if event.is_action_pressed("click") :
show_menu = not show_menu
toggle_menu(show_menu)
func show_text(text:String, time:float):
rich_text_label.text = text
var tw = create_tween()
tw.tween_property(rich_text_label,"visible_ratio",1,time).from(0)
show_text函数负责在聊天界面中显示文本- 首先将文本设置为传入的text字符串
- 然后创建一个Tween对象,并使用tween_property函数设置
visible_ratio属性的值从0到1,时间为time秒。这些会使文本类似打字机那样流式的显示出来。
func on_submitted(new_text:String):
llmapi.call_llm(new_text)
line_edit.text = ""
line_edit.editable = false
show_text("让我想想",0.5)
func on_finished(output:String):
line_edit.editable = true
show_text(output,1)
on_submitted函数负责处理用户提交的问题文本,调用call_llm函数来调用大模型,然后禁用输入控件,并显示文本。on_finished函数负责处理大模型返回的文本,启动输入控件,并显示文本。
运行程序后,一个背景透明的小狐狸宠物会悬浮出现在屏幕中央。用户可以对它进行右键拖动,左键点击后展开聊天界面。你可以在输入框中键入任何问题,它会用幽默、简洁的语气回答你。桌面宠物运行的效果如下图所示:

有时候,尽管你设置了窗口透明等选项,宠物还会带有黑色背景,没有完全的透明。可以尝试将项目渲染方式从forward+转变成Compatibility。
通过这个项目,展示了 Godot 引擎与大模型结合的创造力。你可以进一步丰富这个项目,将其拓展为任务提醒、情绪陪伴、语音互动等更丰富的功能。
本章小结
在本章中,我们首次将大语言模型引入 Godot 游戏开发的实践之中,开启了 AI 与游戏内容深度融合的新篇章。
我们首先简明介绍了大语言模型的工作原理、能力特点与多模态扩展,帮助你理解它不仅能处理文本,还能理解图像、音频乃至多轮上下文。随后,我们从四个角度梳理了大模型在游戏开发中的典型角色:创意设计助手、代码生成工具、美术灵感引擎与 NPC 对话模拟器,并配合实际提示词示例,帮助你掌握如何“向 AI 提问”。
更重要的是,我们通过一个完整的项目AI 桌面宠物,实战演示了如何在 Godot 中封装大模型调用接口、构建可交互的角色系统,并实现窗口透明、鼠标穿透、动画表现与对话驱动等完整功能。你已经掌握了:
- 如何使用 HTTPRequest 节点与外部 API 通信;
- 如何通过
ConfigFile管理密钥安全; - 如何设计自定义编辑器插件或组件结构;
- 如何让 Godot 项目脱离传统游戏界面,像真正的桌面应用一样运行;
- 如何将大模型真正嵌入你的游戏项目中,生成动态内容。
这一章不仅提升了你的技术广度,也为今后构建 AI 驱动的剧情系统、智能关卡助手、甚至自适应玩法设计系统打下了坚实基础。