如何将类类型写入二进制文件并还原以实现关卡加载系统?
解决二进制关卡加载中的类类型序列化与还原问题
首先得明确:typeid(type).name()的结果不能用于反向映射类类型,因为它是编译器实现定义的——不同编译器(甚至同一编译器的不同版本)返回的字符串格式都不一样,完全没有标准化的保证,所以这条路走不通。下面给你几个可行的方案,从手动控制到依赖第三方库,你可以根据项目需求选择:
方案1:手动分配唯一类型ID(最可靠、无依赖)
这是游戏开发中最常用的方案,核心思路是给每个需要序列化的类分配一个固定的唯一标识(比如整数枚举),写入二进制时存这个ID,读取时用工厂模式根据ID创建对应对象。
示例代码:
#include <memory> #include <unordered_map> #include <fstream> // 所有可生成对象的基类 class GameObject { public: virtual ~GameObject() = default; // 序列化:写入自身数据到二进制流(先写类型ID,再写属性) virtual void serialize(std::ostream& stream) const = 0; // 反序列化:从二进制流读取属性 virtual void deserialize(std::istream& stream) = 0; }; // 自定义类型ID枚举,每个可序列化类对应唯一值 enum class ObjectType { Player, Enemy, Collectible, // 新增类时在这里添加对应的ID }; // 对象工厂:负责根据类型ID创建实例 class GameObjectFactory { private: using Creator = std::unique_ptr<GameObject>(*)(); std::unordered_map<ObjectType, Creator> creators_; public: // 注册类型与对应的创建函数 void register_type(ObjectType type, Creator func) { creators_[type] = func; } // 根据ID创建对象 std::unique_ptr<GameObject> create(ObjectType type) { auto it = creators_.find(type); return it != creators_.end() ? it->second() : nullptr; } }; // 全局工厂实例(也可以用单例模式封装) GameObjectFactory g_ObjectFactory; // 示例子类:Player class Player : public GameObject { public: float pos_x = 0.f; float pos_y = 0.f; float scale = 1.f; void serialize(std::ostream& stream) const override { // 先写入类型ID const ObjectType type = ObjectType::Player; stream.write(reinterpret_cast<const char*>(&type), sizeof(type)); // 再写入属性数据 stream.write(reinterpret_cast<const char*>(&pos_x), sizeof(pos_x)); stream.write(reinterpret_cast<const char*>(&pos_y), sizeof(pos_y)); stream.write(reinterpret_cast<const char*>(&scale), sizeof(scale)); } void deserialize(std::istream& stream) override { stream.read(reinterpret_cast<char*>(&pos_x), sizeof(pos_x)); stream.read(reinterpret_cast<char*>(&pos_y), sizeof(pos_y)); stream.read(reinterpret_cast<char*>(&scale), sizeof(scale)); } }; // 自动注册Player到工厂(匿名命名空间确保只执行一次) namespace { bool registerPlayer = [](){ g_ObjectFactory.register_type(ObjectType::Player, [](){ return std::make_unique<Player>(); }); return true; }(); } // 从二进制流加载单个对象的函数 std::unique_ptr<GameObject> loadGameObject(std::istream& stream) { ObjectType type; // 先读取类型ID stream.read(reinterpret_cast<char*>(&type), sizeof(type)); // 创建对象 auto obj = g_ObjectFactory.create(type); if (obj) { // 反序列化属性 obj->deserialize(stream); } return obj; }
优点:
- 完全可控,没有依赖,跨平台兼容性拉满
- 性能高,类型ID是整数,读写和查找都极快
- 新增类时只需要添加枚举值+注册代码,逻辑清晰
方案2:类型注册表+自定义字符串ID(减少手动枚举)
如果不想维护枚举,可以用自定义字符串标识(比如"Player"、"Enemy")配合类型注册表,写入时存字符串,读取时根据字符串查找对应的创建函数。
示例代码:
#include <memory> #include <unordered_map> #include <string> class GameObjectFactory { private: using Creator = std::unique_ptr<GameObject>(*)(); std::unordered_map<std::string, Creator> creators_; public: // 模板方法,自动推导类型 template<typename T> void registerType(const std::string& typeName) { creators_[typeName] = [](){ return std::make_unique<T>(); }; } std::unique_ptr<GameObject> create(const std::string& typeName) { auto it = creators_.find(typeName); return it != creators_.end() ? it->second() : nullptr; } }; // 注册示例 g_ObjectFactory.registerType<Player>("Player"); g_ObjectFactory.registerType<Enemy>("Enemy");
写入时把字符串"Player"写入二进制,读取时先读字符串,再调用create方法即可。这种方案比枚举更灵活,但字符串读写的性能略低于整数,而且要确保字符串唯一(可以用宏定义统一管理)。
方案3:使用成熟的序列化库(最省心)
如果项目允许引入第三方库,直接用现成的序列化库可以省去手动处理类型映射的麻烦,比如:
- Boost.Serialization:C++老牌序列化库,支持多态类型的自动序列化
- Protobuf/FlatBuffers:Google出品的跨平台序列化方案,性能极高,适合游戏场景
- nlohmann/json:如果可以接受JSON格式(虽然不是二进制,但也可以转二进制存储),用法非常简洁
以Boost.Serialization为例,它会自动处理类型信息的序列化与反序列化,示例代码:
#include <boost/serialization/serialization.hpp> #include <boost/serialization/export.hpp> #include <fstream> #include <memory> class GameObject { friend class boost::serialization::access; template<class Archive> void serialize(Archive& ar, const unsigned int version) { // 序列化基类通用数据(如果有的话) } public: virtual ~GameObject() = default; }; class Player : public GameObject { friend class boost::serialization::access; template<class Archive> void serialize(Archive& ar, const unsigned int version) { // 先序列化基类 ar & boost::serialization::base_object<GameObject>(*this); // 再序列化自身属性 ar & pos_x & pos_y & scale; } public: float pos_x = 0.f; float pos_y = 0.f; float scale = 1.f; }; // 导出Player类型,让序列化库识别它 BOOST_CLASS_EXPORT(Player) // 保存对象到二进制文件 void saveGameObject(const GameObject& obj, const std::string& path) { std::ofstream ofs(path, std::ios::binary); boost::archive::binary_oarchive oa(ofs); oa & obj; } // 从二进制文件加载对象 std::unique_ptr<GameObject> loadGameObject(const std::string& path) { std::ifstream ifs(path, std::ios::binary); boost::archive::binary_iarchive ia(ifs); std::unique_ptr<GameObject> obj; ia & obj; return obj; }
优点:
- 几乎不需要手动处理类型映射,库会自动搞定
- 支持复杂数据结构、多态、版本兼容等高级特性
- 跨语言兼容性好(比如Protobuf可以和Java、Python交互)
总结
- 如果追求极致可控和性能,选方案1(手动类型ID+工厂),这是游戏行业的标准做法
- 如果想减少枚举维护的工作量,选方案2(自定义字符串ID+注册表)
- 如果项目可以引入第三方库,选方案3(成熟序列化库),省心省力
内容的提问来源于stack exchange,提问作者fes dfadfa
相关产品推荐
相关产品推荐

