Перейти к содержанию

ООП (ДЗ часть 1)#

Самый простой способ задать карту — объявить массив, например, такой:

const char MAP_DATA[] =
    "#############"
    "#   @     . #"
    "#      .    #"
    "#############";

Для простой аркадной игры это нормально, но когда в игре должно быть несколько уровней, разные противники, разные бонусы — требуется решение получше. Можно написать свой редактор карт, но это большой объём работ. Проще использовать универсальный редактор карт Tiled, и научиться интерпретировать созданные им карты в игровом движке.

Tiled Map Editor — кроссплатформенный открытый редактор тайловых карт для игр. Позволяет создавать карты для 2-мерных игр (с видом сбоку, таких как платформеры, или видом сверху, к примеру JRPG)

Созданные файлы имеют расширение *.tmx и внутри содержат XML

Рекомендуется рабоатть с Tiled Map Editor версии 1.0. Для скачивания установщика (систему выбираете самостоятельно) можете воспользоваться следующими ссылками:

  1. Скачайте Tiled Map Editor для Windows
  2. Скачайте Tiled Map Editor для Linux
  3. Скачайте Tiled Map Editor для MacOS

Настройка проекта#

Создайте проект VS Code. Настройте загрузку библиотеки SFML (Гайд по настройке был в практике 5).

