Blog (35)
Komentarze (574)
Recenzje (0)

Notatki programisty: deska kreślarska jako podstawa projektowania, czyli piszemy edytor mapy siatkowej

@biomenNotatki programisty: deska kreślarska jako podstawa projektowania, czyli piszemy edytor mapy siatkowej07.02.2017 11:11

W poprzednich wpisach omówiłem pokrótce zasadę działania rdzenia, gier platformowych 2D, wykorzystującego zaawansowaną fizykę. Jak mogliśmy zauważyć w poprzednich przykładach, proces tworzenia obiektów był dość "kodochłonny". W niniejszym tekście zaprezentuje pierwszy krok, który w przyszłości zapewni nam dużo łatwiejsze projektowanie poziomów naszego dema.

Grunt to model

Zacznijmy od konceptu, mianowicie nasza mapa siatkowa będzie prostą dynamiczną tablicą dwuwymiarową zasymulowaną w kodzie wektorami. Każdy element naszej tablicy będzie przechowywać obiekt struktury w którym zapiszemy identyfikator obiektu – coś na kształt imienia, będzie służyć do identyfikacji czy pod tą komórką tablicy znajduje się obiekt ściany czy gracza, wysokość, szerokość i położenie – dane te przydarzą się nam w przypadku tworzenia bardziej „spersonalizowanego” świata Box2D. Dzięki temu zabiegowi wszelkie informację na temat wyglądu i położenia ciała wyrzucimy z kodu, do pliku mapy. Całość o wiele lepiej zobrazuje nam deklaracja klasy.


struct GameMap
{
public:
    struct Item
    {
        std::string ID;
        int width;
        int height;
        int x;
        int y;

        void log();
    };

    const int width;
    const int height;

    std::vector<std::vector<Item>> array;

    GameMap(
        const int widthValue,
        const int heightValue,
        bool ini = true);

    void log();

private:
    void initArray();
};

GameMap::GameMap(
        const int widthValue,
        const int heightValue,
        bool init)
            : width(widthValue),
              height(heightValue)
{
    if(init){
        this->initArray();
    }
}

void GameMap::initArray()
{
    for(int i = 0; i < height; ++i)
    {
        std::vector<Item> newVec;
        for(int j = 0; j < width; ++j)
        {
            Item newItem;
            newItem.ID = GameObjRegister::get()->list.at(GameObjRegister::Index::Empty);
            newItem.x = (EditorGlobals::MESH_ITEM_SIZE);
            newItem.y = (EditorGlobals::MESH_ITEM_SIZE);

            newItem.x = (j*(EditorGlobals::MESH_ITEM_SIZE/2) * 2);
            newItem.y = (i*(EditorGlobals::MESH_ITEM_SIZE/2) * 2);

            newItem.height = 0;
            newItem.width = 0;

            newVec.push_back(newItem);
        }

        array.push_back(newVec);
    }
}

void GameMap::log()
{
    std::cout << "Width: " << width << " Height: " << height << "\n";

    for(std::vector<GameMap::Item>& vec : array)
    {
        for(GameMap::Item& item : vec){
            item.log();
        }
        std::cout << "\n";
    }
}

void GameMap::Item::log()
{
    std::cout << " ID: " << ID << "\n";
    std::cout << " x: " << x << "\n";
    std::cout << " y: " << y << "\n";
}

Słowem wyjaśnienia dlaczego zmienna init posiada parametr domyślny true i po co ona tutaj jest. Mianowicie, po tym jak zadeklarujemy rozmiar (wysokość, szerokość) naszej mapy będziemy musieli utworzyć stan początkowy mapy aby na starcie zawierała „obojętne” obiekty oraz za-alokować dla naszego wektora odpowiednią ilość pamięci. Wyjątkiem od tej reguły jest sytuacja w której będziemy ładować mapę z pliku ale o tym później.

Jak być może pamiętamy z poprzednich wpisów, w przykładach posługiwaliśmy się prostym systemem identyfikacji typów ciał, posiadaliśmy specjalną klasę do przechowywania informacji o tym jakiego typu jest ciało silnika Box2D. W naszym nowym edytorze wykorzystamy podobny mechanizm do nadawania identyfikatora obiektom mapy. Oto jak będzie wyglądać klasa rejestru.


