技术分析
import 路径
最后测试,我们要用 dll 的形式而非现存的解释器,需要经过以下操作:

cmake这样设置就不会用解释器了,但是需要我们使用(我们使用的是python3.11版本):python311.lib python311.dll 添加python的include路径以让pybind可以include LIb/encodings(放在和exe同一路径)
这是因为,pybind会首先添加搜索路径exe的路径,而py的搜索路径为sys.path,sys是解释器内包含的。
而py初始化需要encodings模块,所以我们和exe放一起以便py找得到。encodings里面是各种py的内建类。
代码绑定
我们本来想参考这个库:
https://github.com/trevday/eternal2d-engine
但是使用 PYBIND11_EMBEDDED_MODULE 单独在 cpp 中,调用py的时候却不能 import,并没有生效。
所以最后改成头文件的形式,PythonScriptSystem 里面再去包含这个头文件。
Stub
项目使用 pybind11,想要生成 stub 代码提示出来,可以参考:
https://github.com/sizmailov/pybind11-stubgen
https://github.com/robotpy/robotpy-build/issues/1#issuecomment-663871072
接口设计
HEngine模块设计(前面代码绑定的补充)
我们希望把所有功能全部封装在HEngine模块中,例如 Input 等都是作为其子模块存在,并且为了不用dll的形式把C++编写的模块给python调用,我们使用 PYBIND11_EMBEDDED_MODULE 直接给 python 获取模块,而 Input 等则在其中定义子模块(Pybind11 不允许两个模块同时存在,我们一个模块必须全部定义完),因此我改成了在别的头文件使用函数,在HEngine模块的声明中调用此函数,如:
// in CoreBinding.h
PYBIND11_EMBEDDED_MODULE(HEngine, HEngineModule)
{
py::class_<Timestep>(HEngineModule, "Timestep")
.def(py::init<float>())
.def("GetSeconds", &Timestep::GetSeconds);
InputBinding(HEngineModule);
LogBinding(HEngineModule);
}上面的代码在 CoreBinding.h 中,这个头文件又会 include 其他的头文件如 InputBinding.h,而具体的 InputBinding 函数则为:
// in InputBinding.h
static void InputBinding(py::module_& HEngineModule)
{
py::module_ InputModule = HEngineModule.def_submodule("Input");
InputModule.def("IsKeyPressed", &Input::IsKeyPressed);
...
}脚本设计
# in MoveRight.py
import numpy as np
import HEngine
class Props():
step = 5.0
ignore__ = 111
handsome = "hbh"
class MoveRight():
def __init__(self) -> None:
pass
def Start():
return
def Update(ts):
return
def Stop():
return
def UpdateInEditor(ts):
returnProps 全局类里面定义的变量会暴露在编辑器中,可以在编辑器设置值,并最终序列化保存到文件中(Entity的PythonScriptComponent里面),反序列化加载的时候再设定回来。
类名 MoveRight 必须和文件名一致,这是模仿了unity(mono)的设计,这样我们就可以在引擎层根据文件名(这会作为路径索引保存在 PythonScriptComponent 中)去获取到类名,从而实例化这个类出来,并调用其方法。
Start 和 Stop 函数分别在游戏开始和结束时调用一次,Update 每帧调用一次,特别地我设计了一个 UpdateInEditor 接口,PythonScriptComponent 有一个 static 的 UseEditor 的 bool 变量来作为是否开启这个函数的开关,方便我们直接在编辑器下调试。
pybind11
模板函数
因为模板的两阶段编译,我们必须实例化出来才能传给python,可以如下这样使用重载:

根据字符串获取component
返回类型是各个component,而使用模板作为返回值,我们无法推断模板参数是什么(注意我们是要根据字符串获取这个返回值,例如 GetComponent<Transform>,他的返回值就应该是一个 TransformComponent。
因此可以用 std::variant ,但是 variant 不能使用引用,这是因为引用必须要有初始化,而 variant 根本不知道怎么初始化!因此可以用指针来模拟,最终写了这样的代码:
py::class_<Entity>(HEngineModule, "Entity")
.def(py::init())
.def("GetComponent", [](Entity& entity, const std::string& compName) -> std::variant<std::reference_wrapper<TransformComponent>, std::reference_wrapper<PythonScriptComponent>> {
if (compName == "Transform")
return entity.GetComponent<TransformComponent>();
else if (compName == "PythonScript")
return entity.GetComponent<PythonScriptComponent>();
return entity.GetComponent<TransformComponent>();
});Component自动绑定
https://github.com/hebohang/HEngineDev/commit/bdf54859d8ee7a17a67e9598dd6dec6ebfbd44ae


codegen出来所需的 PyBinding 方法,但是目前的问题是,要检测是否 codegen 需要根据类名判断,类名包含 Component 表示这是一个组件,因而生成对应方法。
当然这样子每个类都需要有此方法的声明,可以写到宏里面:

最后简单几句代码就能实现完全的所有的 Component 的绑定:

Last updated