📜 ⬆️ ⬇️

Create a platformer in 30 minutes

Hello! Today we will write a platformer using C ++, Box2D and SFML, as well as a 2D map editor for the Tiled Map Editor games.

image

Here is the result (the map was created 5 minutes + while shooting the game was slowed down + the screen was not so stretched - Bandicam defect):
')


Sources and exe - at the bottom of the article.

What where When?


Box2d

We will use this library to simulate physics in a platform (collision with blocks, gravity). It might not have been worth using this library for blocks alone, but you can't forbid living beautifully;)
Why choose Box2D? Because it is the most common and free physical library.

SFML

Why SFML? At first, I wanted to use the SDL library, but it is very limited in capabilities as compared to SFML, I would have to add a lot myself. Thanks to the SFML author for the time saved!
We use it to draw graphics.

Tiled map editor

What does the Tiled Map Editor do here?
Have you ever tried to create maps for games? I bet that your first card was something like that.

Hidden text
1111111 1000001 1001001 1000011 1000111 1111111 



This is a rather ineffective solution! It is much better to write something like a map editor, but in hindsight we understand that this is not done in 5 minutes, and the above “map” is completely.

Tiled Map Editor is one such map editor. It is good because the map created in this editor (consists of objects, tiles, their layers) can be saved in an XML-like .tmx file and then read it using a special C ++ library. But first things first.

Creating a map


We download TME from the official site
Create a new map "File-> Create ..."

image

The orientation should be orthogonal (if you are not doing an isometric platform), and the format of the XML layer, we will read this format
By the way, neither the format of the layer, nor the size of the tiles can be changed in the created map.

Tiles

Then go to “Map-> New Tile Set ...”, load our tileset:

image

You end up with something like this:

image

What is the meaning of layers of tiles?
Almost every game has multi-layered cards. The first layer is earth (ice, black soil, etc), the second layer is buildings (barracks, fort, etc, the background is transparent), the third is trees (spruce, fir, etc, the background is also transparent). That is, the first layer is drawn first, a second layer is laid over it, and then the third one.

The process of creating layers is captured in the following 4 screenshots:

image
image
image

List of layers:

image

Objects

What is an object in TME?
The object has its own name, type, and parameters with values.
This panel is responsible for the objects.

image

You can easily find out what each button does, yourself.
Now try to create an object.
Delete the “Kolobosha” layer, instead create a layer of objects, for example, with the same name “Kolobosha”. Select "Insert Tile Object" from the panel for objects (or you can select any shape - Shape), click on the Kološi Tile and just put the object in some place.
Then we right-click on the object and click on "Object properties ...". Change the name of the object to Kolobosha.

Then save the card.

In general, there is nothing daunting in the map editors. It's time to move on to read the card.

Card reading


An excellent TinyXML library has been created for reading XML files. Download its sources.

Create a Visual Studio project. Connect the TinyXML files (or just push all these files into the project, except for xmltest.cpp :))
Now we include the SFML libs and libs in “Project-> Properties”. If you do not know how to do this - welcome to Google

Create Level.h for maps

 #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> 

This is the beginning of the file.

Next comes the structure of the object.
 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; }; 

Let's sort it out.
As already mentioned, in TME each object can have parameters. The parameters are taken from the XML file, are recorded in the properties, and then they can be obtained from any of the first three functions. name - the name of the object, type - its type, rect - a rectangle describing the object. Finally, sprite — a sprite (image) —the part of the tileset taken for the object. Sprite may not be.

Now comes the structure of the layer - it is very simple
 struct Layer { int opacity; std::vector<sf::Sprite> tiles; }; 


There is transparency in the layer (yes, yes, we can make translucent layers!) And a list of tiles.

Next is the Level class:

 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 loads the map from the specified file. This is the heart of the Level class.
GetObject returns the first object with the specified name, GetObjects returns a list of objects with the specified name. Actually, in an amicable way, I should have used the type of the object, but it was more convenient for me to catch blocks and the player through the name, since in the editor the name is shown on top of the object, but the type is not.
Draw draws all tiles (not objects!), Taking an instance of RenderWindow.

Now create Level.cpp:

 #include "level.h" #include <iostream> #include "tinyxml.h" 


First we process the structure of the objects.
 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]; } 


For the Layer implementation is not needed, go to 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; } 



The remaining Level functions:

 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]); } 


With Level.h over!

Test it.
Create main.cpp and write:

 #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; } 


The card can look like anything!

You can play with objects:

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; } 


Result:

image

