EnTT-(de)serialization with nlohmann::json

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++;
    }
};
Input-/Outputarchive

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....