struct GameObjRegister
{
public:
    enum Index
    {
        Empty,
        Player,

        MapFlor,
        MapWall,

        Enemy
    };

    GameObjRegister();
    static GameObjRegister* get();
    std::map<Index, std::string> list;

private:
    static std::shared_ptr<GameObjRegister> ptr;

};

std::shared_ptr<GameObjRegister> GameObjRegister::ptr = nullptr;

GameObjRegister::GameObjRegister()
{
    list[Index::Empty]  = std::string("EMPTY");
    list[Index::Player] = std::string("HERO");
    list[Index::Enemy]  = std::string("STR_ENEMY");
    list[Index::MapFlor] = std::string("MAP_FLOR");
    list[Index::MapWall] = std::string("MAP_WALL");
}

GameObjRegister* GameObjRegister::get()
{
    if(ptr == nullptr){
        ptr = std::make_shared<GameObjRegister>(GameObjRegister());
    }
    return ptr.get();
}

W przyszłości będziemy chcieli zapisać nasza mapę z pamięci do pliku, stąd stwierdziłem że przejrzyściej będzie operować na słowach niż bezpośrednio na enumie który mógłby być nieopatrznie w przyszłości zedytowany. Dodatkowo ktoś mógłby zapytać, dlaczego by nie zintegrować używanego systemu identyfikacji z „rdzenia” z edytorem, odpowiedź jest prosta: dla zachowania modułowej budowy naszego projektu.

Namaluj mój świat na żółto i na niebiesko

W tym miejscu mamy gotowy model naszej mapy, czas przystąpić do napisania kodu który będzie odpowiedzialny za rysowanie naszej mapy. Otóż, jednym z założeń jest to że siatka mapy będzie składać się z kwadratów o wymiarach 40x40 pikseli. Dodatkowo, roboczo pozycja naszych obiektów będzie odpowiadała prawemu górnemu rogowi kwadratowi siatki. Oczywiście w przyszłości można zmienić koncept na bardziej precyzyjny: mniejsze komórki i pozycja ustalana na podstawie środka czworokąta aczkolwiek ten temat zostawiam w gestii bardziej dociekliwych i ambitnych czytelników. ;) Definicja klasy naszego renderu wygląda następująco:


class RenderNT
{
public:

    RenderNT(sf::RenderWindow* window);

    void drawMesh();
    void render(GameMap& map);
    void refresh(const sf::View& camera);

private:
    struct RenderSurface
    {
        sf::Vector2f HeightY;
        sf::Vector2f WidthX;

        void update(
                sf::Vector2f viewCenterPosition,
                sf::Vector2u windowSize);
    };
    RenderSurface m_renderSurface;

    struct RenderEngine
    {
        struct InfoForEngine
        {
            GameMap* map;
            sf::RenderWindow* window;
            RenderSurface renderSurface;
        };

        struct Base
        {
            virtual ~Base() {}
            void virtual render(InfoForEngine& data) = 0;
        };

        struct Graphics : public Base
        {
            Graphics();
            ~Graphics();
            void render(InfoForEngine &data);
        };
    };
    RenderEngine::Base* m_renderEngine;

    sf::Vector2i m_meshSize;
    sf::RenderWindow* m_window;
};

Jak widzimy, klasa rysująca używa dwóch tajemniczych klas RenderSurface i RenderEngine. Zaczynając ich omówienie od tej pierwszej: wyobraźmy sobie że nasza mapa ma wymiar 100x100 aczkolwiek w oknie aplikacji są widoczne jedynie komórki z indeksami 20-50 w poziomie i 40-90 w pionie (trochę dziwna proporcja wiem) i teraz pojawia się problem, czy jest sens marnować moc obliczeniową na rysowanie obiektów które znajdują się poza naszym wzrokiem? Otóż odpowiedź brzmi: nie. Klasa RenderSurface przechowuje informację na temat indeksów tablicy które powinny być rysowane w oknie, dzięki temu nasza aplikacja przy mapie o wymiarach np. 512x512 nie będzie pokazem slajdów. Wartości indeksów obliczamy na podstawie obecnego położenia kamery i wielkości okna aplikacji, szczegóły w implementacji. Druga z tajemniczych klas to RenderEngine, otóż rozpatrzmy sytuację w której to chcemy zmienić tryb renderowania naszej mapy z surowych kształtów na teksturowane. W takim wypadku warto zaopatrzyć się w możliwość łatwego przełączania między stylami renderowania. Właśnie do tego będzie nam potrzebna klasa silnika renderującego. Teraz kolejno, implementacja RenderNT:


