ООП (ДЗ часть 1)#
Самый простой способ задать карту — объявить массив, например, такой:
const char MAP_DATA[] =
"#############"
"# @ . #"
"# . #"
"#############";
Для простой аркадной игры это нормально, но когда в игре должно быть несколько уровней, разные противники, разные бонусы — требуется решение получше. Можно написать свой редактор карт, но это большой объём работ. Проще использовать универсальный редактор карт Tiled
, и научиться интерпретировать созданные им карты в игровом движке.
Tiled Map Editor — кроссплатформенный открытый редактор тайловых карт для игр. Позволяет создавать карты для 2-мерных игр (с видом сбоку, таких как платформеры, или видом сверху, к примеру JRPG)
Созданные файлы имеют расширение *.tmx
и внутри содержат XML
Рекомендуется рабоатть с Tiled Map Editor версии 1.0. Для скачивания установщика (систему выбираете самостоятельно) можете воспользоваться следующими ссылками:
- Скачайте Tiled Map Editor для Windows
- Скачайте Tiled Map Editor для Linux
- Скачайте Tiled Map Editor для MacOS
Настройка проекта#
Создайте проект VS Code. Настройте загрузку библиотеки SFML (Гайд по настройке был в практике 5).
Скачайте (данные)[https://disk.yandex.ru/d/wzD9Xd5yB3-DJw] и разархивируйте в файл проекта
- В папке include файлы SFML
- В папке res находится уровень (platformer.tmx), шрифт(arial.ttf) и тайлы (tileset.png)
Подключите файлы tinyxml2.cpp
, tinyxml2.h
, GameScene.cpp
, TmxLevel.cpp
,
ПКМ По Project -> добавить -> существующий элемент
TinyXML2 — это бесплатная лёгкая библиотека для анализа XML-файлов. Она используется для сериализации данных на разных платформах.
TinyXML2 определяет несколько классов, которые обеспечивают различные слои необходимой функциональности. Вот некоторые из них:
XMLDocument
— контейнер для коллекции XMLElements. Это основной интерфейс для сохранения и загрузки XML-файлов.
XMLElement
— контейнер для одного XML-элемента с атрибутами.
XMLNode
— базовый класс для XMLElement и XMLDocument. Он обеспечивает интерфейс для обхода элементов в документе
Создание карты#
Чтобы пропустить этап настройки проекта вы можете загрузить готовый уровень (файл -> открыть):
Затем вы увидите загруженный уровень:
Редактор состоит из нескольких зон
- В зоне наборы тайлов вы можете выбирать тайлы для размещения. Щелкнув по тайлу вы сможете выбрать картинку и разместить на уровне
- Рабочая зона для изменения карты. Здесь располагается карта заданного размера. Пустые ячейки серого цвета.
- Cлои тайлов. Тайлы объединяются в слои, и каждый новый слой рисуется поверх другого (например, полупрозрачный и не везде заполненный слой травы рисуется поверх слоя земли). Слои редактируются вверху правой панели.
- Слои объектов. Если файлы являются только фоном, то объекты активно участвуют в игровой логике. Объект — это квадратная, круглая или многоугольная область. Чтобы создавать объекты, сперва создайте слой объектов на панели слоёв. После станет доступна панель инструментов объектов
У области есть имя
и набор произвольных свойств
, что позволяет использовать области как маркеры мест, в которых потом появится игрок, противники или бонусы. Также можно превратить область в физическое препятствие, или считать областью случайного появления противников, или расположить там портал, при достижении которого происходит переход на новый уровень. Обработкой объектов занимается движок игры.
Поля “Имя” и “Тип” хранят строки, в которых удобно задавать категорию объекта (например, тип юнита игры или бонуса, который он представляет). Впоследствии игра считает эти строковые свойства и сможет выбрать, какой объект надо создать в указанном месте.
Разбор карты в программе#
Для разбора карты в формате 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 ¢er)
, которая устанавливает центр камеры в точке 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 ¢er);
/// Разрушает окно игры и очищает его данные.
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)
Продолжение следует