When you play around with objects, it's time for Box2D:

Boxes-boxes


We want to create a 3D action platform game , the essence is ...
On the map there are objects - with the names player - player, enemy - enemy, block - block, money - coins.
We load the player, make him obey the keystrokes and force of Newton.
Enemies go back and forth, pushing a player too close and die if the player jumps on them
The blocks are fixed "in the air" as static objects, the player can jump on them
Coins give nothing, just disappear when confronted with a player

Open main.h, erase what was written there, and write:

 #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; 


Here we have level.h and Box2D.h. iostream is needed for output to the console, random is for generating the direction of the enemy's movement.
Next come the player and the vectors, each enemy, coin, player relies on his Object and b2Body (body in Box2D).

Attention - the blocks do not rely on this, since they interact with the player only at the level of Box2D physics, and not in the game logic.

Further:

 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)) is needed for randomness.

We load the map, create b2World, passing it gravity. By the way, gravity can come from any direction, and gravity from (0.10) acts more strongly (0.1). Then we take the tile size we need.

Next, create body blocks:

  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; 


The blocks are static bodies, they have no mass and hang in the air:

 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)); 


Here we set the position of the blocks. The fact is that if you simply specify a position the same as the object, an insidious error will be waiting for us.

 b2Body* body = world.CreateBody(&bodyDef); 


Create a block body in the world. Further, we do not work with the body (in the sense that we do not store it anywhere):

 b2PolygonShape shape; shape.SetAsBox(block[i].rect.width / 2, block[i].rect.height / 2); 


Each body has several shape - shapes. I will not analyze this topic in detail, since the blocks (and the rest of the bodies) lack just one rectangle.

 body->CreateFixture(&shape,1.0f); 


We associate the figure with the body.

Then we do the same with enemies, coins, and player, with minor differences:

  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; 


Means that the body can not rotate.

All bodies are created, it remains to initialize the graphics!

  sf::Vector2i screenSize(800, 600); sf::RenderWindow window; window.create(sf::VideoMode(screenSize.x, screenSize.y), "Game"); 


Well-understood code creates a window with the specified size and title:

  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)); 


Here we create a view (View) for the window.

Why is this necessary? In order to give the game a pixel style, we multiply the screen size by 2 using sf :: View and all the pictures are drawn 2 times higher and wider.

  while(window.isOpen()) { sf::Event evt; while(window.pollEvent(evt)) { switch(evt.type) { case sf::Event::Closed: window.close(); break; 


The window closes by pressing the red cross. This code was previously:

  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; 


It's more interesting here! We add speed to the player by pressing the WAD keys:

 world.Step(1.0f / 60.0f, 1, 1); 


Here we are updating the physical world of Box2D. The first argument takes the world update rate (once every 1/60 seconds), as well as the number of velocityIterations and positionIterations. The higher the value of the last two arguments, the more realistic is the physics of the game. Since we do not have any complex shapes, as in AngryBirds, but only rectangles, we need only once.

  for(b2ContactEdge* ce = playerBody->GetContactList(); ce; ce = ce->next) { b2Contact* c = ce->contact; 


Here we handle the collision of a player with other bodies:

  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); } 


Handling collision with coins.
If a coin collides with a player, it is simply destroyed and erased from the vectors:

  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); } 


If an enemy collides with a player, it is checked whether the enemy player is higher or not. If the player is higher than the enemy, he is erased, and the player jumps up.

If not, then the player rebounds from the enemy:

  else { int tmp = (playerBody->GetPosition().x < enemyBody[i]->GetPosition().x) ? -1 : 1; playerBody->SetLinearVelocity(b2Vec2(10.0f * tmp, 0.0f)); } } } 


The player moves to the right or left according to his current position relative to the enemy.

  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)); } } 


If the enemy's speed is 0, then he is given a speed again - he moves either to the right or to the left. Visually, it looks like a jerk movement.

  b2Vec2 pos = playerBody->GetPosition(); view.setCenter(pos.x + screenSize.x / 4, pos.y + screenSize.y / 4); window.setView(view); 


Work with graphics. Take the position of the player, change the center of the view and use our 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); 


Set the sprites of the player, coins and enemies positions obtained from 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(); 


We clear the windows, draw tiles of the map, then the player, coins and enemies, and then present the window.

  } return 0; } 


Done!

Sample map:

image

Sources


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

Thanks


To the author of this topic
SFML Authors
Box2D authors
To authors TinyXml

Source: https://habr.com/ru/post/199064/


All Articles