Скачайте (данные)[https://disk.yandex.ru/d/wzD9Xd5yB3-DJw] и разархивируйте в файл проекта

https://ratcatcher.ru/media/inf/pr/pr8/Tiled_2.png

  • В папке include файлы SFML
  • В папке res находится уровень (platformer.tmx), шрифт(arial.ttf) и тайлы (tileset.png)

Подключите файлы tinyxml2.cpp, tinyxml2.h, GameScene.cpp, TmxLevel.cpp,

ПКМ По Project -> добавить -> существующий элемент

https://ratcatcher.ru/media/inf/pr/pr8/Tiled_3.png

TinyXML2 — это бесплатная лёгкая библиотека для анализа XML-файлов. Она используется для сериализации данных на разных платформах.

TinyXML2 определяет несколько классов, которые обеспечивают различные слои необходимой функциональности. Вот некоторые из них:

XMLDocument — контейнер для коллекции XMLElements. Это основной интерфейс для сохранения и загрузки XML-файлов.
XMLElement — контейнер для одного XML-элемента с атрибутами.
XMLNode — базовый класс для XMLElement и XMLDocument. Он обеспечивает интерфейс для обхода элементов в документе

Создание карты#

Чтобы пропустить этап настройки проекта вы можете загрузить готовый уровень (файл -> открыть):

https://ratcatcher.ru/media/inf/pr/pr8/Tiled.png

Затем вы увидите загруженный уровень:

https://ratcatcher.ru/media/inf/pr/pr8/Tiled_4.png

Редактор состоит из нескольких зон

  • В зоне наборы тайлов вы можете выбирать тайлы для размещения. Щелкнув по тайлу вы сможете выбрать картинку и разместить на уровне

https://ratcatcher.ru/media/inf/pr/pr8/Tiled_5.png

  • Рабочая зона для изменения карты. Здесь располагается карта заданного размера. Пустые ячейки серого цвета.

https://ratcatcher.ru/media/inf/pr/pr8/Tiled_6.png

  • Cлои тайлов. Тайлы объединяются в слои, и каждый новый слой рисуется поверх другого (например, полупрозрачный и не везде заполненный слой травы рисуется поверх слоя земли). Слои редактируются вверху правой панели.

https://ratcatcher.ru/media/inf/pr/pr8/Tiled_7.png

  • Слои объектов. Если файлы являются только фоном, то объекты активно участвуют в игровой логике. Объект — это квадратная, круглая или многоугольная область. Чтобы создавать объекты, сперва создайте слой объектов на панели слоёв. После станет доступна панель инструментов объектов

https://ratcatcher.ru/media/inf/pr/pr8/tiled-objects-toolbar.png

У области есть имя и набор произвольных свойств, что позволяет использовать области как маркеры мест, в которых потом появится игрок, противники или бонусы. Также можно превратить область в физическое препятствие, или считать областью случайного появления противников, или расположить там портал, при достижении которого происходит переход на новый уровень. Обработкой объектов занимается движок игры.

Поля “Имя” и “Тип” хранят строки, в которых удобно задавать категорию объекта (например, тип юнита игры или бонуса, который он представляет). Впоследствии игра считает эти строковые свойства и сможет выбрать, какой объект надо создать в указанном месте.

Разбор карты в программе#

Для разбора карты в формате XML мы воспользуемся библиотекой Tinyxml2, файлы исходного кода tinyxml2.cpp и tinyxml2.h мы можем просто скопировать в проект.

Для удобства можно использовать готовый компонент, способный загружать XML в память, проводить базовую валидацию карты и предоставлять доступ к объектам и тайлам. Компонент представляет собой файлы TmxLevel.h и TmxLevel.cpp, содержащий структуры TmxObject и TmxLayer, а также класс TmxLevel, выполняющий загрузки и хранение слоёв и объектов.

Продемонстрируем объявление TmxObject:

// В картах TMX объект - это область на карте, имеющая имя, тип,
//  границы, набор пользовательских свойств (в формате ключ-значение)
//  и текстурные координаты.
// Текстурные координаты позволяют связать с объектом спрайт,
//  использующий основную текстуру карты как источник данных.
struct TmxObject
{
    int id;
    int GetPropertyInt(const std::string &propertyName);
    float GetPropertyFloat(const std::string &propertyName);
    std::string GetPropertyString(const std::string &propertyName);

    void MoveBy(const sf::Vector2f &movement);
    void MoveTo(const sf::Vector2f &position);

    std::string name;
    std::string type;
    sf::FloatRect rect;
    std::map<std::string, std::string> properties;

    sf::Sprite sprite;
};

Данная структура содержит поле Sprite, которое содержит графическое представление объекта (текстурой спрайта служит единая текстура TMX-карты). Спрайт можно использовать для рисования объекта:

pLogic->level.Draw(target);
for (const TmxObject &coin : pLogic->coins)
{
    target.draw(coin.sprite);
}
for (const TmxObject &enemy : pLogic->enemies)
{
    target.draw(enemy.sprite);
}
target.draw(pLogic->player.sprite);

Методы MoveTo и MoveBy можно использовать для перемещения к заданной точке / на заданное расстояние соответственно:

TmxObject &player = pLogic->player;
const Vector2f movement = Round(GetPlayerDirection() * PLAYER_SPEED);
player.MoveBy(movement)

Слой представлен следующей структурой:

// В картах TMX слой - это набор тайлов (спрайтов),
//  из которых складывается ландшафт карты.
// Слоёв может быть несколько, что позволяет нарисовать,
//  например, слой травы поверх слоя земли.
struct TmxLayer
{
    sf::Uint8 opacity = 0;
    std::vector<sf::Sprite> tiles;
};

Рисовать слой отдельно не требуется, поскольку это статичный элемент карты, и с рисованием слоёв вполне справляется класс TmxLevel:

class TmxLevel
{
  public:
    // Загружает данные из TMX в память объекта.
    bool LoadFromFile(const std::string &filepath);

    TmxObject GetFirstObject(const std::string &name) const;
    std::vector<TmxObject> GetAllObjects(const std::string &name) const;
    sf::Vector2i GetTileSize() const;
    float GetTilemapWidth() const;
    float GetTilemapHeight() const;
    sf::Vector2f GetTilemapSize() const;

    // Рисует все слои тайлов один за другим,
    //  но не рисует объекты (рисованием которых должна заниматься игра).
    // Принимает любую цель для рисования, например, sf::RenderWindow.
    void Draw(sf::RenderTarget &target) const;

  private:
    int m_width = 0;
    int m_height = 0;
    int m_tileWidth = 0;
    int m_tileHeight = 0;
    int m_firstTileID = 0;
    sf::Texture m_tilesetImage;
    std::vector<TmxObject> m_objects;
    std::vector<TmxLayer> m_layers;
};

Помимо рисования всех статических элементов карты, данный класс даёт доступ к набору объектов на карте, а также к размеру карты, что даёт возможность использовать их остальной игровой логике. Метод для загрузки карты может выбросить исключение, и его лучше обработать в функции main, как показано ниже — это обеспечит выход из программы с предупреждающим сообщением из консоли и ненулевым кодом возврата, сообщающим системе об ошибке во входных данных.

int main(int argc, char *argv[])
{
    (void)argc;
    (void)argv;

    try
    {
        // NOTE: Если при загрузке карты будет выброшено исключение,
        //  то память утечёт. Избавиться от этого можно с помощью
        //  замены new/delete на make_unique и unique_ptr.
        GameView *pGameView = NewGameView({800, 600});
        GameScene *pGameScene = NewGameScene();

        // Аргумент типа `GameLogic*` будет преобразован в `void*`.
        EnterGameLoop(*pGameView, UpdateGameScene, DrawGameScene, pGameScene);
    }
    catch (const std::exception &ex)
    {
        std::cerr << ex.what() << std::endl;
        return 1;
    }

    return 0;
}

Метод TmxLevel::Draw был оптимизирован для ускоренного рисования карт огромного размера: невидимые тайлы отсекаются с помощью проверки пересечения с прямоугольником карты.

void TmxLevel::Draw(sf::RenderTarget &target) const
{
    const sf::FloatRect viewportRect = target.getView().getViewport();

    // Draw all tiles (and don't draw objects)
    for (const auto &layer : m_layers)
    {
        for (const auto &tile : layer.tiles)
        {
            if (viewportRect.intersects(tile.getLocalBounds()))
            {
                target.draw(tile);
            }
        }
    }
}

Ограничения класса загрузчика
- Класс не умеет загружать форматы слоя тайлов, отличные от XML, поэтому не забудьте выставит данный формат для всех карт уровней, которые собираетесь загружать в игре.
- Класс не умеет обрабатывать более одного атласа тайлов (tilesheet)
- Класс не умеет загружать изометрические карты

Указания к работе#

Проект состоит из файлов:

  • main.cpp
  • GameScene.cpp
  • GameScene.h
  • GameView.cpp
  • GameView.h
  • TmxLevel.cpp
  • TmxLevel.h
  • tinyxml2.cpp
  • tinyxml2.h

Жирным отмечены файлы, которые вы можете скачать и подсоединить к проекту

Курсивом отмечены файлы, которые необходимо дописать? используя следующие указания

Обратите внимание, что заголовочный файлы объявляют функции и структуры, но не описывают поведение! Поэтому вам нужно лишь объявить прототипы функций и основные структуры!

Файлы заголовков в программировании — это файл, содержащий объявления функций, классов, переменных и других элементов, которые могут быть использованы в нескольких файлах исходного кода.

1) GameScene.h