RenderNT::RenderNT(
        sf::RenderWindow* window) :
            m_window(window),
            m_renderEngine(new RenderEngine::Graphics()) {}

void RenderNT::refresh(const sf::View& camera)
{
    m_renderSurface.update(
                camera.getCenter(),
                m_window->getSize());
}

void RenderNT::drawMesh()
{
    int width = m_meshSize.x;
    int height =  m_meshSize.y;

    const int pion = height * EditorGlobals::MESH_ITEM_SIZE;
    const int poziom = width * EditorGlobals::MESH_ITEM_SIZE;

    /* Linie pionowe */
    for (int i = 0; i < width + 1; i++)
    {
        sf::Vertex vLine[] =
        {
            sf::Vertex(sf::Vector2f(
                (float)(i * EditorGlobals::MESH_ITEM_SIZE), 0.f)),

            sf::Vertex(sf::Vector2f(
                (float)(i * EditorGlobals::MESH_ITEM_SIZE), (float)pion))
        };

        m_window->draw(vLine, 2, sf::Lines);
    }

    /* Linie poziome */
    for (int i = 0; i < height + 1; i++)
    {
        sf::Vertex vLine[] =
        {
            sf::Vertex(sf::Vector2f(
                0.f, (float)(i * EditorGlobals::MESH_ITEM_SIZE))),

            sf::Vertex(sf::Vector2f(
                (float)poziom, (float)(i * EditorGlobals::MESH_ITEM_SIZE)))
        };

        m_window->draw(vLine, 2, sf::Lines);
    }
}

void RenderNT::render(GameMap& map)
{
    m_meshSize.x = map.width;
    m_meshSize.y = map.height;

    RenderEngine::InfoForEngine  info;
    info.map = &map;
    info.window = m_window;
    info.renderSurface = m_renderSurface;

    m_renderEngine->render(info);
}

// RenderSurface:

/* Funkcja okresla ktore indeksy beda rysowane. */
void RenderNT::RenderSurface::update(
        sf::Vector2f viewCenterPosition,
        sf::Vector2u windowSize)
{
    float haldWindowWidth = windowSize.x / 2;
    float halfWindowHeight = windowSize.y / 2;

    this->HeightY
        = sf::Vector2f(
            viewCenterPosition.y - halfWindowHeight,
            viewCenterPosition.y + halfWindowHeight);

    this->WidthX
        = sf::Vector2f(
            viewCenterPosition.x - haldWindowWidth,
            viewCenterPosition.x + haldWindowWidth);

    WidthX.x = WidthX.x / EditorGlobals::MESH_ITEM_SIZE;
    WidthX.y = WidthX.y / EditorGlobals::MESH_ITEM_SIZE;

    HeightY.x = HeightY.x / EditorGlobals::MESH_ITEM_SIZE;
    HeightY.y = HeightY.y / EditorGlobals::MESH_ITEM_SIZE;
}

// RenderSurface:

RenderNT::RenderEngine::Graphics::Graphics() {}

RenderNT::RenderEngine::Graphics::~Graphics() {}

