写点什么

Rust 元宇宙 16 —— 里程碑,二人世界

作者:Miracle
  • 2021 年 12 月 11 日
  • 本文字数:3086 字

    阅读完需:约 10 分钟

Rust 元宇宙 16 —— 里程碑,二人世界

作为一个重要的里程碑,我们将完善自己的协议和通知,在本章结束的时候,我们希望能看到两个独立的客户端登录进入元宇宙,并能看见彼此,以及彼此的移动。

我们首先整理我们的通知结构,在最早的版本中,我们混淆了需要通知的目标和事件的主人,这样造成了不少困扰,最早的通知结构是这样的

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

可以看到一个同伴

我们再登录一个角色

没有变化,因为位置太远了,我们把这个角色移动到可视范围内


下面的视频我们可以看到,角色开始动起来了,而且是别的角色移动,同步到身为邻居的自己



发布于: 4 小时前阅读数: 26
用户头像

Miracle

关注

三十年资深码农 2019.10.25 加入

还未添加个人简介

评论

发布
暂无评论
Rust 元宇宙 16 —— 里程碑,二人世界