Ваша задача – реализовать функции для управления объектом GameScene, который описывает игровую сцену. Код написан в процедурном стиле, и для выделения и освобождения памяти будут использоваться команды new и delete. Следуйте описанию ниже.

Описание структуры
- GameScene – структура, которая включает:
- уровень (level) типа TmxLevel;
- игрока (player) типа TmxObject;
- массивы врагов (enemies) и монет (coins), каждый из которых представлен как std::vector<TmxObject>.

Функции для реализации
- Создание новой игровой сцены
- Реализуйте функцию GameScene *NewGameScene(), которая выделяет память под объект GameScene и возвращает указатель на него. Настройте начальные параметры уровня, игрока, врагов и монет.

Обновление игровой сцены
- В функции void UpdateGameScene(void *pData, GameView &view, float deltaSec):

  • Преобразуйте pData в указатель на GameScene.
  • Обновите положение и состояние игрока, врагов и монет, используя deltaSec (интервал времени между кадрами).
    Отрисовка игровой сцены
  • Реализуйте функцию void DrawGameScene(void *pData, GameView &view), которая:
  • Преобразует pData в указатель на GameScene.
  • Отображает уровень, игрока, врагов и монеты.
  • Удаление игровой сцены
  • Напишите функцию void DestroyGameScene(GameScene *&pScene), которая освобождает память, выделенную под объект GameScene, и устанавливает указатель pScene в null
Tip
    #pragma once
    #include "TmxLevel.h"

    /// Предварительное объявление (pre-declaration) структуры
    ///  позволит передавать и хранить указатели и ссылки на неё,
    ///  но не позволит пользоваться или создавать,
    ///  поскольку мы ещё не знаем ни размер в байтах, ни свойства структуры.
    struct GameView;

    /// Структура, абстрагирующая игровую сцену.
    ///  Код ниже намеренно написан в процедурном стиле:
    ///  - используются структуры
    ///  - используется явный вызов new и delete.
    struct GameScene
    {
        TmxLevel level;
        TmxObject player;
        std::vector<TmxObject> enemies;
        std::vector<TmxObject> coins;
    };

    GameScene *NewGameScene();
    void UpdateGameScene(void *pData, GameView &view, float deltaSec);
    void DrawGameScene(void *pData, GameView &view);
    void DestroyGameScene(GameScene *&pScene);

2) GameScene.h

GameView – структура, содержащая:
- объект окна sf::RenderWindow для вывода графики;
- камеру sf::View для управления видимой областью;
- размер окна sf::Vector2i (ширина и высота);
- таймер sf::Clock для отслеживания времени между кадрами.

  • OnUpdate – тип функции для обновления состояния игры, принимает:
  • указатель void *pData на пользовательские данные,
  • ссылку на объект GameView,
  • deltaSec – время в секундах, прошедшее с предыдущего кадра.

  • OnDraw – тип функции для отрисовки игрового кадра, принимает void *pData и GameView.

