显然,元宇宙中玩家拥有自己的状态和数字资产,这些状态独立的存在于虚拟的元宇宙中,和现实世界无关,通过在元宇宙的各种行为,玩家可以获得自己的数字资产,并存放在元宇宙中。
这些数字资产必须是持续化的,不管玩家在不在元宇宙中,数据都应该独立存在。
结合区块链的技术,以及现代的加密技术,我们可以为玩家在元宇宙的数字资产打上数字签名,铸造独一无二的玩家数字资产 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 我们用一种普适的方法实现了 角色数据的持续化存储。
评论