当前位置:
文章详情

Godot 4.7 移植到鸿蒙 HarmonyOS NEXT:从零到真机点亮

2026-08-10 22:07:06 0点赞 0收藏 0评论

前段时间做了一件事:把开源游戏引擎 Godot 4.7 原生移植到鸿蒙 HarmonyOS NEXT(OpenHarmony)平台,并且让引擎自带的编辑器能在鸿蒙真机上跑起来。过程踩了不少坑,尤其是那个让画面永远卡死在启动画面上的诡异问题。这里把技术路线和排查过程整理出来,希望能帮到同样在做鸿蒙原生移植的朋友。

项目源码已开源github.com/ambitiouscat/godot-harmonyos(含完整鸿蒙适配层实现,欢迎 Star)。

Godot 4.7 移植到鸿蒙 HarmonyOS NEXT:从零到真机点亮

为什么要做这件事

Godot 是当前最活跃的开源游戏引擎之一,官方支持 Windows / macOS / Linux / Android / iOS 等平台,但没有鸿蒙。鸿蒙的生态正在起来,游戏和工具类应用都需要引擎支持。

方案的出发点很明确:不是套壳(WebView 里跑 Web 版 Godot),而是真正的原生移植——把 Godot 的渲染、输入、音频全部接到鸿蒙的底层 API 上,让引擎跑在鸿蒙自己的 Vulkan 和 NDK 之上。

整体架构

整个方案分三层,从上层 UI 到引擎核心,各司其职:

{{< arch >}}

ArkTS 层(UI 线程)

  • EntryAbility → EditorViewport

  • XComponent(Vulkan 渲染画布)

NAPI 桥接层(libgodot_napi.so)

  • dlsym 动态解析 libgodot.so 符号

  • XComponent 生命周期回调

  • 导出函数: setup / setSurface / inputTouch / inputMouse / inputKey

引擎层(libgodot.so)

  • OS_OpenHarmony(继承 OS_Unix)

  • DisplayServerOpenHarmony

  • RenderingContextDriverVulkanOpenHarmony(VK_OHOS 扩展)

  • AudioDriverOpenHarmony(OHAudio) {{< /arch >}}

  • ArkTS 层:鸿蒙 UI 线程,负责承载 XComponent(一个底层物理渲染表面,绕开 ArkUI 的 2D 渲染树,直接对接 Vulkan)。

  • NAPI 桥接层:C++ 动态库 libgodot_napi.so,把 ArkTS 的调用转成引擎的 C 导出函数。为了不把引擎库编译成强耦合,桥接层用 dlsym 动态解析引擎符号。

  • 引擎层:libgodot.so,重写了 Godot 的 OS、DisplayServer、Vulkan 渲染驱动、音频驱动四层抽象。

值得一提的技术选择是净室移植(Clean-Room):完全不拷贝任何参考实现(某个商业产品的鸿蒙分支)的源码,只把它当"黑盒参考"提取底层 API 调用逻辑,然后从零重写鸿蒙适配层。这样既规避了版权风险,又能保证与 Godot upstream 的合并兼容性。

Phase 1:工程初始化 + SCons 交叉编译

Godot 用 SCons 构建,鸿蒙用 DevEco(hvigor)构建。第一步要打通交叉编译:

  • 配置 aarch64-linux-ohos-clang 工具链,屏蔽 SDL3 / Wayland

  • 产出首个纯净的 ARM64 ELF 引擎核心 libgodot.so(5.1MB)

  • 配置自动同步脚本,把编译产物同步进 HAP 工程,DevEco 打包(7.8s)

这个阶段踩的坑比较常规:embree 的线程亲和性 API(pthread_getaffinity_np)在鸿蒙 NDK 下需要补 __OPEN_HARMONY__ 守卫、GLES3 头文件适配、编译脚本的 mySubProcess 兼容。

Phase 2:Vulkan 渲染 + NAPI 桥接