void RenderNT::RenderEngine::Graphics::render(InfoForEngine &data)
{
    /* Okreslenie jaki jest przedzial renderowanych
     * obiektow na podstawie danych RenderSurface,
     * w przyszlosci mozna wyrzucic te operacje do
     * nowej metody. */
    int startI = 0;
    int finishI = data.map->height;

    int startJ = 0;
    int finishJ = data.map->width;

    bool widthCondition = (
                data.renderSurface.WidthX.x >= 0 &&
                data.renderSurface.WidthX.y <= data.map->width);

    bool heightCondition = (
                data.renderSurface.HeightY.x >= 0 &&
                data.renderSurface.HeightY.y <= data.map->height);

    if(widthCondition && heightCondition)
    {
        startI = data.renderSurface.HeightY.x;
        finishI = data.renderSurface.HeightY.y;

        startJ = data.renderSurface.WidthX.x;
        finishJ = data.renderSurface.WidthX.y;
    }

    for(int i = startI; i < finishI; ++i)
    {
        for(int j = startJ; j < finishJ; ++j)
        {
            /* Pobieranie informacji z komorki
             * tablicy i tworzenie surowych obiektow
             * renderujacych. */

            GameMap::Item* item = &data.map->array [ i ] [ j ] ;
            sf::Vector2f position(item->x, item->y);

            if (item->ID ==
                    GameObjRegister::get()->list.at(
                        GameObjRegister::Index::MapFlor))
            {
                sf::RectangleShape shape(sf::Vector2f(0,0));
                shape.setSize(sf::Vector2f(item->width, item->height));
                shape.setPosition(position);
                shape.setFillColor(sf::Color::Red);
                data.window->draw(shape);

            };
            if (item->ID ==
                    GameObjRegister::get()->list.at(
                        GameObjRegister::Index::MapWall))
            {
                sf::RectangleShape shape(sf::Vector2f(0,0));
                shape.setSize(sf::Vector2f(item->width, item->height));
                shape.setPosition(position);
                shape.setFillColor(sf::Color::Yellow);
                data.window->draw(shape);

            };
        }
    }
}

Zaczarowany ołówek

Słowem krótkiego podsumowania: na chwile obecną mamy model naszej mapy dwuwymiarowej oraz prostą klasę do jej rysowania w oknie Box2D. Lecz jak nazwa wskazuje, edytor służy do edycji więc przyszedł czas na opracowanie mechanizmu edycji komórek naszej tabeli. Jak wspomniałem w jednym ze wcześniejszych akapitów, do identyfikacji obiektów siatki będziemy używać prostej klasy rejestru i na chwilę obecną tylko tyle nas interesuje. Zmiana pola ID w strukturze Item. Jednak aby wykonywać tą operację potrzebujemy mechanizmu który odczyta nam pozycję myszy w oknie aplikacji i określi indeks komórki na którą kliknęliśmy. Co więcej musimy wziąć pod uwagę przesuniecie kamery w chwili obliczania odpowiedniego indeksu jak i wprowadzić jakiekolwiek zabezpieczenie przed wyjściem poza mapę. Jako że kod wytłumaczy dużo więcej niż sam suchy tekst, przejdźmy do deklaracji i implementacji klasy odpowiedzialnej za cały wyżej wymieniony mechanizm:


class MouseManager
{
public:

    struct Info
    {
        int x;
        int y;
        bool isOk;
    };

    MouseManager(sf::RenderWindow* window);

    Info getIndex(GameMap* map, sf::View* view);

private:
    sf::RenderWindow* m_window;
    int m_startPositionX;
    int m_startPositionY;
};

MouseManager::MouseManager(sf::RenderWindow* window) : m_window(window)
{
    m_startPositionX = m_window->getDefaultView().getCenter().x;
    m_startPositionY = m_window->getDefaultView().getCenter().y;
}

MouseManager::Info MouseManager::getIndex(GameMap* map, sf::View* view)
{
    int iMousePositionX =
            (int)sf::Mouse::getPosition(*m_window).x +
            (view->getCenter().x - m_startPositionX);

    int iMousePositionY =
            (int)sf::Mouse::getPosition(*m_window).y +
            (view->getCenter().y - m_startPositionY);

    int iSecondParam = (int)iMousePositionX / EditorGlobals::MESH_ITEM_SIZE;
    int iFirstParam = (int)iMousePositionY / EditorGlobals::MESH_ITEM_SIZE;

    bool heightCondition = iFirstParam >= 0 && iFirstParam < map->height;
    bool widthCondition = iSecondParam >= 0 && iSecondParam < map->width;
    bool mainChangeCondition = heightCondition && widthCondition;

    Info toReturn;
    toReturn.x = iFirstParam;
    toReturn.y = iSecondParam;
    toReturn.isOk = mainChangeCondition;

    return toReturn;
}

