Skip to content

ECS Parser

Thomaltarix edited this page Oct 13, 2024 · 1 revision

What's a Parser ?

A parser is a function used to fill components using json files. Parsers are used by systems to help configure themselves using json files.

All the parsers should take a json file path as parameter. For example:

  • A Position2DParser might open the json file and check for a Position2DComponent field in the file. If the field is found, the parser will check if both x and y field can be found in the component field. If they're found, the parser will return an instance of the Position2DComponent with fulfilled parameters.

How to Write One

If you want to use a component but didn't create it, you can check the R-TypeGame/Parser/ folder and find the parser you want.

If you just wrote a component and want to create a parser for it, that's doable, by creating files using C++

Tutorial

If you haven’t already read the ECS Component section, please take a moment to review it.

In this tutorial, we'll create a Position2DParser. This parser requires a single component: the PositionComponent. The parser will read a json and return an instance of this component.

Let's Implement It

First, create a Position2DParser folder with a Position2DParser.hpp file and a Position2DParser.cpp one.

In a second part, we'll start by editing the header file:

As said previously, a parser is a function that takes a json file path as parameter, and return an instance of the linked component:

#include "Position2DComponent.hpp"

#include <memory>

std::shared_ptr<Position2DComponent> parsePosition2D(std::string pathFile);

Then, we'll focus on the Position2DParser.cpp file:

#include <fstream>
#include <iostream>
#include <exception>
#include <json/json.h>

#include "Position2DComponent.hpp"

std::shared_ptr<Position2DComponent> parsePosition2D(std::string pathFile)
{
    try {
        ...
    } catch {std::exception e) {
        std::cerr << e.what() << std::endl; 
    }
}

Now, we'll go deep into the implementation of the parser.

Foremost, declare jsoncpp variables and open the file

        // Declare jsoncpp variables
        std::ifstream file(pathFile);
        Json::Reader reader;
        Json::Value root;

        if (!file.is_open()) // If the file is already open, we can't open it a second time
            return nullptr;

        if (!reader.parse(file, root, false)) // Check the format of the json file
            return nullptr;

Then, catch the Position2DComponent field in the json:

        const Json::Value& position2D = root["Position2DComponent"];

        if (!position2D) // Checks if the field is found or not
            return nullptr;

Then catch the fields inside the Position2DComponent one, and if they exist, return the needed instance:

        const Json::Value& x = position2D["x"];
        const Json::Value& y = position2D["y"];

        if (x && y) // Checks if the fields are found or not
            return std::make_shared<Position2DComponent>(x.asInt(), y.asInt());
        return nullptr;

Here you are !

You now have your own parser, but...

How to use it ?

A parser is used in initialization systems where entities need to be configured from json files.

Here's an example of the system that initialize a shoot entity:

void ShootInitSystem::_initShoot(ECS::Registry& reg, int idxPacketEntities)
{
    std::shared_ptr<TextureRectComponent> textureRect = parseTextureRect(PATH_JSON);
    if (textureRect) {
        reg.register_component<IComponent>(textureRect->getType());
        reg.set_component<IComponent>(idxPacketEntities, textureRect, textureRect->getType());
    }

    std::shared_ptr<ScaleComponent> scale = parseScale(PATH_JSON);
    if (scale) {
        reg.register_component<IComponent>(scale->getType());
        reg.set_component<IComponent>(idxPacketEntities, scale, scale->getType());
    }

    std::shared_ptr<Position2DComponent> position2D = parsePosition2D(PATH_JSON);
    if (position2D) {
        reg.register_component<IComponent>(position2D->getType());
        reg.set_component<IComponent>(idxPacketEntities, position2D, position2D->getType());
    }

    std::shared_ptr<SpeedComponent> speed = parseSpeed(PATH_JSON);
    if (speed) {
        reg.register_component<IComponent>(speed->getType());
        reg.set_component<IComponent>(idxPacketEntities, speed, speed->getType());
    }

    std::shared_ptr<VelocityComponent> velocity = parseVelocity(PATH_JSON);
    if (velocity) {
        reg.register_component<IComponent>(velocity->getType());
        reg.set_component<IComponent>(idxPacketEntities, velocity, velocity->getType());
    }

    std::shared_ptr<ShootComponent> shoot = parseShoot(PATH_JSON);
    if (shoot) {
        reg.register_component<IComponent>(shoot->getType());
        reg.set_component<IComponent>(idxPacketEntities, shoot, shoot->getType());
    }
}

Here you can see, the all shoot entity is configured using the same json file using parsers. For the same example, here's the json of the shoot entity:

{
    "TextureRectComponent":
    {
        "path":"./Assets/shots.gif",
        "left": 250,
        "top": 90,
        "width": 17,
        "height": 6
    },
    "ScaleComponent": 5,
    "Position2DComponent":
    {
        "x": 100,
        "y": 100
    },
    "SpeedComponent":
    {
        "x":5,
        "y":5
    },
    "VelocityComponent":
    {
        "vx": 600,
        "vy": 0
    },
    "ShootComponent":
    {
        "damage": 1,
        "friendlyFire": true,
        "type": "Player"
    }
}