1. 项目概述:为什么要在Godot里集成Python?
如果你是一个从Python生态转战游戏开发的开发者,或者你的游戏项目需要用到一些强大的Python库(比如数据分析、机器学习、科学计算或者某些特定的网络协议库),那么直接在Godot里调用Python代码会是一个极具吸引力的想法。Godot内置的GDScript虽然易学易用,但在处理复杂算法、对接现有Python生态时,就显得有些力不从心。这时,为Godot开发一个Python扩展模块,就成了一条打通两个世界的“高速公路”。
这个项目的核心目标,不是简单地用Python脚本替换GDScript,而是创建一个能被Godot引擎原生识别、像其他GDScript或C#脚本一样,可以附加到节点上、拥有完整生命周期回调的自定义模块。这意味着你可以在Godot编辑器中直接创建继承自PythonScript的脚本,在_ready()、_process()里写Python代码,并且能安全、高效地与Godot的节点树、信号、资源系统进行交互。
我最初尝试这个方向,是因为需要在一个模拟经营游戏里集成一个轻量级的机器学习模型来做NPC的行为预测。用纯GDScript重写模型推理既不现实,效率也低。通过构建Python扩展,我成功地将训练好的TensorFlow Lite模型封装成了一个Godot节点,游戏运行时直接调用,性能和数据流都处理得非常优雅。下面,我就把从零开始构建一个可用、可靠的Godot Python扩展模块的全过程,包括我踩过的坑和总结的最佳实践,详细分享给你。
2. 环境准备与工具链选型
在动手写代码之前,搭好一个稳固的开发环境是成功的一半。Godot的扩展开发主要依赖C++和其自身的构建系统(SCons),而Python扩展则需要额外考虑Python的嵌入(Embedding)或扩展(Extending)问题。
2.1 核心工具与版本锁定
版本兼容性是第一个要跨过的坎。不同版本的Godot引擎其模块API可能有细微差别,Python的C API也在不断演进。
- Godot版本:我强烈建议使用Godot 4.x的稳定版本(如4.2.1)。4.x版本相比3.x在模块构建系统和GDExtension(新的扩展方式)上有了长足进步,文档也更完善。本项目基于传统的“模块(Module)”方式,它在4.x中依然被支持且对于深度集成更为合适。确保你从官网下载的是源码版本,而非预编译的编辑器。
- Python版本:选择Python 3.8到3.11之间的64位版本。太老的版本(如3.6)可能缺少某些API,太新的版本(如3.12)可能与Godot的构建脚本存在未知兼容性问题。关键点:务必使用“Development”安装选项。在Windows安装时勾选“Install for all users”和“Add Python to PATH”,最重要的是要勾选“Download debug binaries”和“Install debug binaries”吗?不,最重要的是勾选“py launcher”和“Install for all users”吗?不对,对于开发扩展,最重要的是在安装时或安装后,确保你有
python3x_d.lib(调试库)和python3x.lib(发布库)以及对应的头文件(include文件夹)。在Linux/macOS上,通常需要安装python3-dev或python3-devel包。 - 编译工具链:
- Windows:Visual Studio 2019或2022,并安装“使用C++的桌面开发”工作负载。MSVC编译器是必须的。
- Linux:GCC或Clang,以及
scons构建工具。通过包管理器安装build-essential。 - macOS:Xcode Command Line Tools,以及
scons。
注意:绝对不要使用Anaconda或Miniconda环境中的Python来编译。这些发行版可能修改了库的链接方式和路径,极易导致链接错误。使用从python.org下载的标准CPython发行版。
2.2 项目目录结构规划
清晰的目录结构能让后续的开发和维护事半功倍。假设你的Godot源码解压在了D:\godot-engine-4.2.1-stable,我建议在它旁边创建一个专门的工作区。
D:\godot-dev\ ├── godot-engine-4.2.1-stable\ # Godot源码 │ ├── modules\ # 官方和其他第三方模块 │ └── ... └── godot-python-module\ # 我们的Python扩展项目 ├── SCsub # SCons构建脚本 ├── config.py # 模块配置(可选) ├── register_types.cpp # 类型注册入口 ├── register_types.h ├── py_script.cpp # 核心:PythonScript类实现 ├── py_script.h ├── godot_python.h # 与Python C API交互的封装 ├── godot_python.cpp └── test_project\ # 用于测试的Godot项目 └── ...我们将把godot-python-module这个文件夹,整个复制或软链接到Godot源码的modules目录下。这样,Godot的SCons构建系统就能自动发现并编译它。
3. 核心原理:Godot模块与Python C API的桥梁
要理解如何开发,首先得明白Godot模块是如何工作的,以及Python如何被嵌入到C++应用中。
3.1 Godot模块系统浅析
Godot引擎是高度模块化的。每个模块都是一个独立的代码库,在引擎启动时被动态加载(或静态链接)。一个标准的Godot模块需要提供几个关键函数:
initialize_<module_name>_module:模块初始化时调用,用于向Godot的核心类DB注册本模块提供的所有类。uninitialize_<module_name>_module:模块卸载时调用,用于清理资源。- 一系列继承自Godot核心类(如
Object,RefCounted,Node)的C++类。
我们的目标就是创建一个名为PythonScript的类,它继承自Godot的Script类。这样,它就能被Godot的脚本语言系统管理,可以被设置给任何Object派生类(主要是Node)。
3.2 Python的嵌入(Embedding)模式
我们不是在用C扩展Python,而是将Python解释器嵌入到Godot这个C++应用程序中。这意味着:
- 在Godot启动时(或模块初始化时),我们需要调用
Py_Initialize()来启动Python解释器。 - 我们可以用C API(如
PyRun_SimpleString,PyObject_CallObject)来执行Python代码、调用Python函数、获取Python对象的值。 - 我们需要管理Python对象与Godot
Variant类型之间的转换。这是整个扩展中最复杂也最核心的部分。 - 在Godot关闭时,需要调用
Py_Finalize()来安全关闭解释器。
这种模式下,Python代码运行在同一个进程内,避免了进程间通信的开销,但也要非常小心内存管理和全局解释器锁(GIL)的问题。
3.3 数据类型转换:Variant vs PyObject
Godot使用Variant作为所有动态类型的通用容器(整数、浮点数、字符串、数组、字典、对象引用等)。Python则使用PyObject*。让两者互通,就需要一个转换层。
Godot -> Python:当从GDScript调用一个Python脚本的方法并传入参数时,我们需要将
Variant参数转换为对应的Python对象。例如:int->PyLong_FromLongfloat->PyFloat_FromDoubleString->PyUnicode_FromStringArray-> 遍历并递归转换,生成PythonlistDictionary-> 遍历并递归转换,生成PythondictObject*(Godot对象) -> 这是一个难点。通常我们需要将其包装成一个自定义的Python类型,这个类型内部持有一个Godot对象的弱引用或安全句柄。
Python -> Godot:当Python方法返回一个值,或者我们需要读取Python对象的属性给Godot时,需要进行反向转换。
PyLong->Variant(int)PyFloat->Variant(float)PyUnicode->Variant(String)PyList-> 遍历生成GodotArrayPyDict-> 遍历生成GodotDictionary- 自定义的Godot对象包装器 -> 解包出内部的
Object*,转换为Variant
实现一个健壮且完整的转换层是扩展稳定性的基石。初期可以只实现基本类型,后续再逐步扩展。
4. 分步实现:从空模块到可运行脚本
让我们从零开始,一步步构建出这个扩展模块。
4.1 第一步:创建模块骨架
在godot-python-module目录下,首先创建register_types.h和register_types.cpp。这是模块的入口。
register_types.h
#ifndef PYTHON_MODULE_REGISTER_TYPES_H #define PYTHON_MODULE_REGISTER_TYPES_H void initialize_python_module_module(); void uninitialize_python_module_module(); #endif // PYTHON_MODULE_REGISTER_TYPES_Hregister_types.cpp
#include "register_types.h" #include "py_script.h" // 我们即将创建的核心类 #include <gdextension_interface.h> #include <godot_cpp/core/class_db.hpp> #include <godot_cpp/core/defs.hpp> #include <godot_cpp/godot.hpp> using namespace godot; void initialize_python_module_module() { // 在这里注册我们模块提供的所有类 ClassDB::register_class<PythonScript>(); // 未来还可以注册其他类,比如PythonNode等 GD.Print("Python module initialized."); } void uninitialize_python_module_module() { // 进行必要的清理工作,例如释放Python解释器 GD.Print("Python module uninitialized."); } extern "C" { // GDExtension要求的入口点 GDExtensionBool GDE_EXPORT python_module_library_init( GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) { godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization); init_obj.register_initializer(initialize_python_module_module); init_obj.register_terminator(uninitialize_python_module_module); // 设置初始化级别,通常为MODULE init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_MODULE); return init_obj.init(); } }4.2 第二步:定义PythonScript核心类
创建py_script.h和py_script.cpp。这个类继承自Script,是扩展的心脏。
py_script.h
#ifndef PY_SCRIPT_H #define PY_SCRIPT_H #include <godot_cpp/classes/script.hpp> #include <godot_cpp/variant/variant.hpp> #include <godot_cpp/core/binder_common.hpp> namespace godot { class PythonScript : public Script { GDCLASS(PythonScript, Script) private: String source_code; // 存储Python源代码 String script_path; // 脚本文件路径 // 这里将来会存储编译后的PyCodeObject指针,或模块对象 protected: static void _bind_methods(); public: PythonScript(); ~PythonScript(); // 重写Script类的关键虚函数 virtual bool can_instantiate() const override; virtual Ref<Script> get_base_script() const override; virtual StringName get_global_name() const override; virtual bool inherits_script(const Ref<Script> &p_script) const override; virtual StringName get_instance_base_type() const override; virtual ScriptLanguage *get_language() const override; // 加载和设置源代码 void set_source_code(const String &p_code); String get_source_code() const; // 一个简单的测试方法,用于验证Python执行 Variant execute_string(const String &p_code); }; } // namespace godot #endif // PY_SCRIPT_Hpy_script.cpp (部分关键实现)
#include "py_script.h" #include "godot_python.h" // 我们将在这里封装Python C API调用 #include <godot_cpp/classes/global_constants.hpp> #include <godot_cpp/core/error_macros.hpp> namespace godot { void PythonScript::_bind_methods() { ClassDB::bind_method(D_METHOD("set_source_code", "code"), &PythonScript::set_source_code); ClassDB::bind_method(D_METHOD("get_source_code"), &PythonScript::get_source_code); ClassDB::bind_method(D_METHOD("execute_string", "code"), &PythonScript::execute_string); ADD_PROPERTY(PropertyInfo(Variant::STRING, "source_code"), "set_source_code", "get_source_code"); } PythonScript::PythonScript() { // 初始化成员变量 source_code = ""; } PythonScript::~PythonScript() { // 清理可能存在的Python资源 } bool PythonScript::can_instantiate() const { // 暂时返回true,表示可以附加到节点 return true; } StringName PythonScript::get_instance_base_type() const { // 这个脚本默认可以附加到任何Object上,但通常我们限定为Node // 返回“Node”会更准确 return "Node"; } void PythonScript::set_source_code(const String &p_code) { if (source_code == p_code) { return; } source_code = p_code; // TODO: 这里应该触发Python代码的编译或重新加载 } String PythonScript::get_source_code() const { return source_code; } Variant PythonScript::execute_string(const String &p_code) { // 这是我们的第一个突破口:直接执行一段Python字符串并返回结果 // 调用我们封装好的Python工具函数 return GodotPython::execute_string(p_code); } // 其他必须重写的虚函数,可以先返回默认值或空值 Ref<Script> PythonScript::get_base_script() const { return Ref<Script>(); } StringName PythonScript::get_global_name() const { return StringName(); } bool PythonScript::inherits_script(const Ref<Script> &p_script) const { return false; } ScriptLanguage *PythonScript::get_language() const { // 我们需要一个自定义的ScriptLanguage实例,初期可以先返回nullptr,后期实现 return nullptr; } } // namespace godot4.3 第三步:封装Python C API交互层
创建godot_python.h和godot_python.cpp。这个文件负责所有与Python解释器的直接交互,管理GIL,并进行类型转换。
godot_python.h
#ifndef GODOT_PYTHON_H #define GODOT_PYTHON_H #include <godot_cpp/variant/variant.hpp> #include <godot_cpp/core/error_macros.hpp> #include <Python.h> namespace godot { class GodotPython { public: // 初始化Python解释器(应在模块初始化时调用) static bool initialize(); // 关闭Python解释器(应在模块卸载时调用) static void finalize(); // 执行一段Python代码字符串,并返回结果(转换为Variant) static Variant execute_string(const String &p_code); // 工具函数:Variant 转 PyObject* static PyObject* variant_to_pyobj(const Variant &p_var); // 工具函数:PyObject* 转 Variant static Variant pyobj_to_variant(PyObject *p_obj); private: static bool is_initialized; // 获取全局解释器锁(GIL),确保线程安全 static void ensure_gil(); static void release_gil(); }; } // namespace godot #endif // GODOT_PYTHON_Hgodot_python.cpp (核心实现)
#include "godot_python.h" namespace godot { bool GodotPython::is_initialized = false; bool GodotPython::initialize() { if (is_initialized) { return true; } Py_Initialize(); if (!Py_IsInitialized()) { ERR_FAIL_V_MSG(false, "Failed to initialize Python interpreter."); } is_initialized = true; GD.Print("Python interpreter initialized successfully."); return true; } void GodotPython::finalize() { if (is_initialized) { Py_Finalize(); is_initialized = false; GD.Print("Python interpreter finalized."); } } Variant GodotPython::execute_string(const String &p_code) { if (!is_initialized) { ERR_FAIL_V_MSG(Variant(), "Python interpreter not initialized."); } // 确保我们持有GIL PyGILState_STATE gstate = PyGILState_Ensure(); Variant ret; PyObject *main_module = PyImport_AddModule("__main__"); PyObject *global_dict = PyModule_GetDict(main_module); PyObject *local_dict = PyDict_New(); // 将Godot的print函数暴露给Python(可选,但非常有用) // 这里需要先实现一个C函数包装godot::print_line,略复杂,先跳过。 // 编译并执行代码 PyObject *py_code_obj = Py_CompileString(p_code.utf8().get_data(), "<godot>", Py_file_input); if (py_code_obj) { PyObject *temp = PyEval_EvalCode(py_code_obj, global_dict, local_dict); Py_DECREF(py_code_obj); if (temp) { // 执行成功,但通常模块级代码返回None // 我们可以尝试获取最后一个表达式的值?这很复杂。 // 简单起见,我们返回一个表示执行成功的布尔值。 ret = true; Py_DECREF(temp); } else { // 执行出错,获取错误信息 PyObject *type, *value, *traceback; PyErr_Fetch(&type, &value, &traceback); if (value) { PyObject *str_obj = PyObject_Str(value); if (str_obj) { const char *err_msg = PyUnicode_AsUTF8(str_obj); ERR_PRINT(String("Python execution error: ") + err_msg); Py_DECREF(str_obj); } } PyErr_Restore(type, value, traceback); PyErr_Print(); // 打印到标准错误 ret = false; } } else { PyErr_Print(); ERR_PRINT("Failed to compile Python code."); ret = false; } Py_DECREF(local_dict); PyGILState_Release(gstate); return ret; } // 以下是类型转换的简化实现(仅示例基本类型) PyObject* GodotPython::variant_to_pyobj(const Variant &p_var) { switch (p_var.get_type()) { case Variant::NIL: Py_RETURN_NONE; case Variant::BOOL: return PyBool_FromLong((int64_t)p_var); case Variant::INT: return PyLong_FromLong((int64_t)p_var); case Variant::FLOAT: return PyFloat_FromDouble((double)p_var); case Variant::STRING: { String s = p_var; return PyUnicode_FromString(s.utf8().get_data()); } // 数组、字典等复杂类型需要递归处理,此处省略... default: // 对于无法直接转换的类型,返回None或抛出错 WARN_PRINT(String("Unsupported Variant type for Python conversion: ") + Variant::get_type_name(p_var.get_type())); Py_RETURN_NONE; } } Variant GodotPython::pyobj_to_variant(PyObject *p_obj) { if (p_obj == nullptr || p_obj == Py_None) { return Variant(); } if (PyBool_Check(p_obj)) { return Variant(p_obj == Py_True); } if (PyLong_Check(p_obj)) { return Variant((int64_t)PyLong_AsLongLong(p_obj)); } if (PyFloat_Check(p_obj)) { return Variant(PyFloat_AsDouble(p_obj)); } if (PyUnicode_Check(p_obj)) { const char *c_str = PyUnicode_AsUTF8(p_obj); return Variant(String(c_str)); } // 处理列表、元组、字典等... 此处省略 WARN_PRINT("Unsupported Python object type for Variant conversion."); return Variant(); } } // namespace godot4.4 第四步:编写SCons构建脚本
创建SCsub文件,告诉SCons如何编译我们的模块。
# SCsub Import('env') # 添加模块源码 srcs = [ "register_types.cpp", "py_script.cpp", "godot_python.cpp", ] # 添加包含路径 # 假设Python安装在标准位置,这里需要根据你的系统调整 # Windows示例,路径可能需要修改 python_prefix = ARGUMENTS.get('python_prefix', 'C:/Python311') python_include = python_prefix + '/include' python_libs = python_prefix + '/libs' # 将模块添加到构建环境 env_thirdparty = env.Clone() env_thirdparty.add_source_files(env.modules_sources, srcs) # 添加Python头文件路径 env_thirdparty.Append(CPPPATH=[python_include]) env_thirdparty.Append(LIBPATH=[python_libs]) # 链接Python库 # 注意库名,debug版本可能叫python311_d python_lib_name = 'python311' if env['target'] == 'debug': python_lib_name += '_d' # 检查你的Python安装是否有调试库 env_thirdparty.Append(LIBS=[python_lib_name])4.5 第五步:集成到Godot并编译
将整个
godot-python-module文件夹复制到Godot源码的modules目录下。打开终端(或VS Developer Command Prompt),导航到Godot源码根目录。
执行SCons编译命令。关键点:需要指定
python_prefix参数。- Windows (VS2022):
scons platform=windows target=editor dev_build=yes -j8 python_prefix="C:/Python311" - Linux:
scons platform=linux target=editor dev_build=yes -j8 python_prefix="/usr" - macOS:
scons platform=macos target=editor dev_build=yes -j8 python_prefix="/usr/local/Frameworks/Python.framework/Versions/3.11"
dev_build=yes会启用更多调试信息,便于排查问题。-j8表示使用8个线程并行编译,根据你的CPU核心数调整。- Windows (VS2022):
编译成功后,会在
bin目录下生成godot.<platform>.tools.<target>(如godot.windows.tools.editor.exe)。这就是集成了我们Python模块的Godot编辑器。
4.6 第六步:在Godot编辑器中测试
- 运行编译好的自定义Godot编辑器。
- 创建一个新项目或打开测试项目。
- 在资源管理器里,右键 ->
创建新资源。如果你在register_types.cpp中正确注册了PythonScript,你应该能在列表里找到它(可能叫PythonScript或Python Script)。 - 创建一个
PythonScript资源,选中它,在检查器面板里找到source_code属性,输入一段简单的Python代码,比如:print("Hello from Python inside Godot!") for i in range(5): print(f"Counting: {i}") - 创建一个简单的
Node,在其检查器面板的Script属性中,选择你刚创建的PythonScript资源。 - 运行场景。如果一切顺利,你暂时还看不到输出,因为我们还没有将Python的
print重定向到Godot的输出面板。但是,如果代码有语法错误,我们的execute_string方法会通过ERR_PRINT将错误信息打印到Godot编辑器底部“输出”面板的“错误”页签。
实操心得:第一次编译十有八九会失败,最常见的问题是找不到Python头文件或库。仔细检查
SCsub中的python_prefix路径是否正确,以及python_lib_name是否与你的Python版本匹配(python311, python310等)。在Windows上,确保链接的是python311.lib而不是python3.lib。
5. 进阶实现:让Python脚本“活”起来
上面的步骤只是让Godot认识了一个可以执行字符串的PythonScript类。一个真正可用的脚本模块,还需要实现以下关键功能:
5.1 实现ScriptLanguage子类
要让Godot将.py文件识别为一种脚本语言,并支持在编辑器中创建、编辑、继承,必须创建一个继承自ScriptLanguage的类(例如PythonLanguage)。这是一个庞大的工程,需要实现数十个虚函数,包括:
get_name(): 返回语言名称(如“Python”)。init()/finish(): 语言初始化和清理。get_template(): 提供新脚本的模板代码。validate(): 验证脚本语法。complete_code(): 代码补全。find_function(): 查找函数定义。- 最重要的是
get_global_class_name()和can_inherit_from_file(),这关系到资源继承系统。
对于初期原型,可以暂时不完整实现ScriptLanguage,而是让PythonScript的get_language()返回nullptr。但这会限制功能,例如无法通过“创建脚本”菜单直接创建.py文件。
5.2 实现脚本实例与方法调用
PythonScript继承自Script,它需要管理脚本实例。当脚本被附加到一个Godot对象(如Node)时,应该:
- 编译源代码:将Python源代码字符串编译成
PyCodeObject,或者更常见的是,将其作为一个模块加载(使用PyImport_ExecCodeModule)。 - 创建脚本实例:在Python端,这个“实例”可以是一个模块字典,或者一个自定义类的对象。我们需要在Godot的
ScriptInstance对象和Python对象之间建立映射。 - 处理方法调用:当GDScript或引擎调用该脚本实例的某个方法(如
_ready)时,PythonScript需要找到对应的Python函数(通过属性查找getattr)并使用转换后的参数调用它,最后将返回值转换回Variant。 - 处理属性访问:类似地,需要拦截对脚本属性的获取(
get)和设置(set)操作,并将其映射到Python对象的属性上。
这涉及到实现ScriptInstance类。一个简化的流程是:在PythonScript中,为每个附加它的Godot对象创建一个对应的、弱引用的Python字典(作为命名空间)。当调用该对象的方法时,就在那个字典的上下文中执行相应的Python函数。
5.3 信号与属性导出
Godot强大的信号(Signal)和导出属性(Export)系统也需要对接。
- 信号:可以在Python脚本中定义Godot信号吗?一种思路是,在
PythonScript的_bind_methods中预定义一些信号,然后提供Python函数来emit它们。更高级的做法是允许在Python代码中使用装饰器或特定语法来声明信号,然后在编译/加载时动态地添加到Godot类中。 - 导出属性:同样困难。需要在Python源码中解析特殊注释(如
# @export var speed: float),然后在Godot端通过_get_property_list等方法动态添加这些属性。这需要实现一个Python源码解析器(可以是简单的基于正则表达式,也可以集成ast模块进行静态分析)。
6. 常见问题、调试技巧与性能优化
在开发过程中,你肯定会遇到各种奇怪的问题。以下是我总结的一些常见坑点和解决思路。
6.1 编译与链接问题
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
fatal error C1083: Cannot open include file: 'Python.h' | SCons找不到Python头文件。 | 检查SCsub中python_include路径。确保路径中存在Python.h。在Windows上,路径通常是C:\Python311\include。 |
LNK2019: unresolved external symbol __imp__Py_Initialize | 链接器找不到Python库。 | 检查SCsub中python_libs路径和python_lib_name。确保链接的是正确的库文件(如python311.lib)。在Windows上,Debug构建可能需要链接python311_d.lib。 |
undefined reference toPy_Initialize'` (Linux/macOS) | 类似Windows的链接错误。 | 确保LIBS列表里包含了正确的Python库名(如-lpython3.11)。使用pkg-config --libs python3.11来获取正确的链接参数,并整合到SCsub中。 |
| 编译成功但编辑器启动崩溃 | Python运行库版本不匹配或路径错误。 | 确保Godot编辑器运行时能找到对应的Python DLL(Windows)或SO(Linux)。可以将Python安装目录添加到系统PATH,或者将必要的DLL复制到Godot编辑器同级目录。 |
调试技巧:在
godot_python.cpp的initialize()函数开始和结束处添加GD.Print。如果连开始的信息都没打印,说明模块根本没被加载,检查register_types.cpp的初始化函数是否被正确调用。如果打印了开始但没打印成功,说明Py_Initialize()失败了,可能是Python环境损坏。
6.2 运行时崩溃与内存错误
这是嵌入Python时最头疼的问题,多与引用计数和GIL有关。
- 引用计数错误:Python使用引用计数管理内存。
Py_INCREF和Py_DECREF必须成对出现。一个常见错误是,将Python函数返回的“借用引用”(borrowed reference)当作“新引用”(new reference)来处理,导致过早DECREF或漏掉INCREF。黄金法则:除了明确说明返回新引用的API(如PyLong_FromLong),其他返回PyObject*的API(如PyDict_GetItemString)通常都是借用引用,如果你需要长期持有它,必须Py_INCREF它。 - 全局解释器锁(GIL):Python解释器不是线程安全的。任何调用Python C API的代码,如果可能从非创建Python解释器的线程调用,必须先获取GIL(
PyGILState_Ensure),并在调用结束后释放(PyGILState_Release)。Godot的部分回调(如_process)可能发生在其他线程,如果不加锁,100%会导致随机崩溃。 - Godot对象生命周期:Python中持有对Godot对象的引用时,必须使用
Ref<>或弱引用。如果Godot对象先于Python包装器被销毁,而Python代码还试图访问它,就会访问野指针导致崩溃。建议在Python端使用Godot提供的WeakRef对象。
6.3 性能优化考量
- 避免频繁的C++/Python边界穿越:每次调用Python函数、获取/设置属性,都有开销。对于在
_process中每帧都要调用的逻辑,应尽量在C++侧完成,或者将批量操作打包在一次Python调用中。 - 缓存Python对象:不要每次调用都通过
PyObject_GetAttrString查找函数。在脚本初始化时,就将常用的函数对象(如_ready,_process)查找出来并缓存为PyObject*。 - 使用PyPy或C扩展?CPython在数值计算上较慢。如果扩展的核心是密集计算,可以考虑:
- 将计算核心用C/C++写成Godot模块,Python只做胶水。
- 使用
numpy等已高度优化的C扩展库。确保你的嵌入环境能正确导入这些库。 - 理论上可以嵌入PyPy,但集成复杂度极高,不推荐。
6.4 让Python的print输出到Godot编辑器
这是一个提升开发体验的重要功能。我们需要重定向Python的sys.stdout和sys.stderr。
// 在godot_python.cpp中新增一个函数 static PyObject* godot_print(PyObject* self, PyObject* args) { const char* msg = nullptr; if (!PyArg_ParseTuple(args, "s", &msg)) { return nullptr; } // 使用Godot的打印函数,可以带颜色等信息 Godot::print(msg); // 也可以打印到编辑器输出面板 // UtilityFunctions::print(msg); Py_RETURN_NONE; } static PyMethodDef GodotPythonMethods[] = { {"print", godot_print, METH_VARARGS, "Print to Godot's output."}, {nullptr, nullptr, 0, nullptr} }; static struct PyModuleDef GodotPythonModule = { PyModuleDef_HEAD_INIT, "godot_output", nullptr, -1, GodotPythonMethods }; PyMODINIT_FUNC PyInit_godot_output(void) { return PyModule_Create(&GodotPythonModule); } // 在initialize()函数中,初始化解释器后添加: PyImport_AppendInittab("godot_output", &PyInit_godot_output); // 然后执行一段Python代码,将我们的print替换sys.stdout const char* redirect_code = "import sys\n" "import godot_output\n" "class GodotWriter:\n" " def write(self, msg):\n" " godot_output.print(msg)\n" " def flush(self):\n" " pass\n" "sys.stdout = GodotWriter()\n" "sys.stderr = GodotWriter()\n"; PyRun_SimpleString(redirect_code);这样,Python脚本中的print(“Hello”)就会显示在Godot编辑器的“输出”面板中了。
开发Godot的Python扩展是一个深入引擎内部和Python解释器的旅程,充满了挑战,但一旦打通,它将为你的游戏开发打开一扇新的大门。从最简单的字符串执行开始,逐步实现类型转换、方法调用、信号连接,最终形成一个功能完备的脚本系统。这个过程需要耐心调试和对两个系统内存模型的深刻理解。我建议从一个非常具体、有限的目标开始(比如“在Godot中调用一个Python函数计算斐波那契数列”),成功后再逐步扩展,而不是试图一开始就构建一个完整的脚本语言集成。