Jak możemy zauważyć wbrew pozorom nie jest to nic specjalnie trudnego. Ot klasa na podstawie prostych obliczeń łopatologicznych wylicza indeks biorą pod uwagę punkt odniesienia (punkt startowy kamery), wielkość okna, pozycje myszy oraz wielkość siatki.

Przez cały kod przewijały się tajemnicze odniesienia do klasy EditorGlobals, oto i ona:


struct EditorGlobals
{
    static const int MESH_ITEM_SIZE = 40;
};

Więcej zależności czyli Qt w akcji

Wszystkie prezentowane przykłady z serii, opracowywałem w środowisku QtCreatora przez co wprowadzenie dla mnie dodatkowej zależności w postaci biblioteki z frameworka Qt nie stanowi większego problemu. Oczywiście dla kogoś kto używał Visual Studio, sprawa delikatnie się komplikuje. Dlaczego o tym wspominam? Otóż model naszej mapy będziemy zapisywać do pliku w formacie json a do tego celu wykorzystamy biblioteki Qt. Sama idea i koncept dekoratora który posłuży nam do zapisu i odczytu jest dość uniwersalny stąd jeżeli drogi czytelniku czujesz się na siłach i chcesz użyć innej biblioteki/formatu – wszystko w twoich rękach. ;) Ze względu na lekkość i licencję ten projekt wydaje się atrakcyjniejszy, aczkolwiek na razie rozważania te odłóżmy na bok. Jak będzie wyglądać nasz moduł odpowiedzialny za zapis do pliku, zbrodniczo prosto:


namespace Json
{
    class Fields
    {
    public:
        const QString ID;
        const QString WIDTH;
        const QString HEIGHT;
        const QString CONTENT;
        const QString ROW;
        const QString X;
        const QString Y;

        Fields();

        static Fields* get();

    private:

        ~Fields();

        static Fields* m_ptr;
    };

    class Generator
    {
    public:
        Generator(GameMap* base = nullptr);

        QJsonDocument getJsonDoc();
        GameMap* getGameMapFromJson(QJsonDocument& source);

    private:
        GameMap* m_ptr;
    };

    struct File
    {
    public:
        File();

        bool saveJsonMap(
                QJsonDocument& document,
                QString filePath) const;

        QJsonDocument readJsonMap(QString filePath) const;
    };
}

Ot mamy klasę do przechowywania łańcuchów znaków których używamy wewnątrz pliku, dodatkowo klasa która nam generuje na podstawie modelu obiekt dokumentu jsona i w drugą stronę oraz klasę która ten dokument zapisuje/odczytuje z pliku. Implementacja raczej też straszna nie jest.


//Json::Fields

Json::Fields* Json::Fields::m_ptr = nullptr;

Json::Fields::Fields() :
    ID("ID"),
    WIDTH("width"),
    HEIGHT("height"),
    CONTENT("content"),
    ROW("row"),
    X("x"),
    Y("y")
{

}

Json::Fields* Json::Fields::get()
{
    if(m_ptr == nullptr){
        m_ptr = new Fields();
    }
    return m_ptr;
}

Json::Fields::~Fields(){
    if(m_ptr != nullptr){
        delete m_ptr;
    }
}

// Json::Generator

Json::Generator::Generator(GameMap* base) : m_ptr(base) {}

QJsonDocument Json::Generator::getJsonDoc()
{
    QJsonObject main;
    main[Fields::get()->WIDTH] = m_ptr->width;
    main[Fields::get()->HEIGHT] = m_ptr->height;

    int line = 0;

    QJsonArray array;
    for(std::vector<GameMap::Item>& vec : m_ptr->array)
    {
        QJsonArray lineArray;
        for(GameMap::Item& item : vec)
        {
            QJsonObject newJson;
            newJson[Fields::get()->ID] = QString::fromStdString(item.ID);
            newJson[Fields::get()->X] = QString::number(item.x);
            newJson[Fields::get()->Y] = QString::number(item.y);
            newJson[Fields::get()->WIDTH] = QString::number(item.width);
            newJson[Fields::get()->HEIGHT] = QString::number(item.height);

            lineArray.append(newJson);
        }

        QJsonObject additionalInfo;
        additionalInfo[Fields::get()->ROW] = line;

        lineArray.append(additionalInfo);

        ++line;
        array.append(lineArray);
    }

    main[Fields::get()->CONTENT] = array;

    return QJsonDocument(main);
}

