公司动态

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

📅 2026/8/11 3:04:19
Godot 4.7 移植到鸿蒙 HarmonyOS NEXT:从零到真机点亮
前段时间做了一件事把开源游戏引擎Godot 4.7 原生移植到鸿蒙 HarmonyOS NEXTOpenHarmony平台并且让引擎自带的编辑器能在鸿蒙真机上跑起来。过程踩了不少坑尤其是那个让画面永远卡死在启动画面上的诡异问题。这里把技术路线和排查过程整理出来希望能帮到同样在做鸿蒙原生移植的朋友。项目源码已开源github.com/ambitiouscat/godot-harmonyos含完整鸿蒙适配层实现欢迎 Star。为什么要做这件事Godot 是当前最活跃的开源游戏引擎之一官方支持 Windows / macOS / Linux / Android / iOS 等平台但没有鸿蒙。鸿蒙的生态正在起来游戏和工具类应用都需要引擎支持。方案的出发点很明确不是套壳WebView 里跑 Web 版 Godot而是真正的原生移植——把 Godot 的渲染、输入、音频全部接到鸿蒙的底层 API 上让引擎跑在鸿蒙自己的 Vulkan 和 NDK 之上。整体架构整个方案分三层从上层 UI 到引擎核心各司其职ArkTS 层UI 线程EntryAbility → EditorViewportXComponentVulkan 渲染画布NAPI 桥接层libgodot_napi.sodlsym 动态解析 libgodot.so 符号XComponent 生命周期回调导出函数: setup / setSurface / inputTouch / inputMouse / inputKey引擎层libgodot.soOS_OpenHarmony继承 OS_UnixDisplayServerOpenHarmonyRenderingContextDriverVulkanOpenHarmonyVK_OHOS 扩展AudioDriverOpenHarmonyOHAudioArkTS 层鸿蒙 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 构建鸿蒙用 DevEcohvigor构建。第一步要打通交叉编译配置aarch64-linux-ohos-clang工具链屏蔽 SDL3 / Wayland产出首个纯净的 ARM64 ELF 引擎核心libgodot.so5.1MB配置自动同步脚本把编译产物同步进 HAP 工程DevEco 打包7.8s这个阶段踩的坑比较常规embree 的线程亲和性 APIpthread_getaffinity_np在鸿蒙 NDK 下需要补__OPEN_HARMONY__守卫、GLES3 头文件适配、编译脚本的mySubProcess兼容。Phase 2Vulkan 渲染 NAPI 桥接Vulkan 是鸿蒙的原生图形 APIVK_OHOS_surface扩展。这个阶段做了四件事Vulkan 渲染驱动重写 Godot 的RenderingContextDriverVulkan用PFN动态加载vkCreateSurfaceOHOS把鸿蒙的OHNativeWindow*映射成 Vulkan 的VkSurfaceKHR。NAPI 桥接实现godot_ohos_setup引擎启动、godot_ohos_set_surface绑定渲染表面、godot_ohos_change_surface交换链重建等核心接口。ArkTS 视口EditorViewport.ets里挂载 XComponent绑定生命周期回调。这里有个坑SDK 6.0.2 已经废弃了OH_NativeXComponent_GetNativeWindow要从OnSurfaceCreated回调里直接拿void* window。沙箱文件访问重写FileAccess/DirAccess继承 Unix 基类Bundle 资源走OH_ResourceManager_OpenRawFile64用户目录走 Unix API。真机验证OnSurfaceCreated→setSurfaceId回调链路完整Vulkan 交换链 清屏渲染成功。Phase 3多模态输入 OHAudio 音频让引擎能用还不行得让用户能操作输入事件流ArkTS 三路事件捕获触控 / 鼠标 / 键盘→ NAPI → C 线程安全事件队列std::queuestd::mutex→process_events()→Input::parse_input_event()。触控模拟鼠标Godot 的 UI 控件Control只监听鼠标事件不认原生触屏。所以在 ArkTS 层做了转换——单指触控时伪造MOUSE_BUTTON_LEFT和MOUSE_MOVE注入底层这样手指就能点按项目管理器的按钮了。OHAudio 音频驱动双流Renderer Capturer驱动处理音频焦点中断、前后台切换联动。剪贴板用napi_threadsafe_function实现跨线程回调。真机上触控 / 鼠标 / 键盘端到端验证全部通过。核心攻坚Boot Splash 卡死的根因这是整个项目最折磨人的问题值得单独写现象应用能编译、能安装、能启动XComponent 挂载成功Godot 经典的红色启动画面正常渲染——然后永远定格进不了项目管理器。渲染循环日志显示在正常跑Rendered frame: 1260没有崩溃、没有 ANR但画面就是不动。排查过程一步步排除日志路由Godot 的print_line默认打不到鸿蒙的hilog导致看不到引擎层日志误以为 C 没执行。修复注册自定义print/error钩子把引擎日志重定向到OH_LOG。启动参数发现之前把沙箱路径传给了setup导致引擎带着--editor进入编辑器模式但目录是空的、没有project.godot于是静默卡死。修复启动项目管理器时把路径置空让引擎走--project-manager分支。输入事件怀疑是低功耗模式下没有输入事件导致渲染休眠。于是做了触控模拟鼠标、主动注入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传过来也没有任何机制唤醒它重新绘制。修复方案三管齐下尺寸变更时强制唤醒在DisplayServerOpenHarmony::set_surface_size尾部调用Main::force_redraw()收到真实尺寸就强制打破渲染循环的睡眠。关闭低功耗模式重写is_in_low_processor_usage_mode()强制返回false移植初期以最高兼容性的持续重绘方式运行。尺寸安全过滤拦截width 0 || height 0的非法尺寸初始化时用安全默认值如 1080×1920兜底。修复后引擎顺利跨过启动画面进入项目管理器。排查画面卡住但进程正常这类问题优先怀疑渲染循环的变化标记是否被消耗——在低功耗模式下没有变化就不会重绘一次被吞掉的帧重绘就可能让渲染循环永久沉睡。更多踩坑记录线程模型试过多种方案。把引擎跑在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子类PanelContainerItemList等通过set_visible()set_position()模拟 overlay 效果。现在的进展与未来截至目前的成果✅ Phase 1-3 全部完成交叉编译、Vulkan 渲染、NAPI 桥接、多模态输入、OHAudio 音频、沙箱文件系统真机全链路验证通过✅ 引擎稳定运行进入项目管理器可以创建 / 打开项目✅ 编辑器 1022 个 SVG 图标和静态资源全部打包进 HAP 正在做一件更酷的事把Claude CodeRust 版作为原生插件嵌入引擎做一个自带 AI 编程助手的 Godot 编辑器——通过 Rust binding 与引擎内核通信可以在属性面板里直接和 AI 对话、让它操作场景节点从一个不存在的平台到引擎在鸿蒙真机上跑起来、点亮编辑器整个过程验证了一个结论Godot 的抽象层设计得足够好一个陌生的平台可以通过重写OS/DisplayServer/ 渲染驱动 / 音频驱动四层干净地接入。如果你也在做类似的平台移植希望这些踩坑记录能帮你少走几个弯路。