写点什么

Rust 元宇宙 12 —— 序列化和存储

作者:Miracle
  • 2021 年 12 月 02 日
  • 本文字数:2308 字

    阅读完需:约 8 分钟

Rust 元宇宙 12 —— 序列化和存储

显然,元宇宙中玩家拥有自己的状态和数字资产,这些状态独立的存在于虚拟的元宇宙中,和现实世界无关,通过在元宇宙的各种行为,玩家可以获得自己的数字资产,并存放在元宇宙中。

这些数字资产必须是持续化的,不管玩家在不在元宇宙中,数据都应该独立存在。

结合区块链的技术,以及现代的加密技术,我们可以为玩家在元宇宙的数字资产打上数字签名,铸造独一无二的玩家数字资产 NFT,这些资产可以在元宇宙中转移和交换,所有的一切都需要对元宇宙世界中的对象进行序列化,并持续存储于非易失介质,比如说磁盘上。


我们首先定义最基本的玩家信息如下:

#[derive(Debug, Serialize, Deserialize, Clone)]pub struct Player {    pub nick: String,    pub world: u32,    pub pos: Position,}
复制代码

玩家的昵称,所在元宇宙的序号(比如说 第 9527 号宇宙),以及玩家在该宇宙中的坐标。随着我们元宇宙内容的不断丰富,这个结构也会不断被扩展,增加 玩家的 好友、属性、战斗力、装备、背包、资产等等等等。


为了节省内存,我们之前说过,使用一个 u32 的数据类型来唯一标识 元宇宙世界里面的玩家。所以我们需要一个映射表 token 是玩家的唯一标识,token 到 u32 ID 的映射,以及到 Player 数据的映射。

u32 足够大,对于一个元宇宙来说,在服务器的生存周期内足够大,所以我们不考虑 u32 ID 的服用,这样可以避免很多潜在的问题,也就是说 一个玩家在游戏服务器 开机之后,所拥有的 ID 是唯一的。


我们使用下面的数据来实现这种映射:

pub struct IDMap<K, V> {    values: Vec<Option<V>>,    idx: HashMap<K, u32>}
impl<K: std::cmp::Eq + std::hash::Hash + Clone, V> IDMap<K, V> { pub fn new()-> IDMap<K, V> { IDMap{ values: Vec::new(), idx: HashMap::new() } }
pub fn get<Q: ?Sized>(&self, k: &Q)-> Option<u32> where K: std::borrow::Borrow<Q>, Q: std::hash::Hash + std::cmp::Eq, { self.idx.get(k).map(|id| *id ) }
pub fn add(&mut self, key: K, value: V)-> u32 { if let Some(id) = self.idx.get(&key) { self.values[*id as usize] = Some(value); return *id; } let id = self.values.len() as u32; self.values.push(Some(value)); self.idx.insert(key, id.clone()); id }
pub fn remove(&mut self, id: u32) { self.values[id as usize] = None; }}
复制代码

get 获取 token 对应的玩家 ID ,add 增加一个玩家到元宇宙,remove 是玩家离开,但是 ID 不会复用。


下面代码重载 [] 运算符,这样可以使用 [id] 获得玩家数据

impl<K, V> Index<u32> for IDMap<K, V> {    type Output = Option<V>;    fn index(&self, index: u32) -> &Self::Output {        &self.values[index as usize]    }}
impl<K, V> IndexMut<u32> for IDMap<K, V> { fn index_mut(&mut self, index: u32) -> &mut Self::Output { &mut self.values[index as usize] }}
复制代码


实现这个简单的数据结构之后,我们考虑如何对玩家的数据进行持续化。


关系数据库还是 KV 数据库

剑侠情缘网络版最早的版本中,玩家的数据是采用 Ms SQL Server 存放的,有段时间存储玩家数据的开销,达到了难以忍受的程度,后来我们仔细分析之后发现,游戏玩家的数据,在整个游戏世界内部,几乎没有关系查询的需求,也就是说,按照年龄 性别 门派等等去查询,可以放在事后进行,而不用在游戏世界运行的过程中维护他们的索引。

所以最早的 剑侠情缘网络版服务器使用了 BerkelyDB 这个最早的 KV 库来存储玩家的数据(BDB 也是后来 MySQL 采用的底层数据存储引擎之一)。


Rust 有众多的 KV 数据库,我们采用看起来超级有前途,像光一样块的 Sled。


下面的代码使用 MessagePack 作为序列化的格式(还记得我们之前用 MessagePack 作为协议格式吗,一举多得,我们顺便把持续化的格式也搞定了)

pub struct Manager {    agents: IDMap<String, Player>,    db: sled::Db,}
impl Manager { pub fn new(db_path: &str)-> Manager { let db = sled::open(db_path).unwrap(); Manager{ agents: IDMap::new(), db } }
pub fn save(&self, token: &str) { let mut buf = Vec::new(); if let Some(id) = self.agents.get(token) { if self.agents[id].serialize(&mut Serializer::new(&mut buf)).is_ok() { self.db.insert(token.to_string(), buf); } } }
pub fn create(&mut self, token: &str, nick: &str)-> u32 { let pos = Position{x: rand::random::<f32>() % 1024.0, y: rand::random::<f32>() % 1024.0}; let id = self.agents.add(token.into(), Player{nick: nick.into(), world: 0, pos}); self.save(token); id }
pub fn load(&mut self, token: &str)-> Option<u32> { if let Some(id) = self.agents.get(token) { return Some(id); } if let Ok(Some(buf)) = self.db.get(token.as_bytes()) { let mut de = rmp_serde::Deserializer::from_read_ref(&buf); let player: Player = Deserialize::deserialize(&mut de).unwrap(); Some(self.agents.add(token.into(), player)) } else { None } }}
复制代码

注意看 save 和 load 的细节,借助 serde 我们用一种普适的方法实现了 角色数据的持续化存储。

发布于: 2021 年 12 月 02 日阅读数: 21
用户头像

Miracle

关注

三十年资深码农 2019.10.25 加入

还未添加个人简介

评论

发布
暂无评论
Rust 元宇宙 12 —— 序列化和存储