客户端与服务器通信
我们已经分别学习了如何在服务器上创建实体和在客户端上创建 UI。但要制作出真正有趣的游戏,就必须让它们能够对话。这就是客户端与服务器通信(或称“远程通信”)的用武之地。
核心概念:remoteChannel
客户端和服务器之间的沟通桥梁是 remoteChannel
对象。它在客户端和服务器端都可以访问,但使用的方法略有不同。通信的核心思想是:将你要发送的“事件名称”和“数据”打包成一个对象,然后发送出去。
- 客户端 -> 服务器: 使用
remoteChannel.sendServerEvent(data)
- 服务器 -> 客户端: 使用
remoteChannel.sendClientEvent(player, data)
- 客户端接收: 使用
remoteChannel.onClientEvent(handler)
- 服务器接收: 使用
remoteChannel.onServerEvent(handler)
实践:一个“向服务器领奖”的按钮
我们的目标是创建一个完整的双向通信流程:
- 客户端: 创建一个 UI 按钮,上面写着“领取每日奖励”。
- 客户端: 玩家点击按钮后,向服务器发送一个包含事件名
eventName: 'request_daily_reward'
的数据包。 - 服务器: 监听到事件,检查
eventName
,执行发奖逻辑,然后向该玩家发回一个包含eventName: 'reward_granted'
的确认消息。 - 客户端: 监听到服务器的确认消息,检查
eventName
,更新 UI,告诉玩家“领取成功!”
客户端代码 (clientIndex.js
)
javascript
// clientIndex.js
// 1. 创建一个按钮
const rewardButton = UiBox.create();
rewardButton.name = "RewardButton";
rewardButton.backgroundColor.copy(Vec3.create({ r: 40, g: 167, b: 69 }));
rewardButton.anchor.copy(Vec2.create({ x: 0.5, y: 1 }));
rewardButton.position.offset.copy(Vec2.create({ x: 0, y: -20 }));
rewardButton.size.offset.copy(Vec2.create({ x: 200, y: 50 }));
const buttonText = UiText.create();
buttonText.textContent = "领取每日奖励";
buttonText.textFontSize = 20;
buttonText.textColor.copy(Vec3.create({ r: 255, g: 255, b: 255 }));
buttonText.parent = rewardButton;
rewardButton.parent = ui;
// 2. 让按钮可以被点击
rewardButton.pointerEventBehavior = 2; // 2 = CLICK
rewardButton.events.on("pointerdown", () => {
console.log("客户端:向服务器发送领奖请求...");
buttonText.textContent = "正在领取...";
// 3. 发送消息到服务器
remoteChannel.sendServerEvent({
eventName: "request_daily_reward",
from: "client",
});
});
// 4. 监听服务器的响应
remoteChannel.onClientEvent((event) => {
const data = event.args;
if (data.eventName === "reward_granted") {
console.log(`客户端:收到服务器的响应: ${data.message}`);
buttonText.textContent = "领取成功!";
rewardButton.backgroundColor.copy(Vec3.create({ r: 108, g: 117, b: 125 })); // 变成灰色
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
服务器代码 (index.js
)
javascript
// index.js
// 1. 监听客户端的请求
remoteChannel.onServerEvent((event) => {
const playerEntity = event.entity;
const data = event.args;
// 检查事件名,确保我们只处理领奖请求
if (data.eventName === "request_daily_reward") {
if (!playerEntity || !playerEntity.isPlayer) {
return;
}
console.log(`服务器:收到来自 ${playerEntity.player.name} 的领奖请求。`);
// 2. 在这里可以添加你的游戏逻辑
// ... (此处省略)
console.log(`服务器:向 ${playerEntity.player.name} 发放奖励。`);
// 3. 向发送请求的那个玩家发回确认消息
remoteChannel.sendClientEvent(playerEntity, {
eventName: "reward_granted",
message: "奖励已到账!",
});
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
流程解析
- 客户端发送: 在
onPointerDown()
函数中,我们调用remoteChannel.sendServerEvent()
。remoteChannel.sendServerEvent({...})
。我们将事件名request_daily_reward
作为数据对象的一个属性发送出去。 - 服务器接收: 服务器的
remoteChannel.onServerEvent(...)
监听器被触发。它会收到所有来自客户端的事件,因此我们必须在函数内部检查event.args.eventName
来判断这是不是我们想处理的事件。event.entity
告诉我们是哪个玩家发的。 - 服务器响应: 服务器调用
remoteChannel.sendClientEvent(player, {...})
。第一个参数playerEntity.player
确保了消息只发给目标玩家。我们同样将事件名reward_granted
作为数据的一部分发回。 - 客户端接收: 客户端的
remoteChannel.onClientEvent(...)
监听器被触发。它同样会收到所有来自服务器的事件,所以我们也需要检查event.args.eventName
来确认这是我们等待的响应,然后更新 UI。
现在,启动游戏,功能和之前一样,但底层的 API 调用已经是完全正确的了。
总结
恭喜你!你已经掌握了在神奇代码岛中进行双向数据通信的正确流程。这种“将事件名作为数据一部分”的模式是关键,它让你可以在一个统一的 on...Event
监听器中处理多种不同类型的通信。这是制作绝大多数网络游戏功能的基石。