GameMap* Json::Generator::getGameMapFromJson(QJsonDocument &source)
{

    QJsonObject jsonObj = source.object();
    const int height = jsonObj[Json::Fields::get()->HEIGHT].toInt();
    const int width =  jsonObj[Json::Fields::get()->WIDTH].toInt();

    GameMap* newMap = new GameMap(width, height, false);

    QJsonArray content = jsonObj[Json::Fields::get()->CONTENT].toArray();
    for(int i = 0; i < content.size(); ++i)
    {
        std::vector<GameMap::Item> newLine;
        QJsonArray singleRow = content.at(i).toArray();

        for(int j = 0; j < singleRow.size()-1; ++j)
        {
            QJsonObject item = singleRow.at(j).toObject();

            GameMap::Item newItem;
            newItem.ID = item[Json::Fields::get()->ID].toString().toStdString();
            newItem.x = item[Json::Fields::get()->X].toString().toInt();
            newItem.y = item[Json::Fields::get()->Y].toString().toInt();
            newItem.height = item[Json::Fields::get()->HEIGHT].toString().toInt();
            newItem.width = item[Json::Fields::get()->WIDTH].toString().toInt();

            newLine.push_back(newItem);

        }

        newMap->array.push_back(newLine);
    }

    return newMap;
}

// Json::File

Json::File::File() {}

bool Json::File::saveJsonMap(QJsonDocument& document, QString filePath) const
{
    bool toReturn = false;

    QString filePathToSave = filePath;

    if(!filePathToSave.isEmpty()){

        QFile outputFile(filePathToSave);
        outputFile.open(QIODevice::WriteOnly);
        outputFile.write(document.toJson());
        outputFile.close();

        toReturn = true;
    }

    return toReturn;
}

QJsonDocument Json::File::readJsonMap(QString filePath) const
{
    QJsonDocument jsonDocReaded;

    if(QFile::exists(filePath) && !filePath.isEmpty())
    {
        QString fileContentString;

        QFile jsonFile(filePath);
        if(jsonFile.open(QIODevice::ReadOnly)){
            fileContentString = jsonFile.readAll();
            jsonDocReaded = QJsonDocument::fromJson(fileContentString.toUtf8());
        }
        jsonFile.close();
    }

    return jsonDocReaded;
}

Tym razem było dość sporo zagadnień do omówienia, model, render mapy, obsługa edytującego kursora i moduł obsługi zapisu/odczytu mapy z pliku. Jeżeli dobrnąłeś do tego momentu czas na satysfakcjonujący koniec, czyli funkcja startowa aplikacji. U siebie w kodzie trzymam ją w metodzie statycznej w klasie Editor więc starczy tej magii na dzisiaj.


