ET6.0关于序列化和反序列化的过程
昨天因为传送需求导致的传送之后找不到挂载组件的问题让人困扰很久,咨询大佬们后才知道原理,以前研究Demo时因为没有相关需求一直没有关注Entity这块的逻辑。
问题帖子:https://et-framework.cn/d/448-20cnyiserializetoentity
需求描述:
ET的每个Scene都是一个进程,实体发送给DBScene保存,或者实体在MapScene之间传送时,这种跨进程转移需要把实体序列化发送过去,而序列化之后实体之间的父子关系将会消失。
那么问题来了,对DB保存来说,对实体下的每个Child和Component单独保存显然很不方便,传送也是一样,传送过去重新挂载关系也是繁琐的事情,而猫大显然不会用这种愚蠢的办法,于是ET提供了IDeserialize和ISerializeToEntity两个接口来处理这个需求。
2022.04.24:字母哥课程的第18节课详细的讲解了Entity的这个设计,这里就当做了个笔记。
问题:Entity怎么在反序列化时找回自己的子节点树,并重新组装起来?带着这个问题翻代码。
1、创建实体需要用AddChild方法(仅以Child为例,Component大同小异就不整理了),有一句是设置Parent,那么设置Parent时发生了什么?
查看代码
public Entity AddChild(Type type, bool isFromPool = false)
{
Entity component = Entity.Create(type, isFromPool);
component.Id = IdGenerater.Instance.GenerateId();
component.Parent = this;
EventSystem.Instance.Awake(component);
return component;
}
2、跳转到public Entity Parent,注意到设置Parent时有一个AddToChildren方法,做了什么处理?
查看代码
// 可以改变parent,但是不能设置为null
[IgnoreDataMember]
[BsonIgnore]
public Entity Parent
{
get => this.parent;
private set
{
if (value == null)
{
throw new Exception($"cant set parent null: {this.GetType().Name}");
}
if (value == this)
{
throw new Exception($"cant set parent self: {this.GetType().Name}");
}
// 严格限制parent必须要有domain,也就是说parent必须在数据树上面
if (value.Domain == null)
{
throw new Exception($"cant set parent because parent domain is null: {this.GetType().Name} {value.GetType().Name}");
}
if (this.parent != null) // 之前有parent
{
// parent相同,不设置
if (this.parent == value)
{
Log.Error($"重复设置了Parent: {this.GetType().Name} parent: {this.parent.GetType().Name}");
return;
}
this.parent.RemoveFromChildren(this);
}
this.parent = value;
this.IsComponent = false;
this.parent.AddToChildren(this); //添加到ChildRen字典
this.Domain = this.parent.domain;
}
}
3、每个实体都有一个Children字段,以一个字典类型保存自己的子节点,AddChildren又调用了AddChildrenDB,在AddChildrenDB看到了ISerializeToEntity接口,原来有ISerializeToEntity接口的实体才会加入到Children保存。但是这个时候还是没有涉及到反序列化之后怎么组装,只知道每个实体有保存自己下一级的节点列表。
查看代码
[IgnoreDataMember]
[BsonIgnore]
public Dictionary<long, Entity> Children
{
get
{
if (this.children == null)
{
this.children = MonoPool.Instance.Fetch<Dictionary<long, Entity>>();
}
return this.children;
}
}
private void AddToChildren(Entity entity)
{
this.Children.Add(entity.Id, entity);
this.AddToChildrenDB(entity);
}
private void RemoveFromChildren(Entity entity)
{
if (this.children == null)
{
return;
}
this.children.Remove(entity.Id);
if (this.children.Count == 0)
{
MonoPool.Instance.Recycle(this.children);
this.children = null;
}
this.RemoveFromChildrenDB(entity);
}
private void AddToChildrenDB(Entity entity)
{
if (!(entity is ISerializeToEntity))
{
return;
}
this.childrenDB = this.childrenDB ?? MonoPool.Instance.Fetch<HashSet<Entity>>();
this.childrenDB.Add(entity);
}
private void RemoveFromChildrenDB(Entity entity)
{
if (!(entity is ISerializeToEntity))
{
return;
}
if (this.childrenDB == null)
{
return;
}
this.childrenDB.Remove(entity);
if (this.childrenDB.Count == 0 && this.IsNew)
{
MonoPool.Instance.Recycle(this.childrenDB);
this.childrenDB = null;
}
}
4、再找Children和childrenDB在哪里调用,跳转到public Entity Domain这里。
正常来说Domain就是某个Scene,一般情况下不会直接Set Domain,只有创建新的Unit时才会Set Domain,序列化之后其实是要在新的Scene创建新的实体,再把传过去的对象赋值给新的实体,而创建实体则会执行Domain逻辑,检查childrenDB是否为空,如果不为空则重新设置Parent并重新加入到新实体的Children字典,再递归层层设置每一级子节点的子节点来恢复完整的节点树。
查看代码
[IgnoreDataMember]
[BsonIgnore]
public Entity Domain
{
get
{
return this.domain;
}
private set
{
if (value == null)
{
throw new Exception($"domain cant set null: {this.GetType().Name}");
}
if (this.domain == value)
{
return;
}
Entity preDomain = this.domain;
this.domain = value;
if (preDomain == null)
{
this.InstanceId = IdGenerater.Instance.GenerateInstanceId(); //Entity生成新的InstanceID
this.IsRegister = true;
// 反序列化出来的需要设置父子关系
if (this.componentsDB != null)
{
foreach (Entity component in this.componentsDB)
{
component.IsComponent = true;
this.Components.Add(component.GetType(), component);
component.parent = this;
}
}
if (this.childrenDB != null)
{
foreach (Entity child in this.childrenDB)
{
child.IsComponent = false;
this.Children.Add(child.Id, child);
child.parent = this;
}
}
}
// 递归设置孩子的Domain
if (this.children != null)
{
foreach (Entity entity in this.children.Values)
{
entity.Domain = this.domain;
}
}
if (this.components != null)
{
foreach (Entity component in this.components.Values)
{
component.Domain = this.domain;
}
}
if (!this.IsCreated)
{
this.IsCreated = true;
EventSystem.Instance.Deserialize(this); //调用EventSystem的反序列化方法,执行Entity下全部节点的DeserializeSystem
}
}
}
5、好像到这里已经很完美了,跨进程之后能找回全部节点关系,那么IDeserialize又是什么作用?
ET的传送和保存是通过Bson消息直接发送实体,而Bson只支持string Key的字典序列化,否则会报错。这时很容易想到用[BsonDictionaryOptions(DictionaryRepresentation.ArrayOfArrays)]标签,实际上不需要,因为如果道具已经作为Child挂在背包下,序列化之后发给DBScene的Entity,反序列化已经包含了整个节点树,MongoDB会一起保存,如果还要保存字典就重复了,因此正确的做法是用[BsonIgnore]标签忽略序列化字典。(List也是一样,需要忽略序列化)
但是传送怎么办?保存可以不管容器内容直接Save即可,但传送之后还得恢复容器内容,这时就需要用到Deserialize接口来处理。Domain逻辑最后一部分会调用EvenetSystem的Deserialize方法,遍历执行Entity子节点的DeserializeSystem,即反序列化时的处理,可以在这里遍历Entity.Children.Values重新恢复容器内容。