Реализуйте функцию GameView *NewGameView(const sf::Vector2i &windowSize), которая:
- Создаёт и настраивает окно sf::RenderWindow с заданными размерами windowSize.
- Инициализирует камеру camera так, чтобы она охватывала весь экран.
- Возвращает указатель на созданный объект GameView.

Напишите функцию void EnterGameLoop(GameView &view, OnUpdate onUpdate, OnDraw onDraw, void *pData), которая:
- Входит в основной цикл, пока окно игры открыто.
- В каждом кадре обновляет deltaSec (разницу во времени с последнего кадра).
- Вызывает функции onUpdate и onDraw для обработки игровой логики и отрисовки.
- Завершает работу при закрытии окна.

Реализуйте функцию void SetCameraCenter(GameView &view, const sf::Vector2f &center), которая устанавливает центр камеры в точке center. Эта функция поможет управлять камерой в зависимости от положения главного героя или важного объекта.

Реализуйте функцию void DestroyGameView(GameView *&pView), которая:
- Освобождает ресурсы, связанные с GameView.
- Устанавливает pView в nullptr для избежания утечек памяти.

Tip
    #pragma once

    #include <SFML/Graphics.hpp>
    #include <functional>

    /// Структура, абстрагирующая работу с окном и камерой.
    ///  Код ниже намеренно написан в процедурном стиле:
    ///  - используются структуры
    ///  - используются указатели на функции
    ///  - используется явный вызов new и delete.

    struct GameView
    {
        sf::RenderWindow window;
        sf::View camera;
        sf::Vector2i windowSize;
        sf::Clock clock;
    };

    /// Объявляем типы указателей на функции-колбеки (callback),
    ///  вызываемые из основного цикла игры для совершения игровой логики.
    /// Параметр userData - произвольный указатель на внешние данные того,
    ///  кто предоставляет колбек.
    using OnUpdate = void (*)(void *pData, GameView &view, float deltaSec);
    using OnDraw = void (*)(void *pData, GameView &view);

    /// Создаёт новое окно игры.
    GameView *NewGameView(const sf::Vector2i &windowSize);

    /// Входит в основной цикл игры и возвращается, когда цикл завершён.
    void EnterGameLoop(GameView &view, OnUpdate onUpdate, OnDraw onDraw, void *pData);

    /// Центрирует камеру в заданной точке
    void SetCameraCenter(GameView &view, const sf::Vector2f &center);

    /// Разрушает окно игры и очищает его данные.
    void DestroyGameView(GameView *&pView);

3) main.cpp

Создать функцию main, в которой:
- Инициализируется игровое представление (GameView) с разрешением окна 800x600.
- Инициализируется игровая сцена (GameScene).
- Запустить игровой цикл с использованием функции EnterGameLoop, которая принимает в себя объекты GameView, функции UpdateGameScene и DrawGameScene, а также указатель на объект GameScene.
- Обработать возможные исключения типа std::exception и, в случае возникновения, вывести сообщение об ошибке в стандартный поток ошибок.
- Подсказка: Обратите внимание на выделение и освобождение памяти. В данном коде используется new и delete, что может привести к утечкам памяти при возникновении исключений. Чтобы этого избежать, замените new и delete на std::make_unique и std::unique_ptr.

Tip
    #include <iostream>
    #include "GameView.h"
    #include "GameScene.h"

    int main(int argc, char *argv[])
    {
        (void)argc;
        (void)argv;

        try
        {
            // NOTE: Если при загрузке карты будет выброшено исключение,
            //  то память утечёт. Избавиться от этого можно с помощью
            //  замены new/delete на make_unique и unique_ptr.
            GameView *pGameView = NewGameView({800, 600});
            GameScene *pGameScene = NewGameScene();

            // Аргумент типа `GameLogic*` будет преобразован в `void*`.
            EnterGameLoop(*pGameView, UpdateGameScene, DrawGameScene, pGameScene);
        }
        catch (const std::exception &ex)
        {
            std::cerr << ex.what() << std::endl;
            return 1;
        }

        return 0;
    }

Структура первой главы#

Название пункта: Глава 1 "Разработка средств загрузки уровня"

1.1) Нарисовать диаграмму классов по всем файлам проекта, кроме tinyxml2.cpp и tinyxml2.h

(можете воспользоваться средствами генерации классов по коду, например: https://uml-generator.vercel.app/
Однако! Обратите внимание, что между классами не отображаются связи! Их следует дорисовать)

1.2) Модифицировать уровень, используя Tiled. Показать результат на рисунке.

1.3) Приложить код файлов хэдеров с описанием в виде листингов (кроме tinyxml2.h)

Продолжение следует