乐于分享
好东西不私藏

02-Godot Ollama 插件化:大模型接入层设计

02-Godot Ollama 插件化:大模型接入层设计
题外话,我打算利用工作之余的时间,把Godot接入Ollama写成系列教程,连篇串成一个以Godot插件形式运行的简单演示项目。更新节奏看工作日忙不忙。不忙的时候尽量两三天一更,忙起来可能一周一更。对Godot引擎感兴趣的朋友可以点一波关注,如发现错误欢迎评论指正。

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(_promptString_historyArray[Dictionary]) -> Dictionary:    assert(false"chat() must be implemented by subclass")    return {"error""not_implemented"}# 流式聊天请求func chat_stream(_promptString_historyArray[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(backendBackendBase) -> void:    _backend = backend# 静态日志方法,任何地方直接 CompanionManager.log_info(...) 调用static func log_info(msgString) -> void:    print_rich("[color=cyan][INFO][/color] " + msg)static func log_error(msgString) -> void:    push_error("ERROR: " + msg)static func log_debug(msgString) -> 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


中途上车?系列教程已合集,快速补票可点下方合集快速导航,感兴趣的同学请点一波关注!谢谢。

相关学习资料