Vulkan 是鸿蒙的原生图形 API(VK_OHOS_surface 扩展)。这个阶段做了四件事:

  1. Vulkan 渲染驱动:重写 Godot 的 RenderingContextDriverVulkan,用 PFN 动态加载 vkCreateSurfaceOHOS,把鸿蒙的 OHNativeWindow* 映射成 Vulkan 的 VkSurfaceKHR。

  2. NAPI 桥接:实现 godot_ohos_setup(引擎启动)、godot_ohos_set_surface(绑定渲染表面)、godot_ohos_change_surface(交换链重建)等核心接口。

  3. ArkTS 视口:EditorViewport.ets 里挂载 XComponent,绑定生命周期回调。这里有个坑:SDK 6.0.2 已经废弃了 OH_NativeXComponent_GetNativeWindow,要从 OnSurfaceCreated 回调里直接拿 void* window。

  4. 沙箱文件访问:重写 FileAccess / DirAccess(继承 Unix 基类),Bundle 资源走 OH_ResourceManager_OpenRawFile64,用户目录走 Unix API。

真机验证:OnSurfaceCreated → setSurfaceId 回调链路完整,Vulkan 交换链 + 清屏渲染成功。

Phase 3:多模态输入 + OHAudio 音频

让引擎"能用"还不行,得让用户能操作:

  • 输入事件流:ArkTS 三路事件捕获(触控 / 鼠标 / 键盘)→ NAPI → C++ 线程安全事件队列(std::queue + std::mutex)→ process_events() → Input::parse_input_event()。

  • 触控模拟鼠标:Godot 的 UI 控件(Control)只监听鼠标事件,不认原生触屏。所以在 ArkTS 层做了转换——单指触控时伪造 MOUSE_BUTTON_LEFT 和 MOUSE_MOVE 注入底层,这样手指就能点按项目管理器的按钮了。

  • OHAudio 音频驱动:双流(Renderer + Capturer)驱动,处理音频焦点中断、前后台切换联动。

  • 剪贴板:用 napi_threadsafe_function 实现跨线程回调。

真机上触控 / 鼠标 / 键盘端到端验证全部通过。

从零到能跑:实施流程

{{< step >}} 工程初始化 | 搭建鸿蒙壳工程 + SCons 交叉编译,产出 ARM64 的 libgodot.so Vulkan 渲染 | NAPI 桥接 + XComponent 挂载 + vkCreateSurfaceOHOS 绑定 输入与音频 | 多模态事件流 + OHAudio 双流驱动 + 剪贴板 {{< /step >}}

核心攻坚:Boot Splash 卡死的根因

这是整个项目最折磨人的问题,值得单独写:

现象:应用能编译、能安装、能启动,XComponent 挂载成功,Godot 经典的红色启动画面正常渲染——然后永远定格,进不了项目管理器。渲染循环日志显示在正常跑(Rendered frame: 1260),没有崩溃、没有 ANR,但画面就是不动。

排查过程(一步步排除):

  1. 日志路由:Godot 的 print_line 默认打不到鸿蒙的 hilog,导致看不到引擎层日志,误以为 C++ 没执行。修复:注册自定义 print / error 钩子,把引擎日志重定向到 OH_LOG。

  2. 启动参数:发现之前把沙箱路径传给了 setup,导致引擎带着 --editor 进入编辑器模式,但目录是空的、没有 project.godot,于是静默卡死。修复:启动项目管理器时把路径置空,让引擎走 --project-manager 分支。

  3. 输入事件:怀疑是低功耗模式下没有输入事件导致渲染休眠。于是做了触控模拟鼠标、主动注入 WINDOW_EVENT_FOCUS_IN / MOUSE_ENTER。卡死依旧,但也排除了输入因素。

真正根因(最终定位):一个由 0x0 尺寸引发的渲染沉睡死锁

  • Godot 在低功耗模式下,只有当渲染内容"有变化"(has_changed == true)时才会真正提交一帧画面。

  • XComponent 刚创建时,底层 Vulkan 拿到的物理尺寸是 0x0

  • 项目管理器排队请求第一帧重绘时,has_changed 被置为 true;但 screen_prepare_for_drawing 发现宽高是 0,判定不可用,跳过了这一帧的 swap 提交

  • 关键点:这一帧虽然没有真正渲染,但 RenderingServer::draw() 已经执行过了,has_changed 清零了。

  • 之后没有任何输入事件、没有任何人重新标记变化,于是渲染循环进入了永久"沉睡"。即使后来系统把真实尺寸(如 2560×1600)通过 OnSurfaceChanged 传过来,也没有任何机制唤醒它重新绘制

