142 lines
6.5 KiB
Markdown
142 lines
6.5 KiB
Markdown
# LeanCLR Godot
|
||
|
||
浅尝一下 LeanCLR 跑 Godot 4 脚本。
|
||
(目前是娱乐向,请勿用于生产环境,建议等官方绑定)
|
||
|
||
原版不带Mono Godot加载GDExtension,C# 代码交给 LeanCLR 解释执行。现在 demo 里可以把 `.cs` 直接挂到节点上,也可以在游戏窗口里的 CodeEdit 改 C#,点运行后编译一个新 assembly,然后不重启切过去。
|
||
|
||
已完成:
|
||
|
||
- Godot API 绑定尽量全量生成,热更代码后面会直接用
|
||
- C# 脚本资源能在编辑器里加载、保存、挂节点
|
||
- 运行时热重载保留字段状态,接近 Python/Lua 那种 reload 手感
|
||
- demo 是一个简单 Flappy Bird,用来测输入、Process、状态迁移和重载
|
||
|
||
## 编译
|
||
|
||
拉子模块:
|
||
|
||
```bash
|
||
git submodule update --init --recursive
|
||
```
|
||
|
||
LeanCLR 默认会把所有未实现的 icall 视为致命错误并 abort。本项目对接的 Godot API 只是一小部分,所以必须先把这个开关关掉,否则跑 demo 时第一碰到未实现接口就崩。修改 `thirdparty/leanclr/src/runtime/build_config.h`:
|
||
|
||
```c
|
||
-#define LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR 1
|
||
+#define LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR 0
|
||
```
|
||
|
||
> 这个 patch 在子模块里,`git submodule update` 之后需要重新打。
|
||
|
||
编 native 扩展:
|
||
|
||
```bash
|
||
cmake -S . -B build-master
|
||
cmake --build build-master --config Debug --target leanclr_godot
|
||
```
|
||
|
||
> 第一次 `cmake -S . -B build-master` 会通过 FetchContent 拉 godot-cpp 4.5,但此时 `src/generated/godot_api.generated.cpp` 还不存在,配置会失败。所以顺序是:
|
||
>
|
||
> ```bash
|
||
> cmake -S . -B build-master
|
||
> python3 tools/binding_generator/generate_bindings.py --api build-master/_deps/godot-cpp-src/gdextension/extension_api.json
|
||
> cmake -S . -B build-master # 再配一次,让 add_library 看到生成的 cpp
|
||
> cmake --build build-master --config Debug --target leanclr_godot
|
||
> ```
|
||
|
||
> `GODOT_CPP_BRANCH` 默认是 `4.5`,仓库里已经为 4.5 打过两个补丁(见下方「已知补丁」)。
|
||
|
||
编 demo 的 C#:
|
||
|
||
```bash
|
||
dotnet msbuild managed/GodotSharpCompat/GodotSharpCompat.csproj -p:Configuration=Debug
|
||
dotnet msbuild project/Game.csproj -p:Configuration=Debug
|
||
```
|
||
|
||
> `dotnet msbuild` 只能用单横线参数(`-p:...`),双横线(`/p:...`)会报 `MSB1008: 只能指定一个项目`。
|
||
> Windows 上要用 `python3`,别用 `python`(会被 Microsoft Store 别名劫持弹出安装器)。
|
||
|
||
## 运行
|
||
|
||
```bash
|
||
"/path/to/Godot" --path project
|
||
```
|
||
|
||
> 把 `/path/to/Godot` 换成你机器上实际的 Godot 可执行文件路径。这台机器是 `D:/Projects/godot-build/build/godot.exe`,Godot 4.6.3。
|
||
|
||
## 已知补丁
|
||
|
||
### `thirdparty/leanclr` 子模块
|
||
|
||
- `src/runtime/build_config.h`:`LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR` 设为 `0`(见上文)。
|
||
|
||
### `src/leanclr_script.cpp`
|
||
|
||
godot-cpp 4.5 把 `gdextension_interface_script_instance_create3` 从 `gdextension_interface::` 命名空间搬到了 `godot::internal::` 里,且原来的 `<godot_cpp/core/gdextension_interface_loader.hpp>` 头文件已经不存在。要把:
|
||
|
||
```cpp
|
||
#include <godot_cpp/core/gdextension_interface_loader.hpp>
|
||
// ...
|
||
void* script_instance = gdextension_interface::script_instance_create3(&script_instance_info(), instance);
|
||
```
|
||
|
||
改成:
|
||
|
||
```cpp
|
||
#include <godot_cpp/godot.hpp>
|
||
// ...
|
||
void* script_instance = ::godot::internal::gdextension_interface_script_instance_create3(&script_instance_info(), instance);
|
||
```
|
||
|
||
### `tools/binding_generator/generate_bindings.py`
|
||
|
||
godot-cpp 4.5 把 `ClassDB` 整体重命名成了 `ClassDBSingleton` 单例包装。生成器原本的 `CPP_HEADER_NAME_OVERRIDES["ClassDB"] = "class_db_singleton"` 只重命名了头文件路径,C++ 类名仍是 `ClassDB`,编译会爆 `get_class_static` 不存在。补丁:新增 `SINGLETON_CLASS_CPP_NAMES = {"ClassDB": "ClassDBSingleton"}`,让生成出的 C++ 走 `ClassDBSingleton::get_singleton()->method(...)`,C# 侧 `ClassDB` 的方法全部变成 `static`。
|
||
|
||
### `CMakeLists.txt`(Web 构建)
|
||
|
||
仓库里已经为 Web 构建持久化地打了两个补丁,都在 `if(EMSCRIPTEN)` 块(或紧邻它的 `option` 块)里:
|
||
|
||
1. **强制单线程**(Web 当前不支持多线程,且 `-sSIDE_MODULE + pthreads` 在 emcc 里是实验性的):
|
||
|
||
```cmake
|
||
if(EMSCRIPTEN)
|
||
set(GODOTCPP_THREADS OFF CACHE BOOL "godot-cpp threading support" FORCE)
|
||
endif()
|
||
```
|
||
|
||
2. **降到 `-O1`**:自动生成的 `godot_api.generated.cpp` 里有几千个 invoker 函数,`-O3` 下编译器会把参数拆包全部内联,单个函数会塞进 11 万+ 局部变量,浏览器加载时会报 `Uncaught (in promise) CompileError: WebAssembly.instantiate(): Compiling function #N failed: local count too large`(V8 单函数局部变量上限 50,000)。`leanclr_godot` target 在 `if(EMSCRIPTEN)` 块里被显式加了 `target_compile_options(leanclr_godot PRIVATE -O1)`。
|
||
|
||
Web 构建方法:
|
||
|
||
```bash
|
||
# 需要 Emscripten 3.1+、Ninja 都在 PATH 上
|
||
rm -rf build-web
|
||
cmake -S . -B build-web -G Ninja \
|
||
-DCMAKE_TOOLCHAIN_FILE="<emsdk>/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake" \
|
||
-DCMAKE_BUILD_TYPE=Release
|
||
cmake --build build-web --target leanclr_godot
|
||
```
|
||
|
||
> 这台机器 Emscripten 在 `D:/Projects/emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake`。
|
||
> `leanclr.gdextension` 已经配了 `web.release.wasm32 = "res://bin/Release/leanclr_godot.wasm"`,构建产物会自动落到 `project/bin/Release/libleanclr_godot.wasm`。要跑起来用编辑器导出一份 Web 即可(或 `godot --headless --export-release "Web" output.html` 配 `project/export_presets.cfg`)。
|
||
>
|
||
> 注意事项:
|
||
> - `-O1` 让 wasm 从 `-O3` 时的 26 MB 涨到 10 MB 左右(Release 去掉符号),是必要的代价。
|
||
> - `leanclr_wasm_libcxx_shim.cpp` 提供 Emscripten libc++ 缺的 `std::__1::__hash_memory` 符号,Web 链接时会自动包含。
|
||
> - `em++: warning: ignoring unsupported linker flag: -soname` 是无害的,`-soname` 是 ELF 链接器标志,emcc 不识别但 wasm 产物仍然正确。
|
||
|
||
## 几个常看的文件
|
||
|
||
`src/` : GDExtension 和 bridge。
|
||
|
||
`managed/GodotSharpCompat/` : 给 C# 用的 Godot API 外观。`Generated/` 子目录是绑定生成器的产物,`.gitignore` 已忽略。
|
||
|
||
`project/runtime_hot_reload_demo.tscn` : 现在的主 demo。
|
||
|
||
`project/scripts/HotReloadSmoke.cs` : Flappy 的 C# 脚本。
|
||
|
||
`project/scripts/RuntimeCSharpEditor.gd` : 游戏里的 C# 编辑器窗口。
|
||
|
||
`project/leanclr/live_reload.txt` : 热重载 marker。写 `Game` 就回到默认 `Game.dll`,写别的 assembly 名就切到那个 dll。
|