作为一个重要的里程碑,我们将完善自己的协议和通知,在本章结束的时候,我们希望能看到两个独立的客户端登录进入元宇宙,并能看见彼此,以及彼此的移动。
我们首先整理我们的通知结构,在最早的版本中,我们混淆了需要通知的目标和事件的主人,这样造成了不少困扰,最早的通知结构是这样的
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
可以看到一个同伴
我们再登录一个角色
没有变化,因为位置太远了,我们把这个角色移动到可视范围内
下面的视频我们可以看到,角色开始动起来了,而且是别的角色移动,同步到身为邻居的自己
评论