【OpenHarmony/HarmonyOS】自定义对战大厅设计:1v1、3v3、队伍槽位与邀请流程
【OpenHarmony/HarmonyOS】自定义对战大厅设计:1v1、3v3、队伍槽位与邀请流程
多人大厅的难点不在于把六个头像摆成两列,而在于“谁有权修改什么”。房主选择模式和地图,附近设备收到邀请,访客接受后进入房间,槽位可能是等待、AI 或关闭,最后所有设备必须基于同一份配置同时进入游戏。本篇基于 OpenHarmony/HarmonyOS 项目中的自定义房间原型,拆解 ArkUI 大厅的数据结构、P2P 信令和路由参数,并明确当前模拟设备、UDP 原型与 3v3 未闭环的边界。👥
一、页面有两个主要阶段
CustomTeamPage 没有拆成两个路由页,而是使用 showLobby 在设置视图和大厅视图之间切换:
@State selectedMode: '1v1' | '3v3' = '1v1';
@State mapSize:
'small' | 'medium' | 'large' = 'medium';
@State mapSizeValue: number = 1;
@State showLobby: boolean = false;
build(): void {
Stack() {
GalaxyBackground();
if (!this.showLobby) {
this.SetupView();
} else {
this.LobbyView();
}
}
}
| 阶段 | 用户任务 | 核心状态 |
|---|---|---|
| Setup | 选择 1v1/3v3、地图小中大、创建房间 | selectedMode、mapSize |
| Lobby | 发现设备、邀请、安排槽位、开始游戏 | 玩家数组、slotConfig、P2P 回调 |
这种页面内切换能保留设置数据,也避免房间创建前增加一层路由。但 showLobby 只是布尔值,无法表达 Joining、Waiting、Starting、Failed 等网络阶段;网络流程复杂后需要显式状态。
二、队伍与设备的当前模型
页面使用轻量 PlayerModel:
class PlayerModel {
name: string;
isHost: boolean;
deviceId: string = '';
constructor(name: string, isHost: boolean = false) {
this.name = name;
this.isHost = isHost;
}
}
三个列表分别表达本队、对方和附近设备:
@State teamAPlayers: PlayerModel[] = [
new PlayerModel('我 (本机)', true)
];
@State teamBPlayers: PlayerModel[] = [];
@State nearbyDevices: PlayerModel[] = [];
这个模型适合 UI 原型,但不足以支撑真实多人状态。name 不是稳定身份,deviceId 只对发现列表有意义,加入队伍后的 newPlayer 没有保存 peer IP 或 ID,断线时难以定位应移除哪个槽位。
生产模型至少还需要:
type PlayerConnection =
'invited' | 'joining' | 'connected' | 'disconnected';
interface LobbyPlayer {
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';
else if (value === 1) this.mapSize = 'medium';
else this.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 连接、房主开始游戏。这形成了一套事件驱动 UI:Manager 不依赖 ArkUI,页面将网络事件转换为列表、Dialog、Toast 与路由。
五、设备发现来自两条通道
P2PConnectionManager 同时尝试分布式设备管理和 UDP 广播:
flowchart TD
A[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]
分布式设备在线事件被转换为统一 DiscoveredDevice;UDP 收到 DISCOVER 后生成 lan_<ip> 作为设备 ID。页面按 deviceId 去重:
manager.onDeviceFound = (device: DiscoveredDevice) => {
const exists = this.nearbyDevices.find(
item => item.deviceId === device.deviceId
);
if (exists) return;
const player = new PlayerModel(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,并在一秒后伪造 ACCEPT:
if (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。接受时发送 ACCEPT:
acceptInvite(from: P2PInviteInfo): void {
manager.acceptInvite(from.ip, '我 (本机)');
this.showLobby = true;
this.teamAPlayers = [
new PlayerModel(from.name, true)
];
this.teamBPlayers = [
new PlayerModel('我 (本机)', false)
];
}
房主收到 ACCEPT 后把 peer 写入 peers Map,并触发 onPeerConnected。页面当前只在 Team B 为空时加入一个玩家:
manager.onPeerConnected = (peer: P2PPeerInfo) => {
if (this.teamBPlayers.length === 0) {
this.teamBPlayers.push(
new PlayerModel(peer.name, false)
);
}
// 当前源码明确保留 3v3 待处理注释
};
因此握手链路主要覆盖 1v1。第二、第三名玩家、断线重连、队伍切换和房主迁移都没有闭环。
八、槽位状态:等待、AI、关闭
空槽位点击时在三个值间循环:
@State slotConfig: Map<string, string> = new Map();
toggleSlotState(
index: number,
_count: number,
isTeamA: boolean
): void {
const team = isTeamA ? 'A' : 'B';
const key = `${team}_${index}`;
let current = this.slotConfig.get(key) || 'open';
if (current === 'open') current = 'ai';
else if (current === 'ai') current = 'closed';
else current = 'open';
this.slotConfig.set(key, current);
}
| 值 | UI 含义 | GameEngine 当前消费 |
|---|---|---|
open | 等待真人加入 | 没有完整真人槽位绑定 |
ai | 电脑补位 | Engine 会按 A_/B_ key 生成 AI |
closed | 关闭该槽位 | 不生成 AI |
页面还保留 teamASlotStates、teamBSlotStates 两个数组和 getSlotState(),但实际显示主要读取 slotConfig Map。这是原型迭代留下的重复模型,应选一个权威来源。
Map 原地 set() 后使用 this.mapSizeValue = this.mapSizeValue 尝试强制刷新,不够可靠。更合适的是克隆 Map 后重新赋值。
九、房主与访客权限目前没有真正隔离 ⚠️
客端接受邀请后进入同一个 LobbyView。源码注释已经提出“Guest shouldn't change settings”,但没有实现房主权限判断。开始按钮、槽位按钮和返回设置仍可能对访客可见。
多人大厅至少要区分:
| 操作 | 房主 | 访客 |
|---|---|---|
| 选择模式/地图 | 允许 | 只读 |
| 修改 AI/关闭槽位 | 允许 | 不允许 |
| 邀请附近设备 | 允许 | 通常不允许 |
| 切换自己队伍/准备 | 可按规则 | 允许 |
| 开始游戏 | 允许且需校验 | 不允许 |
| 离开房间 | 解散或迁移房主 | 退出自身 |
权限不能只隐藏按钮,网络信令接收端也必须验证 sender 是否为房主,否则访客可以自行构造 START_GAME。
十、START_GAME 配置如何广播
房主把 Map 转为普通对象并字符串化:
const configObject: Record<string, 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 会发送并路由 gameMode='multiplayer'、地图和槽位配置,但 Index 的 onPageShow() 中消费多人参数的代码被注释,标注为离线版本移除。
GameEngine 确实能解析多人配置并为 A_*、B_* 的 ai 槽生成不同队伍 AI,但页面路由到 startGame('multiplayer', ..., config) 的接线当前没有执行。
因此准确结论是:
- 大厅 UI、设备发现接口、邀请信令和开局配置格式已经存在;
- 模拟设备可以演示 1v1 加入效果;
- 3v3 真人槽位尚未处理;
- 路由接收端当前不会自动启动完整多人局;
- UDP 状态同步属于原型,不能宣称正式联机已完成。
十三、页面离开时的清理还不完整
aboutToDisappear() 调用 stopDiscovery(),能清除广播 interval 和设备发现监听。但页面赋给单例 Manager 的以下回调仍保留:
onDeviceFound
onReceiveInvite
onGameStart
onPeerConnected
如果页面实例离开后单例收到 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 丢包 | 重试并等待 ACK |
| START_GAME 重复 | 只导航一次 |
| 非房主伪造 START_GAME | 消息被拒绝 |
| 页面离开后收到消息 | 不再修改旧组件 |
| 非法 slotConfig JSON | 保留大厅并提示配置错误 |
| Index 参数消费关闭 | 测试明确标记端到端未完成 |
十六、总结 ✨
项目已经搭建了一条有价值的自定义大厅原型:ArkUI 中选择模式与地图,进入大厅后发现设备,UDP 发送邀请和接受信令,槽位可切换等待、AI、关闭,房主广播统一开局配置,客端根据回调导航。
但“原型可演示”和“多人功能完成”必须严格区分。当前附近列表主动注入模拟设备,Peer 加入只完整处理 1v1,3v3 真人槽位留有待处理注释,访客权限没有隔离,UDP 没有 ACK、身份验证和去重,Index 的开局参数消费也处于注释状态。下一步应先建立权威 LobbySnapshot、房主权限、稳定玩家身份和可靠信令,再连接战斗会话。只有配置、网络、页面和引擎四端都消费同一份状态,大厅才真正成为多人对战的入口。🚀
推荐标签: OpenHarmony HarmonyOS ArkTS ArkUI 局域网 UDP 多人游戏 状态同步 P2P

更多推荐


所有评论(0)