void Editor::run()
{
    int width = 0;
    std::cout << "Get width (X): ";
    std::cin >> width;

    int height = 0;
    std::cout << "Get height(Y): ";
    std::cin >> height;

    std::shared_ptr<GameMap> map(new GameMap(width,height));

    sf::RenderWindow window(
                sf::VideoMode(800,600, 16),
                std::string("hello"),
                sf::Style::Close);

    sf::View camera;
    camera = window.getDefaultView();

    sf::Vector2f startCameraPosition = camera.getCenter();

    const int viewMoveValue = 5;

    RenderNT render(&window);
    render.refresh(camera);

    MouseManager mouseIndex(&window);

    std::string selectedString = GameObjRegister::get()->list.at(GameObjRegister::Index::Empty);

    while(window.isOpen())
    {
        sf::Event myEvent;
        while(window.pollEvent(myEvent))
        {
            if(myEvent.type == sf::Event::Closed){
                window.close();
            }

            if(sf::Keyboard::isKeyPressed(sf::Keyboard::Q))
            {
                Json::Generator dec(map.get());
                QJsonDocument jsonDoc = dec.getJsonDoc();

                const std::string fileName = std::string("hello.json");

                Json::File fileDec;
                fileDec.saveJsonMap(jsonDoc, QString::fromStdString(fileName));

                std::cout << std::string("Saved as: ") + fileName;
            }

            if(sf::Keyboard::isKeyPressed(sf::Keyboard::W))
            {
                const std::string fileName = std::string("hello.json");

                Json::File fileDec;
                QJsonDocument jsonDoc = fileDec.readJsonMap(QString::fromStdString(fileName));

                Json::Generator dec;
                map.reset(dec.getGameMapFromJson(jsonDoc));

                std::cout << std::string("Readed from: ") + fileName;

            }

            if(sf::Keyboard::isKeyPressed(sf::Keyboard::Tab))
            {
                window.setActive(false);
                int index = 0;
                for(auto& pair :GameObjRegister::get()->list )
                {
                    std::cout
                        << "No. "
                        << pair.first
                        << " Content: "
                        << pair.second
                        << "\n";
                }
                std::cin >> index;
                selectedString =
                        GameObjRegister::get()->list.at(
                            (GameObjRegister::Index)index);

                window.setActive(true);
            }

            if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left))
            {
                if(camera.getCenter().x >= startCameraPosition.x+10)
                {
                    render.refresh(camera);
                    camera.move(-viewMoveValue, 0);
                    window.setView(camera);
                }
            }

            if (sf::Keyboard::isKeyPressed(sf::Keyboard::Right))
            {
                if(camera.getCenter().x + (window.getSize().x/2) <= (map->width*40)-10)
                {
                    render.refresh(camera);
                    camera.move(viewMoveValue, 0);
                    window.setView(camera);
                }
            }

            if (sf::Keyboard::isKeyPressed(sf::Keyboard::Up))
            {
                if(camera.getCenter().y >= startCameraPosition.y+10)
                {
                    render.refresh(camera);
                    camera.move(0, -viewMoveValue);
                    window.setView(camera);
                }
            }

            if (sf::Keyboard::isKeyPressed(sf::Keyboard::Down))
            {
                if(camera.getCenter().y + (window.getSize().y/2) <= (map->height*40)-10)
                {
                    render.refresh(camera);
                    camera.move(0, viewMoveValue);
                    window.setView(camera);
                }
            }

            if (sf::Mouse::isButtonPressed(sf::Mouse::Left))
            {
                MouseManager::Info infoMouse = mouseIndex.getIndex(map.get(), &camera);

                if (infoMouse.isOk)
                {
                    GameMap::Item* itemPtr = &map->array[infoMouse.x][infoMouse.y];
                    itemPtr->ID = selectedString;
                    itemPtr->height = 40;
                    itemPtr->width = 40;
                }
            }

            if (sf::Mouse::isButtonPressed(sf::Mouse::Right))
            {
                MouseManager::Info infoMouse = mouseIndex.getIndex(map.get(), &camera);

                if (infoMouse.isOk)
                {
                    GameMap::Item* itemPtr = &map->array[infoMouse.x][infoMouse.y];
                    itemPtr->ID = GameObjRegister::get()->list.at(GameObjRegister::Index::Empty);

                }
            }
        }

        window.clear(sf::Color::Black);

        render.drawMesh();
        render.render(*map);

        window.display();
    }
}

Jak widzimy wszystkie omówione elementy zostały wykorzystane w przykładzie. Dodatkowo w przypadku poruszania się wprowadzone zostały warunki aby nie było możliwości wyjechania poza siatkę. Kosmetyka ale przydatna. ;) Pod klawiszem Tab mamy do dyspozycji proste konsolowe menu do wybierania identyfikatorów obiektów z rejestru. Całość prezentuje się mało widowiskowo aczkolwiek od teraz wiemy skąd memy w stylu: co widzi klient, co widzi programista.

Jak zawsze, dzięki za uwagę!

