OK,废话讲完。言归正传,这篇的目标是:把上篇的代码拆分重构,升级成 Godot 插件。骨架搭好、配置项就绪,下一篇接上 Ollama 就能跑。
回顾:上一篇的 OllamaClient
Godot Ollama 本地大模型接入:零基础从安装到 API 跑通(附源码)
上一篇写了一个 OllamaClient.gd,在 Godot 接入 Ollama 并跑通对话。
extendsNodeconstBASE_URL:="http://127.0.0.1:11434"constMODEL:="qwen2.5:0.5b-instruct-q4_0"funcchat(user_message:String)->void:_send("/api/chat",{model=MODEL,messages=[{role="user",content=user_message}],stream=false})三个问题:1、MODEL 和 BASE_URL 写死在代码里;2、日志用 print 散落各处;3、整个逻辑塞一个文件。插件化全部解决。
话说回来。
先花一分钟介绍 Godot 插件的几个基本概念
一个 Godot 插件就是一个 addons/<插件名>/ 目录,里面最少两个文件。
- plugin.cfg
:声明插件名、版本、作者。 - plugin.gd
:继承 EditorPlugin,Godot 在启用和禁用插件时分别调_enter_tree()和_exit_tree()。加@tool注解后这两个方法在编辑器里也能跑。
插件能做的最重要的两件事:注册 autoload(全局单例,任何场景里直接调)和注册 Project Settings(配置项,编辑器对话框里改)。我这个 AI 插件用的就是这两样。
好了,概念啰嗦完,直接看代码。
插件骨架:四个文件
写完四个文件之后,目录长这样:

嗯,四个文件看着多,其实四种职责,拆开看就清楚了。
文件 1:plugin.cfg
[plugin]name="XX"description="XX"author="XX"version="0.2.0"script="plugin.gd"
script="plugin.gd" 告诉引擎去哪找入口。剩下四行展示用。就这一个文件,Godot 就知道这是插件。
文件 2:plugin.gd
插件启用时调 _enter_tree(),禁用时调 _exit_tree()。做两件事:注册全局管理器、注册配置项。
@toolextends EditorPluginconst AUTOLOAD_NAME := "CompanionManager"const AUTOLOAD_PATH := "res://addons/ai-companion/companion_manager.gd"func _enter_tree() -> void:_add_autoload()_add_project_settings()func _exit_tree() -> void:_remove_autoload()
@tool 让脚本在编辑器里也能跑。
注册 autoload:
func _add_autoload() -> void:# 避免重复注册if not ProjectSettings.has_setting("autoload/" + AUTOLOAD_NAME):add_autoload_singleton(AUTOLOAD_NAME, AUTOLOAD_PATH)
add_autoload_singleton 是 EditorPlugin 内置方法。调用后 project.godot 自动加一行:
[autoload]CompanionManager="*res://addons/ai-companion/companion_manager.gd"
禁用插件时同样要反向移除,不然卸载插件后编辑器报错。_remove_autoload 调的是对应的 remove_autoload_singleton。
之后任何地方写 CompanionManager.log_info("...") 就能用。代替上一篇的 print("[OllamaClient] ...")。
注册 Project Settings:
func _add_project_settings() -> void:_add_setting("companion/settings/default_model", "qwen2.5:7b", TYPE_STRING)_add_setting("companion/settings/ollama_url", "http://localhost:11434", TYPE_STRING)func _add_setting(p_name: String, p_default: Variant, p_type: int) -> void:# 写入配置项:设置当前值、标记默认值、声明类型、设为可见if not ProjectSettings.has_setting(p_name):ProjectSettings.set_setting(p_name, p_default)ProjectSettings.set_initial_value(p_name, p_default)ProjectSettings.add_property_info({"name": p_name, "type": p_type})ProjectSettings.set_as_basic(p_name, true)
换模型换端口不用翻源码。在 Project Settings 面板的 General 页搜索 companion(文末附图),两个配置项直接改 -- 骨架部分就绪,后面只管写逻辑。
文件 3:大模型接入层 -- backend_base.gd
核心问题来了 -- 调用哪个大模型推理引擎?写死调 Ollama 最简单,但如果后续如果需要接llama.cpp,得把所有调用方翻一遍。
我的思路是写一个抽象接口。多写一个文件,以后换引擎只加文件,上层不动。(程序员的肌肉记忆:碰到多种实现先抽接口,没毛病)

class_name BackendBaseextends RefCounted# 发送聊天请求,返回完整响应func chat(_prompt: String, _history: Array[Dictionary]) -> Dictionary:assert(false, "chat() must be implemented by subclass")return {"error": "not_implemented"}# 流式聊天请求func chat_stream(_prompt: String, _history: Array[Dictionary]) -> Dictionary:assert(false, "chat_stream() must be implemented by subclass")return {"error": "not_implemented"}# 返回可用模型列表func get_models() -> Array[String]:assert(false, "get_models() must be implemented by subclass")return []# 检查引擎是否可用func is_available() -> bool:assert(false, "is_available() must be implemented by subclass")return false
四个方法,每个只有一个 assert(false) -- 子类忘了实现会直接报错,比静默返回假数据可靠。(GDScript 没 abstract 关键字,只能这么凑合,习惯就好)
CompanionManager 持有 BackendBase 类型的引用,不关心底层是 OllamaBackend 还是 LlamaCppBackend。两个具体实现都遵循同一个接口,运行时想切就切。
extends RefCounted 而不是 Node:上一篇用 Node 是为了 add_child(http)。后面用同步 HTTPClient,不需要挂树。RefCounted 更轻(行吧,解释起来就两行代码的事)。
文件 4:companion_manager.gd
extends Node# 当前活跃的推理引擎,启动时由 Demo 脚本注入var _backend: BackendBase = nullfunc get_backend() -> BackendBase:return _backendfunc set_backend(backend: BackendBase) -> void:_backend = backend# 静态日志方法,任何地方直接 CompanionManager.log_info(...) 调用static func log_info(msg: String) -> void:print_rich("[color=cyan][INFO][/color] " + msg)static func log_error(msg: String) -> void:push_error("ERROR: " + msg)static func log_debug(msg: String) -> void:print_rich("[color=gray][DEBUG][/color] " + msg)
行吧,还剩最后一个文件。全局总管,负责引擎切换和日志输出。不写 class_name -- autoload 本身就是全局单例,加了会冲突。三个 log 方法都是 static,任何地方直接调。引擎切换通过 set_backend() 完成,演示项目启动时调 CompanionManager.set_backend(OllamaBackend.new()) 就行。
嗯,就这么点东西。四个文件,plugin.cfg 声明身份,plugin.gd 注册 autoload 和配置项,backend_base.gd 定接口规范,companion_manager.gd 管全局。
写完就可以在 Godot 里启用。换引擎、换端口都在编辑器里改,不用翻源码。



🫣 点一下关注,比 star 一个 repo 还快,而且不花钱。
#Godot #插件开发 #GDScript #独立开发 #AI
中途上车?系列教程已合集,快速补票可点下方合集快速导航,感兴趣的同学请点一波关注!谢谢。
夜雨聆风