Здравствуйте! Сегодня мы будем писать платформер, используя C++, Box2D и SFML, а также редактор 2D карт для игр Tiled Map Editor.
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/601/b21/f51/601b21f51d4be0d9d0f022c0d30a43ae.png)
Вот результат (карта создавалась 5 минут + во время сьемки игра тормозила + экран не так растянут — дефект Bandicam):
Исходники и exe — внизу статьи.
Эту библиотеку мы будем использовать для симуляции физики в платформере (столкновение с блоками, гравитация). Возможно, не стоило для одних только блоков юзать эту библиотеку, но красиво жить не запретишь ;)
Почему именно Box2D? Потому что это самая распространенная и бесплатная физическая библиотека
Почему SFML? Вначале я хотел использовать библиотеку SDL, но она сильно ограничена в возможностях по сравнению с SFML, многое пришлось бы дописывать самому. Спасибо автору SFML за сэкономленное время!
Ее используем для отрисовки графики
Что делает тут Tiled Map Editor?
Вы когда-нибудь пробовали создавать карты для игр? Спорим, что вашей первой картой было что-то наподобие такого.
Это довольно неэффективное решение! Гораздо лучше написать что-то вроде редактора карт, но задним числом мы понимаем, что это не делается за 5 минут, а приведенная выше «карта» — вполне.
Tiled Map Editor — один из таких редакторов карт. Он хорош тем, что карту, созданную в этом редакторе (состоит из объектов, тайлов, их слоев) можно сохранить в XML-подобном файле .tmx и потом с помощью специальной библиотеки на C++ считать ее. Но обо всем по порядку.
Скачиваем TME с официального сайта
Создаем новую карту «Файл->Создать...»
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/dab/377/6e7/dab3776e7a9c854bff8a878991fff56a.png)
Ориентация должна быть ортогональной (если вы не делаете изометрический платформер), а формат слоя XML, мы будем считывать именно этот формат
Кстати, ни формат слоя, ни размер тайлов нельзя будет поменять в созданной карте.
Затем идем в «Карта->Новый набор тайлов...», загружаем наш тайлсет:
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/3bf/459/069/3bf4590693648204410863d265811961.png)
В итоге у вас получится что-то вроде этого:
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/e91/926/20c/e9192620c22ea1b81890d5b07beb7ba9.png)
В чем смысл слоев тайлов?
Почти в каждой игре есть многослойные карты. Первый слой — земля (лед, чернозем, etc), второй слой — здания (казармы, форт, etc, причем фон прозрачен), третий — деревья (ель, пихта, etc, фон тоже прозрачен). То есть рисуется сначала первый слой, поверх него накладывается второй слой, а потом уже третий.
Процесс создания слоев запечатлен на следующих 4 скриншотах:
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/538/068/9c4/5380689c446222a47247643a8eb84ac9.png)
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/434/709/919/4347099196bdc1027eac74c06c448146.png)
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/9b5/6e1/24f/9b56e124fe8ec32ecb9b5eb12f1e8e3e.png)
Список слоев:
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/dad/944/7cf/dad9447cfadacdc58f961053156fb844.png)
Что такое объект в TME?
Объект имеет свое имя, тип, а также параметры со значениями.
За объекты отвечает эта панель
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/8e3/3ea/7a0/8e33ea7a0a01a43830698c18a4d30efe.png)
Вы вполне можете узнать, что делает каждая из кнопок, сами.
Теперь попробуем создать объект.
Удаляем слой «Колобоша», вместо него создаем слой объектов, допустим, с тем же названием «Колобоша». Выбираем «Вставить тайл-объект» из панели для объектов (или можете выбрать любую фигуру — Shape), нажимаем на тайл Колобоши и просто ставим объект в какое-нибудь место.
После чего нажимаем правой кнопкой мыши на объект и нажимаем на «Свойства объекта...». Измените имя объекта на Kolobosha.
После чего сохраните карту.
В общем, ничего архисложного в редакторах карт нету. Пора переходить к считыванию карты.
Для считывания XML файлов создана отличная библиотека TinyXML, скачайте ее исходники.
Создайте проект Visual Studio. Подключите файлы TinyXML (или просто запихайте все эти файлы в проект, за исключением xmltest.cpp :) )
Теперь подключаем includ'ы и lib'ы SFML в «Проект->Свойства». Если не знаете, как это делать — добро пожаловать в Гугл
Создаем Level.h для карт
Это начало файла.
Дальше идет структура объекта
Разберем её.
Как уже говорилось, в TME каждый объект может иметь параметры. Параметры берутся из XML файла, записываются в properties, и потом их можно получить любой из первых трех функций. name — имя объекта, type — его тип, rect — прямоугольник, описывающий объект. И наконец, sprite — спрайт (изображение) — часть тайлсета, взятая для объекта. Спрайта может и не быть.
Теперь идет структура слоя — она очень проста
В слое есть прозрачность (да, да, мы можем делать полупрозрачные слои!) и список из тайлов.
Дальше идет класс Level:
LoadFromFile загружает карту из указанного файла. Это сердце класса Level.
GetObject возвращает первый объект с указанным именем, GetObjects возвращает список объектов с указанным именем. Вообще-то, по-хорошему, следовало использовать тип (type) объекта, но мне было удобнее вылавливать блоки и игрока через имя, так как в редакторе имя показывается сверху объекта, а тип — нет.
Draw рисует все тайлы (не объекты!), беря себе экземпляр RenderWindow.
Теперь создаем Level.cpp:
Первым мы обрабатываем структуру объектов
Для Layer реализация не нужна, переходим к Level:
Остальные функции Level:
С Level.h окончено!
Протестируем его.
Создаем main.cpp и пишем:
Карта может выглядеть как угодно!
Можете поиграться с объектами:
![image](http://s6.hostingkartinok.com/uploads/images/2013/10/e99cc289b5fb814eb7c509ed69cef81c.png)
main.cpp
Результат:
![image](http://s3.hostingkartinok.com/uploads/images/2013/10/65774b5a0cc0887d293dd29f9eea7677.png)
Когда вы наиграетесь с объектами, наступит пора Box2D:
Мы хотим создать3D-экшон платформер, суть такова…
На карте расположены объекты — с названиями player — игрок, enemy — враг, block — блок, money — монетки.
Мы загружаем игрока, заставляем его повиноваться нажатиям клавиши и силе Ньютона.
Враги ходят туда-сюда, отталкивают слишком близко находящегося игрока и погибают, если игрок прыгает на них
Блоки закрепляются «в воздухе» как статичные объекты, на них игрок может прыгать
Монеты ничего не дают, просто исчезают при столкновении с игроком
Открываем main.h, стираем то, что там было написано, и пишем:
Здесь у нас подключены level.h и Box2D.h. iostream нужен для вывода в консоль, random — для генерации направления движения врага.
Далее идут игрок и векторы, каждому врагу, монетке, игроку полагается свой Object и b2Body (тело в Box2D).
Внимание — блокам этого не полагается, так как они взаимодействуют с игроком только на уровне физики Box2D, а не в игровой логике.
Далее:
srand(time(NULL)) нужен для рандома.
Загружаем карту, создаем b2World, передавая ей гравитацию. Кстати, гравитация может исходить из какого угодно направления, и гравитация из (0,10) действует сильнее (0,1). Потом мы берем нужный нам размер тайлов
Далее создаем тела блоков:
Блоки — статические тела, они не имеют массы и висят в воздухе:
Тут мы устанавливаем позицию блоков. Дело в том, что если просто указать позицию такую же, как у объекта, нас будет ждать коварная ошибка.
Создаем тело блока в world. Далее мы с телом не работаем (в смысле, нигде не храним):
Каждому телу принадлежит несколько shape — фигур. Я не буду подробно разбирать эту тему, так как блокам (и остальным телам) хватает всего-то одного прямоугольника.
Связываем фигуру с телом.
Затем мы делаем то же самое с врагами, монетами и игроком, за небольшими различиями:
Означает, что тело не может вращаться.
Все тела созданы, осталось инициализировать графику!
Хорошо понятный код, создает окно с указанным размером и заголовком:
Тут мы создаем вид (View) для окна.
Зачем это надо? Для того, чтобы придать игре пиксельный стиль, мы умножаем размер экрана на 2 с использованием sf::View и все картинки рисуются в 2 раза выше и шире.
Окно закрывается по нажатию на красный крестик. Такой код был ранее:
Тут уже интереснее! Мы добавляем скорость игроку по нажатию клавиш WAD:
Тут мы обновляем физический мир Box2D. Первый аргумент принимает частоту обновления мира (раз в 1/60 секунд), а также количество velocityIterations и positionIterations. Чем выше значение последних двух аргументов, тем реальнее получается физика игры. Так как у нас нету никаких сложных фигур, как в AngryBirds, а только прямоугольники, то нам достаточно по разу.
Здесь мы обрабатываем столкновение игрока с другими телами:
Обработка столкновения с монетами.
Если какая монета столкнулась с игроком, она просто уничтожается и стирается из векторов:
Если враг сталкивается с игроком, проверяется, выше игрок врага или нет. Если игрок выше врага, то он стирается, а игрок подскакивает вверх.
Если иначе, то игрок отскакивает от врага:
Игрок движется направо или налево в соотвествии с его текущим положением относительно врага.
Если скорость врага равна 0, то ему скорость придается вновь — он движется либо направо, либо налево. Визуально это выглядит как движение рывками.
Работа с графикой. Берем позицию игрока, изменяем центр вида и используем наш вид
Устанавливаем спрайтам игрока, монет и врагов позиции, полученные из b2Body:
Очищаем окна, рисуем тайлы карты, потом игрока, монеты и врагов, после чего представляем окно.
Готово!
Примерная карта:
![image](http://s6.hostingkartinok.com/uploads/images/2013/10/a348a0abc8945f94fa4cb205342b074e.png)
![image](http://static.pmmlabs.ru/images/github-logo.png)
https://github.com/Izaron/Platformer
Автору этого топика
Авторам SFML
Авторам Box2D
Авторам TinyXml
![image](https://habrastorage.org/getpro/habr/post_images/601/b21/f51/601b21f51d4be0d9d0f022c0d30a43ae.png)
Вот результат (карта создавалась 5 минут + во время сьемки игра тормозила + экран не так растянут — дефект Bandicam):
Исходники и exe — внизу статьи.
Что, где, когда?
Box2D
Эту библиотеку мы будем использовать для симуляции физики в платформере (столкновение с блоками, гравитация). Возможно, не стоило для одних только блоков юзать эту библиотеку, но красиво жить не запретишь ;)
Почему именно Box2D? Потому что это самая распространенная и бесплатная физическая библиотека
SFML
Почему SFML? Вначале я хотел использовать библиотеку SDL, но она сильно ограничена в возможностях по сравнению с SFML, многое пришлось бы дописывать самому. Спасибо автору SFML за сэкономленное время!
Ее используем для отрисовки графики
Tiled Map Editor
Что делает тут Tiled Map Editor?
Вы когда-нибудь пробовали создавать карты для игр? Спорим, что вашей первой картой было что-то наподобие такого.
Скрытый текст
1111111
1000001
1001001
1000011
1000111
1111111
Это довольно неэффективное решение! Гораздо лучше написать что-то вроде редактора карт, но задним числом мы понимаем, что это не делается за 5 минут, а приведенная выше «карта» — вполне.
Tiled Map Editor — один из таких редакторов карт. Он хорош тем, что карту, созданную в этом редакторе (состоит из объектов, тайлов, их слоев) можно сохранить в XML-подобном файле .tmx и потом с помощью специальной библиотеки на C++ считать ее. Но обо всем по порядку.
Создание карты
Скачиваем TME с официального сайта
Создаем новую карту «Файл->Создать...»
![image](https://habrastorage.org/getpro/habr/post_images/dab/377/6e7/dab3776e7a9c854bff8a878991fff56a.png)
Ориентация должна быть ортогональной (если вы не делаете изометрический платформер), а формат слоя XML, мы будем считывать именно этот формат
Кстати, ни формат слоя, ни размер тайлов нельзя будет поменять в созданной карте.
Тайлы
Затем идем в «Карта->Новый набор тайлов...», загружаем наш тайлсет:
![image](https://habrastorage.org/getpro/habr/post_images/3bf/459/069/3bf4590693648204410863d265811961.png)
В итоге у вас получится что-то вроде этого:
![image](https://habrastorage.org/getpro/habr/post_images/e91/926/20c/e9192620c22ea1b81890d5b07beb7ba9.png)
В чем смысл слоев тайлов?
Почти в каждой игре есть многослойные карты. Первый слой — земля (лед, чернозем, etc), второй слой — здания (казармы, форт, etc, причем фон прозрачен), третий — деревья (ель, пихта, etc, фон тоже прозрачен). То есть рисуется сначала первый слой, поверх него накладывается второй слой, а потом уже третий.
Процесс создания слоев запечатлен на следующих 4 скриншотах:
![image](https://habrastorage.org/getpro/habr/post_images/538/068/9c4/5380689c446222a47247643a8eb84ac9.png)
![image](https://habrastorage.org/getpro/habr/post_images/434/709/919/4347099196bdc1027eac74c06c448146.png)
![image](https://habrastorage.org/getpro/habr/post_images/9b5/6e1/24f/9b56e124fe8ec32ecb9b5eb12f1e8e3e.png)
Список слоев:
![image](https://habrastorage.org/getpro/habr/post_images/dad/944/7cf/dad9447cfadacdc58f961053156fb844.png)
Объекты
Что такое объект в TME?
Объект имеет свое имя, тип, а также параметры со значениями.
За объекты отвечает эта панель
![image](https://habrastorage.org/getpro/habr/post_images/8e3/3ea/7a0/8e33ea7a0a01a43830698c18a4d30efe.png)
Вы вполне можете узнать, что делает каждая из кнопок, сами.
Теперь попробуем создать объект.
Удаляем слой «Колобоша», вместо него создаем слой объектов, допустим, с тем же названием «Колобоша». Выбираем «Вставить тайл-объект» из панели для объектов (или можете выбрать любую фигуру — Shape), нажимаем на тайл Колобоши и просто ставим объект в какое-нибудь место.
После чего нажимаем правой кнопкой мыши на объект и нажимаем на «Свойства объекта...». Измените имя объекта на Kolobosha.
После чего сохраните карту.
В общем, ничего архисложного в редакторах карт нету. Пора переходить к считыванию карты.
Считывание карты
Для считывания XML файлов создана отличная библиотека TinyXML, скачайте ее исходники.
Создайте проект Visual Studio. Подключите файлы TinyXML (или просто запихайте все эти файлы в проект, за исключением xmltest.cpp :) )
Теперь подключаем includ'ы и lib'ы SFML в «Проект->Свойства». Если не знаете, как это делать — добро пожаловать в Гугл
Создаем Level.h для карт
#ifndef LEVEL_H
#define LEVEL_H
#pragma comment(lib,"Box2D.lib")
#pragma comment(lib,"sfml-graphics.lib")
#pragma comment(lib,"sfml-window.lib")
#pragma comment(lib,"sfml-system.lib")
#include <string>
#include <vector>
#include <map>
#include <SFML/Graphics.hpp>
Это начало файла.
Дальше идет структура объекта
struct Object
{
int GetPropertyInt(std::string name);
float GetPropertyFloat(std::string name);
std::string GetPropertyString(std::string name);
std::string name;
std::string type;
sf::Rect<int> rect;
std::map<std::string, std::string> properties;
sf::Sprite sprite;
};
Разберем её.
Как уже говорилось, в TME каждый объект может иметь параметры. Параметры берутся из XML файла, записываются в properties, и потом их можно получить любой из первых трех функций. name — имя объекта, type — его тип, rect — прямоугольник, описывающий объект. И наконец, sprite — спрайт (изображение) — часть тайлсета, взятая для объекта. Спрайта может и не быть.
Теперь идет структура слоя — она очень проста
struct Layer
{
int opacity;
std::vector<sf::Sprite> tiles;
};
В слое есть прозрачность (да, да, мы можем делать полупрозрачные слои!) и список из тайлов.
Дальше идет класс Level:
class Level
{
public:
bool LoadFromFile(std::string filename);
Object GetObject(std::string name);
std::vector<Object> GetObjects(std::string name);
void Draw(sf::RenderWindow &window);
sf::Vector2i GetTileSize();
private:
int width, height, tileWidth, tileHeight;
int firstTileID;
sf::Rect<float> drawingBounds;
sf::Texture tilesetImage;
std::vector<Object> objects;
std::vector<Layer> layers;
};
#endif
LoadFromFile загружает карту из указанного файла. Это сердце класса Level.
GetObject возвращает первый объект с указанным именем, GetObjects возвращает список объектов с указанным именем. Вообще-то, по-хорошему, следовало использовать тип (type) объекта, но мне было удобнее вылавливать блоки и игрока через имя, так как в редакторе имя показывается сверху объекта, а тип — нет.
Draw рисует все тайлы (не объекты!), беря себе экземпляр RenderWindow.
Теперь создаем Level.cpp:
#include "level.h"
#include <iostream>
#include "tinyxml.h"
Первым мы обрабатываем структуру объектов
int Object::GetPropertyInt(std::string name)
{
return atoi(properties[name].c_str());
}
float Object::GetPropertyFloat(std::string name)
{
return strtod(properties[name].c_str(), NULL);
}
std::string Object::GetPropertyString(std::string name)
{
return properties[name];
}
Для Layer реализация не нужна, переходим к Level:
bool Level::LoadFromFile(std::string filename)
bool Level::LoadFromFile(std::string filename)
{
TiXmlDocument levelFile(filename.c_str());
// Загружаем XML-карту
if(!levelFile.LoadFile())
{
std::cout << "Loading level \"" << filename << "\" failed." << std::endl;
return false;
}
// Работаем с контейнером map
TiXmlElement *map;
map = levelFile.FirstChildElement("map");
// Пример карты: <map version="1.0" orientation="orthogonal"
// width="10" height="10" tilewidth="34" tileheight="34">
width = atoi(map->Attribute("width"));
height = atoi(map->Attribute("height"));
tileWidth = atoi(map->Attribute("tilewidth"));
tileHeight = atoi(map->Attribute("tileheight"));
// Берем описание тайлсета и идентификатор первого тайла
TiXmlElement *tilesetElement;
tilesetElement = map->FirstChildElement("tileset");
firstTileID = atoi(tilesetElement->Attribute("firstgid"));
// source - путь до картинки в контейнере image
TiXmlElement *image;
image = tilesetElement->FirstChildElement("image");
std::string imagepath = image->Attribute("source");
// Пытаемся загрузить тайлсет
sf::Image img;
if(!img.loadFromFile(imagepath))
{
std::cout << "Failed to load tile sheet." << std::endl;
return false;
}
// Очищаем карту от света (109, 159, 185)
// Вообще-то в тайлсете может быть фон любого цвета, но я не нашел решения, как 16-ричную строку
// вроде "6d9fb9" преобразовать в цвет
img.createMaskFromColor(sf::Color(109, 159, 185));
// Грузим текстуру из изображения
tilesetImage.loadFromImage(img);
// Расплывчатость запрещена
tilesetImage.setSmooth(false);
// Получаем количество столбцов и строк тайлсета
int columns = tilesetImage.getSize().x / tileWidth;
int rows = tilesetImage.getSize().y / tileHeight;
// Вектор из прямоугольников изображений (TextureRect)
std::vector<sf::Rect<int>> subRects;
for(int y = 0; y < rows; y++)
for(int x = 0; x < columns; x++)
{
sf::Rect<int> rect;
rect.top = y * tileHeight;
rect.height = tileHeight;
rect.left = x * tileWidth;
rect.width = tileWidth;
subRects.push_back(rect);
}
// Работа со слоями
TiXmlElement *layerElement;
layerElement = map->FirstChildElement("layer");
while(layerElement)
{
Layer layer;
// Если присутствует opacity, то задаем прозрачность слоя, иначе он полностью непрозрачен
if (layerElement->Attribute("opacity") != NULL)
{
float opacity = strtod(layerElement->Attribute("opacity"), NULL);
layer.opacity = 255 * opacity;
}
else
{
layer.opacity = 255;
}
// Контейнер <data>
TiXmlElement *layerDataElement;
layerDataElement = layerElement->FirstChildElement("data");
if(layerDataElement == NULL)
{
std::cout << "Bad map. No layer information found." << std::endl;
}
// Контейнер <tile> - описание тайлов каждого слоя
TiXmlElement *tileElement;
tileElement = layerDataElement->FirstChildElement("tile");
if(tileElement == NULL)
{
std::cout << "Bad map. No tile information found." << std::endl;
return false;
}
int x = 0;
int y = 0;
while(tileElement)
{
int tileGID = atoi(tileElement->Attribute("gid"));
int subRectToUse = tileGID - firstTileID;
// Устанавливаем TextureRect каждого тайла
if (subRectToUse >= 0)
{
sf::Sprite sprite;
sprite.setTexture(tilesetImage);
sprite.setTextureRect(subRects[subRectToUse]);
sprite.setPosition(x * tileWidth, y * tileHeight);
sprite.setColor(sf::Color(255, 255, 255, layer.opacity));
layer.tiles.push_back(sprite);
}
tileElement = tileElement->NextSiblingElement("tile");
x++;
if (x >= width)
{
x = 0;
y++;
if(y >= height)
y = 0;
}
}
layers.push_back(layer);
layerElement = layerElement->NextSiblingElement("layer");
}
// Работа с объектами
TiXmlElement *objectGroupElement;
// Если есть слои объектов
if (map->FirstChildElement("objectgroup") != NULL)
{
objectGroupElement = map->FirstChildElement("objectgroup");
while (objectGroupElement)
{
// Контейнер <object>
TiXmlElement *objectElement;
objectElement = objectGroupElement->FirstChildElement("object");
while(objectElement)
{
// Получаем все данные - тип, имя, позиция, etc
std::string objectType;
if (objectElement->Attribute("type") != NULL)
{
objectType = objectElement->Attribute("type");
}
std::string objectName;
if (objectElement->Attribute("name") != NULL)
{
objectName = objectElement->Attribute("name");
}
int x = atoi(objectElement->Attribute("x"));
int y = atoi(objectElement->Attribute("y"));
int width, height;
sf::Sprite sprite;
sprite.setTexture(tilesetImage);
sprite.setTextureRect(sf::Rect<int>(0,0,0,0));
sprite.setPosition(x, y);
if (objectElement->Attribute("width") != NULL)
{
width = atoi(objectElement->Attribute("width"));
height = atoi(objectElement->Attribute("height"));
}
else
{
width = subRects[atoi(objectElement->Attribute("gid")) - firstTileID].width;
height = subRects[atoi(objectElement->Attribute("gid")) - firstTileID].height;
sprite.setTextureRect(subRects[atoi(objectElement->Attribute("gid")) - firstTileID]);
}
// Экземпляр объекта
Object object;
object.name = objectName;
object.type = objectType;
object.sprite = sprite;
sf::Rect <int> objectRect;
objectRect.top = y;
objectRect.left = x;
objectRect.height = height;
objectRect.width = width;
object.rect = objectRect;
// "Переменные" объекта
TiXmlElement *properties;
properties = objectElement->FirstChildElement("properties");
if (properties != NULL)
{
TiXmlElement *prop;
prop = properties->FirstChildElement("property");
if (prop != NULL)
{
while(prop)
{
std::string propertyName = prop->Attribute("name");
std::string propertyValue = prop->Attribute("value");
object.properties[propertyName] = propertyValue;
prop = prop->NextSiblingElement("property");
}
}
}
// Пихаем объект в вектор
objects.push_back(object);
objectElement = objectElement->NextSiblingElement("object");
}
objectGroupElement = objectGroupElement->NextSiblingElement("objectgroup");
}
}
else
{
std::cout << "No object layers found..." << std::endl;
}
return true;
}
Остальные функции Level:
Object Level::GetObject(std::string name)
{
// Только первый объект с заданным именем
for (int i = 0; i < objects.size(); i++)
if (objects[i].name == name)
return objects[i];
}
std::vector<Object> Level::GetObjects(std::string name)
{
// Все объекты с заданным именем
std::vector<Object> vec;
for(int i = 0; i < objects.size(); i++)
if(objects[i].name == name)
vec.push_back(objects[i]);
return vec;
}
sf::Vector2i Level::GetTileSize()
{
return sf::Vector2i(tileWidth, tileHeight);
}
void Level::Draw(sf::RenderWindow &window)
{
// Рисуем все тайлы (объекты НЕ рисуем!)
for(int layer = 0; layer < layers.size(); layer++)
for(int tile = 0; tile < layers[layer].tiles.size(); tile++)
window.draw(layers[layer].tiles[tile]);
}
С Level.h окончено!
Протестируем его.
Создаем main.cpp и пишем:
#include "level.h"
int main()
{
Level level;
level.LoadFromFile("test.tmx");
sf::RenderWindow window;
window.create(sf::VideoMode(800, 600), "Level.h test");
while(window.isOpen())
{
sf::Event event;
while(window.pollEvent(event))
{
if(event.type == sf::Event::Closed)
window.close();
}
window.clear();
level.Draw(window);
window.display();
}
return 0;
}
Карта может выглядеть как угодно!
Можете поиграться с объектами:
![image](http://s6.hostingkartinok.com/uploads/images/2013/10/e99cc289b5fb814eb7c509ed69cef81c.png)
main.cpp
#include "level.h"
#include <iostream>
int main()
{
Level level;
level.LoadFromFile("test.tmx");
Object kolobosha = level.GetObject("Kolobosha");
std::cout << kolobosha.name << std::endl;
std::cout << kolobosha.type << std::endl;
std::cout << kolobosha.GetPropertyInt("health") << std::endl;
std::cout << kolobosha.GetPropertyString("mood") << std::endl;
sf::RenderWindow window;
window.create(sf::VideoMode(800, 600), "Kolobosha adventures");
while(window.isOpen())
{
sf::Event event;
while(window.pollEvent(event))
{
if(event.type == sf::Event::Closed)
window.close();
}
window.clear();
level.Draw(window);
window.display();
}
return 0;
}
Результат:
![image](http://s3.hostingkartinok.com/uploads/images/2013/10/65774b5a0cc0887d293dd29f9eea7677.png)
Когда вы наиграетесь с объектами, наступит пора Box2D:
Коробки-коробочки
Мы хотим создать
На карте расположены объекты — с названиями player — игрок, enemy — враг, block — блок, money — монетки.
Мы загружаем игрока, заставляем его повиноваться нажатиям клавиши и силе Ньютона.
Враги ходят туда-сюда, отталкивают слишком близко находящегося игрока и погибают, если игрок прыгает на них
Блоки закрепляются «в воздухе» как статичные объекты, на них игрок может прыгать
Монеты ничего не дают, просто исчезают при столкновении с игроком
Открываем main.h, стираем то, что там было написано, и пишем:
#include "level.h"
#include <Box2D\Box2D.h>
#include <iostream>
#include <random>
Object player;
b2Body* playerBody;
std::vector<Object> coin;
std::vector<b2Body*> coinBody;
std::vector<Object> enemy;
std::vector<b2Body*> enemyBody;
Здесь у нас подключены level.h и Box2D.h. iostream нужен для вывода в консоль, random — для генерации направления движения врага.
Далее идут игрок и векторы, каждому врагу, монетке, игроку полагается свой Object и b2Body (тело в Box2D).
Внимание — блокам этого не полагается, так как они взаимодействуют с игроком только на уровне физики Box2D, а не в игровой логике.
Далее:
int main()
{
srand(time(NULL));
Level lvl;
lvl.LoadFromFile("platformer.tmx");
b2Vec2 gravity(0.0f, 1.0f);
b2World world(gravity);
sf::Vector2i tileSize = lvl.GetTileSize();
srand(time(NULL)) нужен для рандома.
Загружаем карту, создаем b2World, передавая ей гравитацию. Кстати, гравитация может исходить из какого угодно направления, и гравитация из (0,10) действует сильнее (0,1). Потом мы берем нужный нам размер тайлов
Далее создаем тела блоков:
std::vector<Object> block = lvl.GetObjects("block");
for(int i = 0; i < block.size(); i++)
{
b2BodyDef bodyDef;
bodyDef.type = b2_staticBody;
bodyDef.position.Set(block[i].rect.left + tileSize.x / 2 * (block[i].rect.width / tileSize.x - 1),
block[i].rect.top + tileSize.y / 2 * (block[i].rect.height / tileSize.y - 1));
b2Body* body = world.CreateBody(&bodyDef);
b2PolygonShape shape;
shape.SetAsBox(block[i].rect.width / 2, block[i].rect.height / 2);
body->CreateFixture(&shape,1.0f);
}
bodyDef.type = b2_staticBody;
Блоки — статические тела, они не имеют массы и висят в воздухе:
bodyDef.position.Set(block[i].rect.left + tileSize.x / 2 * (block[i].rect.width / tileSize.x - 1),
block[i].rect.top + tileSize.y / 2 * (block[i].rect.height / tileSize.y - 1));
Тут мы устанавливаем позицию блоков. Дело в том, что если просто указать позицию такую же, как у объекта, нас будет ждать коварная ошибка.
b2Body* body = world.CreateBody(&bodyDef);
Создаем тело блока в world. Далее мы с телом не работаем (в смысле, нигде не храним):
b2PolygonShape shape;
shape.SetAsBox(block[i].rect.width / 2, block[i].rect.height / 2);
Каждому телу принадлежит несколько shape — фигур. Я не буду подробно разбирать эту тему, так как блокам (и остальным телам) хватает всего-то одного прямоугольника.
body->CreateFixture(&shape,1.0f);
Связываем фигуру с телом.
Затем мы делаем то же самое с врагами, монетами и игроком, за небольшими различиями:
coin = lvl.GetObjects("coin");
for(int i = 0; i < coin.size(); i++)
{
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
bodyDef.position.Set(coin[i].rect.left + tileSize.x / 2 * (coin[i].rect.width / tileSize.x - 1),
coin[i].rect.top + tileSize.y / 2 * (coin[i].rect.height / tileSize.y - 1));
bodyDef.fixedRotation = true;
b2Body* body = world.CreateBody(&bodyDef);
b2PolygonShape shape;
shape.SetAsBox(coin[i].rect.width / 2, coin[i].rect.height / 2);
body->CreateFixture(&shape,1.0f);
coinBody.push_back(body);
}
enemy = lvl.GetObjects("enemy");
for(int i = 0; i < enemy.size(); i++)
{
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
bodyDef.position.Set(enemy[i].rect.left +
tileSize.x / 2 * (enemy[i].rect.width / tileSize.x - 1),
enemy[i].rect.top + tileSize.y / 2 * (enemy[i].rect.height / tileSize.y - 1));
bodyDef.fixedRotation = true;
b2Body* body = world.CreateBody(&bodyDef);
b2PolygonShape shape;
shape.SetAsBox(enemy[i].rect.width / 2, enemy[i].rect.height / 2);
body->CreateFixture(&shape,1.0f);
enemyBody.push_back(body);
}
player = lvl.GetObject("player");
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
bodyDef.position.Set(player.rect.left, player.rect.top);
bodyDef.fixedRotation = true;
playerBody = world.CreateBody(&bodyDef);
b2PolygonShape shape; shape.SetAsBox(player.rect.width / 2, player.rect.height / 2);
b2FixtureDef fixtureDef;
fixtureDef.shape = &shape;
fixtureDef.density = 1.0f; fixtureDef.friction = 0.3f;
playerBody->CreateFixture(&fixtureDef);
bodyDef.fixedRotation = true;
Означает, что тело не может вращаться.
Все тела созданы, осталось инициализировать графику!
sf::Vector2i screenSize(800, 600);
sf::RenderWindow window;
window.create(sf::VideoMode(screenSize.x, screenSize.y), "Game");
Хорошо понятный код, создает окно с указанным размером и заголовком:
sf::View view;
view.reset(sf::FloatRect(0.0f, 0.0f, screenSize.x, screenSize.y));
view.setViewport(sf::FloatRect(0.0f, 0.0f, 2.0f, 2.0f));
Тут мы создаем вид (View) для окна.
Зачем это надо? Для того, чтобы придать игре пиксельный стиль, мы умножаем размер экрана на 2 с использованием sf::View и все картинки рисуются в 2 раза выше и шире.
while(window.isOpen())
{
sf::Event evt;
while(window.pollEvent(evt))
{
switch(evt.type)
{
case sf::Event::Closed:
window.close();
break;
Окно закрывается по нажатию на красный крестик. Такой код был ранее:
case sf::Event::KeyPressed:
if(evt.key.code == sf::Keyboard::W)
playerBody->SetLinearVelocity(b2Vec2(0.0f, -15.0f));
if(evt.key.code == sf::Keyboard::D)
playerBody->SetLinearVelocity(b2Vec2(5.0f, 0.0f));
if(evt.key.code == sf::Keyboard::A)
playerBody->SetLinearVelocity(b2Vec2(-5.0f, 0.0f));
break;
Тут уже интереснее! Мы добавляем скорость игроку по нажатию клавиш WAD:
world.Step(1.0f / 60.0f, 1, 1);
Тут мы обновляем физический мир Box2D. Первый аргумент принимает частоту обновления мира (раз в 1/60 секунд), а также количество velocityIterations и positionIterations. Чем выше значение последних двух аргументов, тем реальнее получается физика игры. Так как у нас нету никаких сложных фигур, как в AngryBirds, а только прямоугольники, то нам достаточно по разу.
for(b2ContactEdge* ce = playerBody->GetContactList(); ce; ce = ce->next)
{
b2Contact* c = ce->contact;
Здесь мы обрабатываем столкновение игрока с другими телами:
for(int i = 0; i < coinBody.size(); i++)
if(c->GetFixtureA() == coinBody[i]->GetFixtureList())
{
coinBody[i]->DestroyFixture(coinBody[i]->GetFixtureList());
coin.erase(coin.begin() + i);
coinBody.erase(coinBody.begin() + i);
}
Обработка столкновения с монетами.
Если какая монета столкнулась с игроком, она просто уничтожается и стирается из векторов:
for(int i = 0; i < enemyBody.size(); i++)
if(c->GetFixtureA() == enemyBody[i]->GetFixtureList())
{
if(playerBody->GetPosition().y < enemyBody[i]->GetPosition().y)
{
playerBody->SetLinearVelocity(b2Vec2(0.0f, -10.0f));
enemyBody[i]->DestroyFixture(enemyBody[i]->GetFixtureList());
enemy.erase(enemy.begin() + i);
enemyBody.erase(enemyBody.begin() + i);
}
Если враг сталкивается с игроком, проверяется, выше игрок врага или нет. Если игрок выше врага, то он стирается, а игрок подскакивает вверх.
Если иначе, то игрок отскакивает от врага:
else
{
int tmp = (playerBody->GetPosition().x < enemyBody[i]->GetPosition().x)
? -1 : 1;
playerBody->SetLinearVelocity(b2Vec2(10.0f * tmp, 0.0f));
}
}
}
Игрок движется направо или налево в соотвествии с его текущим положением относительно врага.
for(int i = 0; i < enemyBody.size(); i++)
{
if(enemyBody[i]->GetLinearVelocity() == b2Vec2_zero)
{
int tmp = (rand() % 2 == 1) ? 1 : -1;
enemyBody[i]->SetLinearVelocity(b2Vec2(5.0f * tmp, 0.0f));
}
}
Если скорость врага равна 0, то ему скорость придается вновь — он движется либо направо, либо налево. Визуально это выглядит как движение рывками.
b2Vec2 pos = playerBody->GetPosition();
view.setCenter(pos.x + screenSize.x / 4, pos.y + screenSize.y / 4);
window.setView(view);
Работа с графикой. Берем позицию игрока, изменяем центр вида и используем наш вид
player.sprite.setPosition(pos.x, pos.y);
for(int i = 0; i < coin.size(); i++)
coin[i].sprite.setPosition(coinBody[i]->GetPosition().x, coinBody[i]->GetPosition().y);
for(int i = 0; i < enemy.size(); i++)
enemy[i].sprite.setPosition(enemyBody[i]->GetPosition().x, enemyBody[i]->GetPosition().y);
Устанавливаем спрайтам игрока, монет и врагов позиции, полученные из b2Body:
window.clear();
lvl.Draw(window);
window.draw(player.sprite);
for(int i = 0; i < coin.size(); i++)
window.draw(coin[i].sprite);
for(int i = 0; i < enemy.size(); i++)
window.draw(enemy[i].sprite);
window.display();
Очищаем окна, рисуем тайлы карты, потом игрока, монеты и врагов, после чего представляем окно.
}
return 0;
}
Готово!
Примерная карта:
![image](http://s6.hostingkartinok.com/uploads/images/2013/10/a348a0abc8945f94fa4cb205342b074e.png)
Исходники
![image](http://static.pmmlabs.ru/images/github-logo.png)
https://github.com/Izaron/Platformer
Благодарности
Автору этого топика
Авторам SFML
Авторам Box2D
Авторам TinyXml