Создаем платформер за 30 минут

в 15:14, , рубрики: c++, cplusplus, game development, Gamedev, tiled, метки: , , ,

Здравствуйте! Сегодня мы будем писать платформер, используя C++, Box2D и SFML, а также редактор 2D карт для игр Tiled Map Editor

image

Вот результат (карта создавалась 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

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

Тайлы

Затем идем в «Карта->Новый набор тайлов...», загружаем наш тайлсет

image

В итоге у вас получится что-то вроде этого

image

В чем смысл слоев тайлов?
Почти в каждой игре есть многослойные карты. Первый слой — земля (лед, чернозем, etc), второй слой — здания (казармы, форт, etc, причем фон прозрачен), третий — деревья (ель, пихта, etc, фон тоже прозрачен). То есть рисуется сначала первый слой, поверх него накладывается второй слой, а потом уже третий.

Процесс создания слоев запечатлен на следующих 4 скриншотах

image
image
image

Список слоев

image

Объекты

Что такое объект в TME?
Объект имеет свое имя, тип, а также параметры со значениями.
За объекты отвечает эта панель

image

Вы вполне можете узнать, что делает каждая из кнопок, сами.
Теперь попробуем создать объект.
Удаляем слой «Колобоша», вместо него создаем слой объектов, допустим, с тем же названием «Колобоша». Выбираем «Вставить тайл-объект» из панели для объектов (или можете выбрать любую фигуру — 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

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

Когда вы наиграетесь с объектами, наступит пора Box2D

Коробки-коробочки

Мы хотим создать 3D-экшон платформер, суть такова…
На карте расположены объекты — с названиями player — игрок, enemy — враг, block — блок, money — монетки.
Мы загружаем игрока, заставляем его повиноваться нажатиям клавиши и силе Ньютона.
Враги ходят туда-сюда, отталкивают слишком близко находящегося игрока и погибают, если игрок прыгает на них
Блоки закрепляются «в воздухе» как статичные объекты, на них игрок может прыгать
Монеты ничего не дают, просто исчезают при столкновении с игроком

Открываем main.h, стираем то, что там было написано, и пишем

#include "level.h"
#include <Box2DBox2D.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

Исходники

image
https://github.com/Izaron/Platformer

Вопросы пишите тут или в ЛС, либо, если вам не повезло быть зарегистрированным на Хабре — на izarizar@mail.ru

Автор: Izaron

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js