作为一个重要的里程碑,我们将完善自己的协议和通知,在本章结束的时候,我们希望能看到两个独立的客户端登录进入元宇宙,并能看见彼此,以及彼此的移动。
我们首先整理我们的通知结构,在最早的版本中,我们混淆了需要通知的目标和事件的主人,这样造成了不少困扰,最早的通知结构是这样的
pub enum NotifyType {
Enter(u32, Vec<(u32, Position, Movement)>),
Leave(u32, Vec<u32>),
}
复制代码
实际上,前面的 u32 是要通知的对象,后面的 Vec 才是 事件的主人,所以我们将上面的结构改写如下:
pub enum NotifyType {
Enter(Vec<(u32, RoleBase)>),
Leave(Vec<u32>),
Move(u32, Movement),
Stop(u32, Position),
}
复制代码
NotifyType 本身仅仅表示通知事件本身,当发生需要通知事件的时候,我们采用这样的 channel 来传递通知的目标和通知的事件:
pub notifier: mpsc::Sender<(Vec<u32>, NotifyType)>,
复制代码
一个 tuple 前面是通知的目标数组,后面是通知事件。
比如说当一个玩家进入元宇宙之后,我们需要通知这个玩家,他周围的其他人,也需要通知周围其他人,这个玩家进入元宇宙了。代码如下:
let bases = self.get_role_infos(&neighbor);
self.notifier.blocking_send((vec![id], NotifyType::Enter(bases))).ok();
self.notifier.blocking_send((neighbor.clone(), NotifyType::Enter(vec![(id, RoleBase{pos: pos.clone(), action: action.clone()})] ))).ok();
复制代码
base 是周围所有玩家的位置信息,所以通知自己 id,周围所有玩家的信息,然后最后一句通知所有的邻居,自己的位置信息。
我们的位置服务 world 可以产生 这些通知 i 消息,但是 world 本身并不持有 连接上来 客户端的 Websocket 端口,如果按照传统 C/C++的方案,这个时候,我们需要将 Websocket 端口的指针 告诉位置服务,但是由于位置服务和 连接服务(实际持有 Websocket 端口的服务)处于不同的线程中,我们需要在访问 Websocket 加上 加锁和解锁的代码,如果这个锁跟逻辑相关的话,那简直是一场噩梦,服务器的 crash 和死锁多半就会随之而来。。。
我们用 channel 来避免这些问题,在异步主循环中,我们接收 world 服务的通知消息,封装后转发到 连接服务,也就是 agent service,代码如下:
tokio::select! {
msg = rx.recv() => {
msg.map(|msg| share::rpc::run(&agent_caller, AgentRequest::Notify(msg.0, msg.1)) );
}
复制代码
其中 msg 就是刚才所定义的 tuple。
当持有 客户端 Websocket 端口的 agent service 接收到这个 tuple 的消息之后,找到对应的 websocket 端口,并转发给客户端。
match msg {
AgentRequest::Notify(ids, msg)=> {
for id in &ids {
self.id_txs.get(id).map(|tx| tx.send(AgentMsg::Notify(msg.clone())));
}
}
复制代码
Stop 通知消息有些特殊性,为了性能考虑,当玩家开始移动之后,我们不会任何时候都去保存玩家在元宇宙中的位置,只要服务器不挂掉,只要在玩家结束移动的时候保存就行了,所以我们在 得到 Stop 消息,需要保存一下玩家信息(玩家离线的时候触发停止移动,是一个合理的假设,这样离线之后,玩家再次连线会回到上次离开的地方)。
if let NotifyType::Stop(id, pos) = msg {
if ids.len() > 0 && ids[0] == id {
self.sync_pos(id, pos);
}
}
复制代码
如果是通知自己的停止消息,就保存一下数据。
我们在客户端同样使用 SDL2 来显示简单的角色,所以我们把 移动的相关代码 抽象出来,放在 share/pos.rs 中:
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RoleBase {
pub pos: Position,
pub action: Movement,
}
impl RoleBase {
pub fn move_to(&mut self, x: f32, y: f32, speed: f32) {
let x_distance = x - self.pos.x;
let y_distance = y - self.pos.y;
let time = (x_distance * x_distance + y_distance * y_distance).sqrt() / speed * 1000.0;
self.action.x = x;
self.action.y = y;
self.action.x_speed = x_distance / time;
self.action.y_speed = y_distance / time;
}
pub fn is_moving(&self)-> bool {
self.action.x_speed != 0.0 || self.action.y_speed != 0.0
}
pub fn stop(&mut self)-> Option<Position> {
if self.is_moving() {
self.action.x_speed = 0.0;
self.action.y_speed = 0.0;
Some(self.pos.clone())
} else {
None
}
}
pub fn step(&mut self, mills: f32) {
self.pos.x += self.action.x_speed * mills;
if self.action.x_speed > 0.0 && self.pos.x > self.action.x { self.pos.x = self.action.x; }
else if self.action.x_speed < 0.0 && self.pos.x < self.action.x { self.pos.x = self.action.x; }
self.pos.y += self.action.y_speed * mills;
if self.action.y_speed > 0.0 && self.pos.y > self.action.y { self.pos.y = self.action.y; }
else if self.action.y_speed < 0.0 && self.pos.y < self.action.y { self.pos.y = self.action.y; }
if self.action.x == self.pos.x { self.action.x_speed = 0.0 }
if self.action.y == self.pos.y { self.action.y_speed = 0.0 }
}
}
复制代码
这样,服务器和客户端都可以支持角色的移动了。
我们在 计算速度分量的时候 乘了 1000,这样移动的单位就是毫秒/米(假设位置的单位都是米的话)。
我们使用下面的结构表示一个 元宇宙客户端的信息:
struct GameClient {
player: Option<(u32, Player)>,
neighbor: BTreeMap<u32, Player>,
now: std::time::Instant,
display: Display,
deal_rx: crossbeam_channel::Receiver<AgentMsg>,
}
复制代码
player 是自己,neighbor 是可以看到的临近玩家 deal_rx 用来处理命令行消息,或者今后的复杂客户端发送的请求。
客户端处理消息的代码如下:
fn deal(&mut self, msg: AgentMsg) {
match msg {
AgentMsg::LoginOk(id, player)=> {
self.player = Some((id, player));
},
AgentMsg::Notify(notify)=> {
match notify {
NotifyType::Enter(players)=> {
for (id, base) in players {
if let Some(player) = self.neighbor.get_mut(&id) {
player.base = base;
} else {
self.neighbor.insert(id, Player{nick: "".into(), world: 0, base});
}
}
}
NotifyType::Leave(ids)=> {
for id in &ids {
self.display.hide_node(*id);
}
}
NotifyType::Move(id, act)=> {
if !self.player.as_mut().map(|(_id, p)|
if id == *_id {
p.base.action = act.clone();
true
} else { false }
).unwrap_or(false) {
self.neighbor.get_mut(&id).map(|r| r.base.action = act );
}
}
_=> {}
}
},
_=>{}
}
}
复制代码
我们处理了进入离开和移动通知消息。
现在合并在一起,我们连接客户端到元宇宙中:
可以看到屏幕上一个孤零零的自己
我们在服务器运行脚本 登录一个之前创建好的角色 zhuzhu
可以看到一个同伴
我们再登录一个角色
没有变化,因为位置太远了,我们把这个角色移动到可视范围内
下面的视频我们可以看到,角色开始动起来了,而且是别的角色移动,同步到身为邻居的自己
评论