游戏引擎开发总结:序列化系统

文章讲述了如何在C++中实现序列化和反序列化,使用Yaml-cpp库,通过定义接口类和多态来处理不同类型的组件。在序列化时,利用类的静态成员和unordered_map存储反序列化函数。反序列化过程中,通过类名字符串和预构建的映射表来调用相应的反序列化方法。此外,还提到了脚本组件的特殊序列化处理方式。
摘要由CSDN通过智能技术生成
序列化需要准备什么?

首先,我们需要一个被序列化类实现序列化函数,以及序列化库。准备我的序列化库是Yaml-cpp,序列话函数就命名为Serialize,另外我们不需要关心组件类型是具体是什么,所以我这边使用多态,接口类为

struct Serializer{
    virtual void Serialize() = 0; 
}

所有需要运行时序列化的类都实现接口;

另外当我们给定一个Carrier[Ecs系统中Enttity]时,我们需要知道这个实体的那些类实现了这个接口:

这边的话,我们可以在调用Carrier.AddComponent时用标准库type_traits中的std::is_base_of_v<base,T>方法来判断。那么这张表被谁持有呢?这边我是让Sence类型持有,原因如下:

1:每一个Sence的Carrier都应该被隔离,所以Serializable也应该被隔离,Sence的资源不应该交付给其他类托管;

2:方便管理

另外表的键值是组件名字的字符串;

看实现:

另外反序列话有点不同,序列话是类的数据流向文件,我们是知道组件类型的,但是反序列化是数据从文件流向组件,我们是不知道类类型,如果要调用成员函数的话,会比较复杂,所以我这边是用类静态成员,输入是Carrier和数据节点;

实现:

另外,怎么知道让哪个类调用哪个反序列化方法呢?或者说我们用什么数据将其与函数关联起来;

这边我选择的是用一张unordered_map<std::string,std::function<void(YAML::Node&,Carrier&)>>表,我们只需要在序列化的时候将类名字符串与数据对应起来,然后反序列化的时候取出节点中的对应数据即可;

接下来的问题是,我们怎么准备这张表?或者说,我们怎么在引擎loop运行前准备这张表,因为这张表是我们loop时随时可调用的;

有一个方法是硬编码,直接在邀请运行前增加unordered_map::emplace方法,但是...这太蠢了,硬编码一向不是我的风格;

我这边是利用了inline static变量会在运行main函数的第一段代码前都初始化好的特性,这意味这我们可以在引擎运行前就运行一些代码,而且这些代码是可以分布在不同类中的,这为我们软初始化表提供了可能;

我这边是定义了一个类,这种运行引擎前的做初始化的工具类我统称为Assistant类:

    template<class T>
    struct SerializeAssistant {
        SerializeAssistant(::std::string name, std::function<void(YAML::Node& node, Carrier& carrier)> deSerializerFun) {
            if constexpr (std::is_base_of_v<Serializable, T>)
                SenceSerializer::GetDeserializerFunMap().emplace(name, deSerializerFun);
            else
                LC_CORE_ASSERT(false, "Serializable permission needs to be added");

        }
    };

这边为什么是个模板,这个是另外一个组件依赖的系统,我称之为权限系统,作用是规范代码,这里不做介绍;

那么这张表被谁持有呢?当然不能被Sence持有,因为Sence不是唯一的,但是表是唯一的,被因为不是Carrier的属性,所以不能被Sence持有,这边我是让Serializer类持有;

我这边定义了一个宏:

#define COMPONENTREGISTER(ClassName) inline static DynamicEmbedAssistant<ClassName> DynamicEmbedAss{#ClassName,&ClassName::OnEmbed};

在类完善这个宏可以将该类中的反序列花函数登记到Serializer中所持有的表中;

到此为止,我们已经有两张表了,一张是运行时可动态增删的表,里面存储着每一个Carrier持有的可序列化组件指针,另外一张表是运行时确定的表,里面存储着每一个组件的反序列化函数;

大概的序列化步骤是这样的:

另外还有一张小表需要维护,那就是每一个Carrier持有的组件的类名字符串,这张表也会被流向文件当中,在反序列化时会被作为索引,指定对应的Carrier需要调用那些反序列化函数

准备前工作:将每个组件的反序列化函数登记;

运行时工作:当一个Carrier增加一个组件,我们需要判断这个组件是否实现了Serializable接口,如果实现了,那么在Sence的序列化指针表中增加对应的指针

序列化:

1:实例化一个接受数据的emitter

2:便利Scene中的每一个Carrier,调用该Carrier中的所有的序列化指针,其会将组件进行序列化;

3:将序列化好的数据流向文件

反序类化:

1:实例化一个新的Scene,并提供对应的数据

2:提取小表节点,遍历每一个Carrier需要调用那些反序列话函数,并将数据数据节点和Carrier传入反序列化函数,向Carrier中流入数据

///

补充内容

//

脚本的序列化和反序列化:

脚本的序列化和组件的序列化有点不同,这是因为脚本组件内存中是不持有脚本的实例的,而是持有脚本的多态指针,正因为如此,所以我们不能用组件的普通序列化方法了;

脚本组件的序列化和普通组件的序列化差不多,我们只需要将脚本的名字和脚本的数据给流向文件即可,名字在绑定脚本的时候可以得到,这个名字在反序列话的时候是作为索引的,数据的话,直接调用脚本的序列化方法即可;

反序列化的话,这里我们需要维护一张表,这个表的键是std::string,即脚本类的名字,值是脚本的反序列化静态函数,为什么是静态的话和普通脚本是一个理由;

然后在反序列化的时候数据数据节点中的名字索引,我们可以调用表中的具体函数,最后得到序列化后的脚本,因为这个时候我们已经没有类型信息了,所以只能通过多态绑定脚本函数;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值