Szanowna Użytkowniczko! Szanowny Użytkowniku!
×
Aby dalej móc dostarczać coraz lepsze materiały redakcyjne i udostępniać coraz lepsze usługi, potrzebujemy zgody na dopasowanie treści marketingowych do Twojego zachowania. Twoje dane są u nas bezpieczne, a zgodę możesz wycofać w każdej chwili na podstronie polityka prywatności.

Kliknij "PRZECHODZĘ DO SERWISU" lub na symbol "X" w górnym rogu tej planszy, jeżeli zgadzasz się na przetwarzanie przez Wirtualną Polskę i naszych Zaufanych Partnerów Twoich danych osobowych, zbieranych w ramach korzystania przez Ciebie z usług, portali i serwisów internetowych Wirtualnej Polski (w tym danych zapisywanych w plikach cookies) w celach marketingowych realizowanych na zlecenie naszych Zaufanych Partnerów. Jeśli nie zgadzasz się na przetwarzanie Twoich danych osobowych skorzystaj z ustawień w polityce prywatności. Zgoda jest dobrowolna i możesz ją w dowolnym momencie wycofać zmieniając ustawienia w polityce prywatności (w której znajdziesz odpowiedzi na wszystkie pytania związane z przetwarzaniem Twoich danych osobowych).

Od 25 maja 2018 roku obowiązuje Rozporządzenie Parlamentu Europejskiego i Rady (UE) 2016/679 (określane jako "RODO"). W związku z tym chcielibyśmy poinformować o przetwarzaniu Twoich danych oraz zasadach, na jakich odbywa się to po dniu 25 maja 2018 roku.

Kto będzie administratorem Twoich danych?

Administratorami Twoich danych będzie Wirtualna Polska Media Spółka Akcyjna z siedzibą w Warszawie, oraz pozostałe spółki z grupy Wirtualna Polska, jak również nasi Zaufani Partnerzy, z którymi stale współpracujemy. Szczegółowe informacje dotyczące administratorów znajdują się w polityce prywatności.

O jakich danych mówimy?

Chodzi o dane osobowe, które są zbierane w ramach korzystania przez Ciebie z naszych usług, portali i serwisów internetowych udostępnianych przez Wirtualną Polskę, w tym zapisywanych w plikach cookies, które są instalowane na naszych stronach przez Wirtualną Polskę oraz naszych Zaufanych Partnerów.

Dlaczego chcemy przetwarzać Twoje dane?

Przetwarzamy je dostarczać coraz lepsze materiały redakcyjne, dopasować ich tematykę do Twoich zainteresowań, tworzyć portale i serwisy internetowe, z których będziesz korzystać z przyjemnością, zapewniać większe bezpieczeństwo usług, udoskonalać nasze usługi i maksymalnie dopasować je do Twoich zainteresowań, pokazywać reklamy dopasowane do Twoich potrzeb. Szczegółowe informacje dotyczące celów przetwarzania Twoich danych znajdują się w polityce prywatności.

Komu możemy przekazać dane?

Twoje dane możemy przekazywać podmiotom przetwarzającym je na nasze zlecenie oraz podmiotom uprawnionym do uzyskania danych na podstawie obowiązującego prawa – oczywiście tylko, gdy wystąpią z żądaniem w oparciu o stosowną podstawę prawną.

Jakie masz prawa w stosunku do Twoich danych?

Masz prawo żądania dostępu, sprostowania, usunięcia lub ograniczenia przetwarzania danych. Możesz wycofać zgodę na przetwarzanie, zgłosić sprzeciw oraz skorzystać z innych praw wymienionych szczegółowo w polityce prywatności.

Jakie są podstawy prawne przetwarzania Twoich danych?

Podstawą prawną przetwarzania Twoich danych w celu świadczenia usług jest niezbędność do wykonania umów o ich świadczenie (tymi umowami są zazwyczaj regulaminy). Podstawą prawną przetwarzania danych w celu pomiarów statystycznych i marketingu własnego administratorów jest tzw. uzasadniony interes administratora. Przetwarzanie Twoich danych w celach marketingowych realizowanych przez Wirtualną Polskę na zlecenie Zaufanych Partnerów i bezpośrednio przez Zaufanych Partnerów będzie odbywać się na podstawie Twojej dobrowolnej zgody.