修复方案(三管齐下):

  1. 尺寸变更时强制唤醒:在 DisplayServerOpenHarmony::set_surface_size 尾部调用 Main::force_redraw(),收到真实尺寸就强制打破渲染循环的睡眠。

  2. 关闭低功耗模式:重写 is_in_low_processor_usage_mode() 强制返回 false,移植初期以最高兼容性的持续重绘方式运行。

  3. 尺寸安全过滤:拦截 width <= 0 || height <= 0 的非法尺寸;初始化时用安全默认值(如 1080×1920)兜底。

修复后,引擎顺利跨过启动画面,进入项目管理器。

{{< warn >}}排查"画面卡住但进程正常"这类问题,优先怀疑渲染循环的"变化标记"是否被消耗——在低功耗模式下,没有变化就不会重绘,一次被吞掉的帧重绘就可能让渲染循环永久沉睡。{{< /warn >}}

更多踩坑记录

线程模型:试过多种方案。把引擎跑在 std::thread 上会导致 Main::start() 不返回;用 VSync 回调驱动会在主线程 / VSync 线程上都 SIGSEGV(且崩溃地址完全相同,说明是引擎内部某处线程不安全的代码路径);主线程同步跑则会锁死 ArkUI 的 VSync 信号。最终采用独立后台线程跑引擎 + 2 秒轮询等待物理窗口就绪,既避免 UI 线程锁死,又解决 XComponent 挂载与引擎线程启动的时序竞争。

时序竞争:XComponent 的物理 Surface 挂载和独立引擎线程启动完全异步。如果 Vulkan 设备初始化时窗口还没就绪会崩溃,所以引擎线程启动后先轮询等待 g_native_window 就绪(最多 2 秒)。

帧率控制:鸿蒙真机上若 Vulkan 的 vkQueuePresentKHR 没有垂直同步,渲染线程会以极高帧率空转,GPU 发热严重甚至饿死系统合成器。硬性 delay_usec(16000) 锁 60 FPS 上限,并用 --rendering-method mobile 锁定移动级 Vulkan 管线。

UI 约束:鸿蒙的 XComponent 环境里,Window 子类(PopupMenu、AcceptDialog 等)做弹窗不可靠,因为 DisplayServerOpenHarmony 没实现 create_sub_window()。替代方案是用普通 Control 子类(PanelContainer + ItemList 等)通过 set_visible() + set_position() 模拟 overlay 效果。

现在的进展与未来

{{< timeline >}} 2026-05-24 | 项目启动 | 净室移植方案确定 2026-05-27 | Boot Splash 卡死 | 定位 0x0 渲染沉睡根因并修复 2026-06-10 | 文件系统打通 | FileAccess/DirAccess 全链路验证 2026-06-13 | AI 编辑器启动 | 集成 Rust Claude Code 2026-07-26 | 编译链路全绿 | Rust → C++ → HAP → 真机部署 {{< /timeline >}}

以下为当前进展:

截至目前的成果:

  • ✅ Phase 1-3 全部完成:交叉编译、Vulkan 渲染、NAPI 桥接、多模态输入、OHAudio 音频、沙箱文件系统,真机全链路验证通过

  • ✅ 引擎稳定运行,进入项目管理器,可以创建 / 打开项目

  • ✅ 编辑器 1022 个 SVG 图标和静态资源全部打包进 HAP

  • 🔄 正在做一件更酷的事:把 Claude Code(Rust 版)作为原生插件嵌入引擎,做一个自带 AI 编程助手的 Godot 编辑器——通过 Rust binding 与引擎内核通信,可以在属性面板里直接和 AI 对话、让它操作场景节点

从一个不存在的平台,到引擎在鸿蒙真机上跑起来、点亮编辑器,整个过程验证了一个结论:Godot 的抽象层设计得足够好,一个陌生的平台可以通过重写 OS / DisplayServer / 渲染驱动 / 音频驱动四层干净地接入。如果你也在做类似的平台移植,希望这些踩坑记录能帮你少走几个弯路。

作者声明本文无利益相关,欢迎值友理性交流,和谐讨论~

展开 收起
0评论

当前文章无评论,是时候发表评论了
提示信息

取消
确认
评论举报

有野心的喵

Ta还没有介绍自己

关注 打赏
最新文章 热门文章
0
扫一下,分享更方便,购买更轻松