【OpenHarmony/HarmonyOS】自定义对战大厅设计:1v1、3v3、队伍槽位与邀请流程

📅 2026/7/24 16:19:42
【OpenHarmony/HarmonyOS】自定义对战大厅设计:1v1、3v3、队伍槽位与邀请流程
【OpenHarmony/HarmonyOS】自定义对战大厅设计1v1、3v3、队伍槽位与邀请流程多人大厅的难点不在于把六个头像摆成两列而在于“谁有权修改什么”。房主选择模式和地图附近设备收到邀请访客接受后进入房间槽位可能是等待、AI 或关闭最后所有设备必须基于同一份配置同时进入游戏。本篇基于 OpenHarmony/HarmonyOS 项目中的自定义房间原型拆解 ArkUI 大厅的数据结构、P2P 信令和路由参数并明确当前模拟设备、UDP 原型与 3v3 未闭环的边界。一、页面有两个主要阶段CustomTeamPage没有拆成两个路由页而是使用showLobby在设置视图和大厅视图之间切换StateselectedMode:1v1|3v31v1;StatemapSize:small|medium|largemedium;StatemapSizeValue: number 1;StateshowLobby: boolean false;build(): void { Stack() { GalaxyBackground();if(!this.showLobby) {this.SetupView(); }else{this.LobbyView(); } } }阶段用户任务核心状态Setup选择 1v1/3v3、地图小中大、创建房间selectedMode、mapSizeLobby发现设备、邀请、安排槽位、开始游戏玩家数组、slotConfig、P2P 回调这种页面内切换能保留设置数据也避免房间创建前增加一层路由。但showLobby只是布尔值无法表达 Joining、Waiting、Starting、Failed 等网络阶段网络流程复杂后需要显式状态。二、队伍与设备的当前模型页面使用轻量PlayerModelclassPlayerModel{name:string;isHost:boolean;deviceId:string;constructor(name:string, isHost:booleanfalse) {this.name name;this.isHost isHost; } }三个列表分别表达本队、对方和附近设备StateteamAPlayers: PlayerModel[] [ new PlayerModel(我 (本机), true) ];StateteamBPlayers: PlayerModel[] [];StatenearbyDevices: PlayerModel[] [];这个模型适合 UI 原型但不足以支撑真实多人状态。name不是稳定身份deviceId只对发现列表有意义加入队伍后的newPlayer没有保存 peer IP 或 ID断线时难以定位应移除哪个槽位。生产模型至少还需要typePlayerConnectioninvited|joining|connected|disconnected;interfaceLobbyPlayer {playerId:string;displayName:string; deviceId?:string; peerAddress?:string;team:A|B;slotIndex:number;role:host|guest|ai;connection:PlayerConnection;ready:boolean; }这是演进方案。当前代码尚未建立完整玩家身份与准备状态。三、模式与地图选择如何驱动 UI模式按钮接收1v1 | 3v3选中时更新颜色并修改状态。地图使用 0、1、2 的分段控件同时维护字符串值。.onClick(() { animateTo({ duration:200}, () {this.mapSizeValue value;if(value 0)this.mapSize small;elseif(value 1)this.mapSize medium;elsethis.mapSize large; }); })1v1 时每队显示 1 个槽位3v3 时显示 3 个this.TeamSlots(this.selectedMode 1v1?1:3,this.teamAPlayers,#00E5FF);模式状态确实改变了大厅布局但它还没有限制加入人数、验证队伍完整性或改变网络同步协议。所以“UI 可选 3v3”和“支持六人真实对战”是两回事。四、创建房间后才开始发现设备 点击创建房间会清空附近设备并开始扫描createRoom(): void { animateTo({duration: 300 }, () { this.showLobby true; this.nearbyDevices [];P2PConnectionManager.getInstance().startDiscovery(); }); }把扫描放在进入大厅之后能避免用户还在选择配置时持续消耗无线与定时器资源。页面离开时调用stopDiscovery()清除广播 interval 并停止分布式发现。页面显示时先初始化单例 Manager再绑定四类回调发现设备、收到邀请、Peer 连接、房主开始游戏。这形成了一套事件驱动 UIManager 不依赖 ArkUI页面将网络事件转换为列表、Dialog、Toast 与路由。五、设备发现来自两条通道P2PConnectionManager同时尝试分布式设备管理和 UDP 广播flowchartTDA[startDiscovery]--B[Distributed Device Discovery]A-- C[UDP 每 2 秒广播 DISCOVER]A-- D[2 秒后注入模拟设备]B-- E[onDeviceFound]C -- F[收到对端 DISCOVER]F -- E D -- E E -- G[页面去重并加入 nearbyDevices]分布式设备在线事件被转换为统一DiscoveredDeviceUDP 收到DISCOVER后生成lan_ip作为设备 ID。页面按deviceId去重manager.onDeviceFound(device: DiscoveredDevice) {constexists this.nearbyDevices.find(itemitem.deviceId device.deviceId);if(exists)return;constplayer newPlayerModel(device.deviceName); player.deviceId device.deviceId;this.nearbyDevices.push(player); };同一物理设备可能同时通过分布式发现和 UDP 出现两个通道的 ID 不同所以按 deviceId 仍可能显示两次。需要统一身份关联或给通道结果标注来源并合并。六、附近列表中包含明确的模拟设备startDiscovery()两秒后总会调用setTimeout((){ this.mockDeviceFound(dev_001,Mate 60 Pro (模拟)); },2000);邀请dev_*时也不会真正发送网络包而是使用模拟 IP并在一秒后伪造 ACCEPTif(deviceId.startsWith(dev_)) { const targetIp 192.168.1.100;setTimeout((){ this.handleSignal({ type:ACCEPT, name:Mock Player, ip: targetIp }, targetIp); },1000);return; }这对单机演示 UI 很有价值但文章、截图和产品说明必须标注“模拟”。否则读者会以为两台真机发现和握手已经完成。七、邀请握手是怎样完成的真实 LAN 设备 ID 形如lan_ip。房主点击连接后发送{type:INVITE,name:我 (本机)}客端收到后弹出 AlertDialog。接受时发送ACCEPTacceptInvite(from: P2PInviteInfo):void{ manager.acceptInvite(from.ip,我 (本机));this.showLobby true;this.teamAPlayers [newPlayerModel(from.name,true) ];this.teamBPlayers [newPlayerModel(我 (本机),false) ]; }房主收到 ACCEPT 后把 peer 写入peersMap并触发onPeerConnected。页面当前只在 Team B 为空时加入一个玩家manager.onPeerConnected (peer: P2PPeerInfo) {if(this.teamBPlayers.length0) { this.teamBPlayers.push(newPlayerModel(peer.name,false)); }// 当前源码明确保留 3v3 待处理注释};因此握手链路主要覆盖 1v1。第二、第三名玩家、断线重连、队伍切换和房主迁移都没有闭环。八、槽位状态等待、AI、关闭空槽位点击时在三个值间循环StateslotConfig:Mapstring,string newMap();toggleSlotState(index:number,_count:number,isTeamA:boolean):void{constteam isTeamA ?A:B;constkey ${team}_${index};letcurrent this.slotConfig.get(key) ||open;if(current open) current ai;elseif(current ai) current closed;elsecurrent open;this.slotConfig.set(key, current); }值UI 含义GameEngine 当前消费open等待真人加入没有完整真人槽位绑定ai电脑补位Engine 会按 A_/B_ key 生成 AIclosed关闭该槽位不生成 AI页面还保留teamASlotStates、teamBSlotStates两个数组和getSlotState()但实际显示主要读取slotConfigMap。这是原型迭代留下的重复模型应选一个权威来源。Map 原地set()后使用this.mapSizeValue this.mapSizeValue尝试强制刷新不够可靠。更合适的是克隆 Map 后重新赋值。九、房主与访客权限目前没有真正隔离 ⚠️客端接受邀请后进入同一个 LobbyView。源码注释已经提出“Guest shouldnt change settings”但没有实现房主权限判断。开始按钮、槽位按钮和返回设置仍可能对访客可见。多人大厅至少要区分操作房主访客选择模式/地图允许只读修改 AI/关闭槽位允许不允许邀请附近设备允许通常不允许切换自己队伍/准备可按规则允许开始游戏允许且需校验不允许离开房间解散或迁移房主退出自身权限不能只隐藏按钮网络信令接收端也必须验证 sender 是否为房主否则访客可以自行构造 START_GAME。十、START_GAME 配置如何广播房主把 Map 转为普通对象并字符串化constconfigObject:Recordstring,string {};this.slotConfig.forEach((value, key) { configObject[key] value; }); manager.broadcastGameStart({mapSize:this.mapSize,teamMode:this.selectedMode,slotConfig:JSON.stringify(configObject) });Manager 为每个已连接 peer 发送 UDP JSON{type:START_GAME,config: {mapSize:medium,teamMode:3v3,slotConfig:{\A_1\:\ai\}} }客端收到后回调页面并pushUrl到 Index。房主也执行同样路由。这个协议表达了“开始意图”但没有房间 ID、配置版本、随机种子、开始时间和确认 ACK。十一、UDP 信令的可靠性与安全边界当前原型使用固定 8888 端口和 JSON 明文。UDP 不保证送达、顺序或唯一性START_GAME 丢包时可能房主已进入、客端仍留在大厅。重复包则可能重复导航。生产设计至少需要messageId去重roomId隔离同网段不同房间senderId验证消息来源sequence判断新旧配置ACK retry关键命令确认protocolVersion兼容升级身份验证或会话签名阻止任意局域网设备伪造信令长度与字段白名单避免恶意大包和非法 JSON。当前handleSignal(START_GAME)会直接把发送者加入 peers 并触发回调没有确认其此前是否通过邀请成为房主。它适合可信局域网原型不适合作为公网或正式竞技协议。十二、开局参数当前没有在 Index 形成闭环CustomTeamPage 会发送并路由gameModemultiplayer、地图和槽位配置但 Index 的onPageShow()中消费多人参数的代码被注释标注为离线版本移除。GameEngine 确实能解析多人配置并为A_*、B_*的ai槽生成不同队伍 AI但页面路由到startGame(multiplayer, ..., config)的接线当前没有执行。因此准确结论是大厅 UI、设备发现接口、邀请信令和开局配置格式已经存在模拟设备可以演示 1v1 加入效果3v3 真人槽位尚未处理路由接收端当前不会自动启动完整多人局UDP 状态同步属于原型不能宣称正式联机已完成。十三、页面离开时的清理还不完整aboutToDisappear()调用stopDiscovery()能清除广播 interval 和设备发现监听。但页面赋给单例 Manager 的以下回调仍保留onDeviceFoundonReceiveInviteonGameStartonPeerConnected如果页面实例离开后单例收到 UDP 消息回调仍可能修改旧页面状态或执行路由。应在离开时仅当回调仍属于当前页面时置空或由 Manager 返回 unsubscribe 函数。UDP socket 本身也没有在页面离开时关闭因为它属于全局单例。是否保持监听取决于产品如果后台也要接收邀请应由 Ability 生命周期管理如果只在大厅有效应提供disposeSocket()。十四、推荐的大厅状态机stateDiagram-v2 [*] -- Configuring Configuring--Discovering: 房主创建房间 Configuring--Joining: 访客接受邀请 Discovering--Lobby: 扫描启动 Joining--Lobby: 握手确认 Lobby--Starting: 房主点击开始 Starting--Lobby: 有设备未确认/发送失败 Starting--InGame: 所有设备确认同一配置 Lobby--Closed: 房主解散 Lobby--Left: 访客离开主状态之外再保存LobbySnapshot房间 ID、版本、房主、玩家、槽位、地图和模式。每次变更由房主增加 revision 并广播完整快照客端只接受更新版本能比零散地修改数组更容易恢复一致。十五、测试矩阵 场景预期重复发现同一 deviceId附近列表只出现一次分布式与 LAN 发现同设备通过稳定身份合并模拟设备出现UI 明确标“模拟”1v1 接受邀请房主和访客占据正确队伍第二名 peer 加入 1v1拒绝或进入观察不静默丢失3v3 三名访客加入每人有稳定 slotIndex访客点击开始UI 和协议端都拒绝START_GAME 丢包重试并等待 ACKSTART_GAME 重复只导航一次非房主伪造 START_GAME消息被拒绝页面离开后收到消息不再修改旧组件非法 slotConfig JSON保留大厅并提示配置错误Index 参数消费关闭测试明确标记端到端未完成十六、总结 ✨项目已经搭建了一条有价值的自定义大厅原型ArkUI 中选择模式与地图进入大厅后发现设备UDP 发送邀请和接受信令槽位可切换等待、AI、关闭房主广播统一开局配置客端根据回调导航。但“原型可演示”和“多人功能完成”必须严格区分。当前附近列表主动注入模拟设备Peer 加入只完整处理 1v13v3 真人槽位留有待处理注释访客权限没有隔离UDP 没有 ACK、身份验证和去重Index 的开局参数消费也处于注释状态。下一步应先建立权威 LobbySnapshot、房主权限、稳定玩家身份和可靠信令再连接战斗会话。只有配置、网络、页面和引擎四端都消费同一份状态大厅才真正成为多人对战的入口。推荐标签OpenHarmonyHarmonyOSArkTSArkUI局域网UDP多人游戏状态同步P2P