鸿蒙近场联机:ENet + Harmony 双栈架构
godot-game-one 的联机模式目标是:鸿蒙设备间通过近场发现配对,进入实时协作射击。但 Godot 4 原生不支持鸿蒙,ENet 也无法做设备发现。解决方案是:大厅用 Harmony,战斗用 ENet。
graph TD
A["玩家打开游戏"] --> B{"鸿蒙插件可用?"}
B -->|"是"| C["Harmony 近场发现"]
B -->|"否"| D["UDP 广播发现"]
C --> E["大厅配对"]
D --> E
E --> F["房主创建 ENet Server"]
F --> G["客户端连接 ENet"]
G --> H["战斗开始"]
H --> I["ENet 实时同步"]
H --> J["Harmony 断开"]
双栈架构:谁负责什么
设计原则很简单:Harmony 只管发现,ENet 只管同步。
graph LR
subgraph "发现阶段"
H["Harmony NSD"] -->|"附近设备列表"| L["Lobby UI"]
U["UDP 广播"] -->|"桌面调试回退"| L
end
subgraph "战斗阶段"
N["NetworkManager"] -->|"ENet Server"| S["Host"]
N -->|"ENet Client"| C["Client"]
S -->|"PackedFloat32Array"| C
C -->|"输入指令"| S
end
核心代码层:
1 | # network_manager.gd |
HarmonyBridge 是 Autoload 单例,封装了鸿蒙原生插件的 Godot 调用。插件不可用时自动回退到 UDP 广播:
1 | # harmony_bridge.gd |
战斗同步:PackedFloat32Array 批量打包
进入战斗后,Harmony 完全退出,所有数据走 ENet。同步策略按频率分层:
| 数据 | 频率 | 可靠性 | 格式 |
|---|---|---|---|
| 实体位置 | ~45Hz(0.022s 间隔) | unreliable | PackedFloat32Array [eid, x, y] |
| 实体血量 | 10Hz | unreliable | PackedFloat32Array [eid, hp, max_hp] |
| 护盾值 | 随位置同步 | reliable | PackedFloat32Array [eid, shield, max] |
| 朝向 | 随位置同步 | unreliable | PackedFloat32Array [eid, rotation] |
| 子弹生成 | 事件驱动 | reliable | Array [x, y, angle, damage, …] |
批量打包的核心实现:
1 | func _batch_sync_entity_positions() -> void: |
用 PackedFloat32Array 而不是 Dictionary 或 Array,是因为 Float32 数组可以被 ENet 高效序列化,20Hz 同步 50 个实体的位置数据,单包只有 ~600 字节。
客户端插值:平滑网络抖动
Host 每约 22ms 发一次位置(ENTITY_SYNC_INTERVAL = 0.022),Client 不直接赋值,而是插值到目标位置:
1 | # main.gd — Client 收到位置包 |
为什么不用纯 Harmony?
两个原因:
- ENet 是 Godot 原生,
@rpc注解 +multiplayerAPI 已经把网络层抽象得非常好,不需要重造轮子 - Harmony 的 NSD 只做服务发现,不提供游戏级的实时同步能力,强行用它传游戏数据会非常低效
最好的架构不是最炫的,是最少改动的。Harmony 只替换了一个入口,其他全复用 Godot 原生能力。