EnTT-(de)serialization with nlohmann::json

Again playing with EnTT (ECS) which has a builtin mechanism that helps you (de)serialize a registry. You have to implement an 'archive' that (de)serializes the data.
Here in the source-code I use nlohmann::json for this. In the end it was simpler than it seemed at the beginning.
The Input/Output-Archives:
#pragma once
#include <nlohmann/json.hpp>
#include <entt/entt.hpp>
// nlohmann-json based entt-(de)serialization
class NJSONOutputArchive {
public:
NJSONOutputArchive(){
root = nlohmann::json::array();
};
// new element for serialization. giving you the amount of elements that is going to be
// pumped into operator()(entt::entity ent) or operator()(entt::entity ent, const T &t)
void operator()(std::underlying_type_t<entt::entity> size){
int a=0;
if (!current.empty()){
root.push_back(current);
}
current = nlohmann::json::array();
current.push_back(size); // first element of each array keeps the amount of elements.
}
// persist entity ids
void operator()(entt::entity entity){
// Here it is assumed that no custom entt-type is chosen
current.push_back((uint32_t)entity);
}
// persist components
// ent is the entity and t a component that is attached to it
// in json we first push the entity-id and then convert the component
// to json just by assigning: 'nlohmann:json json=t'
// For this to work all used component musst have following in its body:
// NLOHMANN_DEFINE_TYPE_INTRUSIVE([component_name], fields,....)
// e.g.
// struct Transform {
// float x;
// float y;
//
// NLOHMANN_DEFINE_TYPE_INTRUSIVE(Transform, x,y)
// };
//
template<typename T>
void operator()(entt::entity ent, const T &t){
current.push_back((uint32_t)ent); // persist the entity id of the following component
// auto factory = entt::type_id<T>();
// std::string component_name = std::string(factory.name());
// current.push_back(component_name);
nlohmann::json json = t;
current.push_back(json);
}
void Close(){
if (!current.empty()){
root.push_back(current);
}
}
// create a json as string
const std::string AsString() {
std::string output = root.dump();
return output;
}
// create bson-data
const std::vector<uint8_t> AsBson(){
std::vector<std::uint8_t> as_bson = nlohmann::json::to_bson(root);
return as_bson;
}
private:
nlohmann::json root;
nlohmann::json current;
};
class NJSONInputArchive {
private:
nlohmann::json root;
nlohmann::json current;
int root_idx=-1;
int current_idx=0;
public:
NJSONInputArchive(const std::string& json_string)
{
root = nlohmann::json::parse(json_string);
};
~NJSONInputArchive(){
}
void next_root(){
root_idx++;
if (root_idx >= root.size()){
// ERROR
return;
}
current = root[root_idx];
current_idx = 0;
}
void operator()(std::underlying_type_t<entt::entity> &s){
next_root();
int size = current[0].get<int>();
current_idx++;
s = (std::underlying_type_t<entt::entity>)size; // pass amount to entt
}
void operator()(entt::entity &entity){
uint32_t ent = current[current_idx].get<uint32_t>();
entity = entt::entity(ent);
current_idx++;
}
template<typename T>
void operator()(entt::entity &ent, T &t){
nlohmann::json component_data = current[current_idx*2];
auto comp = component_data.get<T>();
t = comp;
uint32_t _ent = current[current_idx*2-1];
ent = entt::entity(_ent); // last element is the entity-id
current_idx++;
}
};
Some sample structs:
#include <nlohmann/json.hpp>
#include <entt/entt.hpp>
#include <string>
struct Tower{
std::string name;
int type_id;
float range;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(Tower, name, type_id, range)
};
struct Walker {
int type_id;
float speed;
entt::entity target;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(Walker, type_id,speed,target)
};
struct Transform {
float x;
float y;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(Transform, x,y)
};
And test it:
void test()
{
entt::registry reg;
auto e1 = reg.create();
Tower t{"Tower", 1895, 18.95f};
t.name = "hansi";
Tower tw1 = reg.emplace<Tower>(e1, std::string("tower1"), 1895, 18.95f);
reg.emplace<Transform>(e1, 1.0f, 1.0f);
auto e2 = reg.create();
Tower tw2 = reg.emplace<Tower>(e2, "tower2", 18, 0.95f);
reg.emplace<Transform>(e2, 5.0f, 5.0f);
auto w1 = reg.create();
Walker &w = reg.emplace<Walker>(w1, 1, 0.5f);
reg.emplace<Transform>(w1, 100.0f, 100.0f);
NJSONOutputArchive json_archive;
entt::basic_snapshot snapshot(reg);
snapshot.entities(json_archive)
.component<Tower, Walker, Transform>(json_archive);
json_archive.Close();
std::string json_output = json_archive.AsString();
printf("json:%s", json_output.c_str());
NJSONInputArchive json_in(json_output);
entt::registry reg2;
entt::basic_snapshot_loader loader(reg2);
loader.entities(json_in)
.component<Tower, Walker, Transform>(json_in);
auto _e1 = entt::entity(0);
auto _e2 = entt::entity(1);
auto [tower, transform] = reg2.get<Tower, Transform>(e1);
auto [tower2, transform2] = reg2.get<Tower, Transform>(e2);
}
This will result in following JSON:
[[3,0,1,2,1048575],
[2,0,{"name":"tower1","range":18.950000762939453,"type_id":1895},1,{"name":"tower2","range":0.949999988079071,"type_id":18}],
[1,2,{"speed":0.5,"target":0,"type_id":1}],
[3,0,{"x":1.0,"y":1.0},1,{"x":5.0,"y":5.0},2,{"x":100.0,"y":100.0}]]
Line 1(EntityIDs): [0]=amount of entities,[1-3]=entity-ids,[4]=destroyed(not sure what this is and why it is serialized)
Line 2(Tower): [0]=amount of components,[1,3]=entity-id,[2,4]=component data to be attached to the entity of [1,3]
Line 3(Walker):[0]=amount of components,[1]=entity-id.....
Line 4(Transform):....
Let's recall that how we called the snapshot:
snapshot.entities(json_archive)
.component<Tower, Walker, Transform>(json_archive);
You see that the component serialization was in exactly the order that we specified and it is important that the order is in the same in the snapshot_loader....