📜 ⬆️ ⬇️

Simple SFML game

We will make a “tag” game in C ++ using the SFML library. Fifteen is a well-known puzzle that looks like this:


On the playing field of 4x4 size there are 15 randomly placed dice with numbers from 1 to 15 and one free space. It is possible to move dies only one by one and only on an empty seat. The goal of the game is to align the dice on the playing field in the order corresponding to their numbers.

So, let's begin.

We start the Visual Studio and we create the new empty project. You can call it what you want, I called "15". In this project, create a new main.cpp file and an empty main function:
')
// main.cpp int main() { return 0; } 

Next, download the SFML library from sfml-dev.org and unpack it. In the unpacked library we have the necessary folders: include , lib and bin . In the project properties in the C / C ++ section of Additional Include Directories, add the path to the include folder:


In the Linker section of the Additional Library Directories, add the path to the lib folder:


And from the bin directory you need to copy the DLL-files and put them in the directory with the exe-file of our project:


In addition, in the Linker section, in the Input section, you need to add used library files to Additional Dependencies . In our case, it is enough to add three files: sfml-system-d.lib, sfml-window-d.lib and sfml-graphics-d.lib:


The symbol -d in the file name means that it is a debug version and should be used in the Debug configuration. In the release version settings, you will need to specify files without the -d symbol in the name.

A good guide for connecting the SFML library to a Visual Studio project is on the library's website .

Now let's try to involve the library in our project. Create a window and run the event loop:

main.cpp
 // main.cpp #include <SFML/Graphics.hpp> int main() { //    600  600    60    sf::RenderWindow window(sf::VideoMode(600, 600), "15"); window.setFramerateLimit(60); sf::Event event; while (window.isOpen()) { while (window.pollEvent(event)) { if (event.type == sf::Event::Closed) window.close(); if (event.type == sf::Event::KeyPressed) { //    -    if (event.key.code == sf::Keyboard::Escape) window.close(); } } //      window.clear(); window.display(); } return 0; } 


The result will be a square window measuring 600 by 600 pixels with a black background:


The window can be closed in the usual way with the mouse, or via the Esc key. Keyboard keystroke is also included in the message loop.

Before we get down to business, we need some kind of font to display text on the screen. For example, I took the font TrueType Calibri.

Now we can start making our game.

Create a new Game class:


The class will be responsible for the operation of the game and for drawing the playing field. To do this, we will inherit our class from the Drawable and Transformable SFML libraries.

So, we begin to describe our class.

Game.h
 #pragma once #include <SFML/Graphics.hpp> const int SIZE = 4; //      const int ARRAY_SIZE = SIZE * SIZE; //   const int FIELD_SIZE = 500; //      const int CELL_SIZE = 120; //     enum class Direction { Left = 0, Right = 1, Up = 2, Down = 3 }; class Game : public sf::Drawable, public sf::Transformable { protected: int elements[ARRAY_SIZE]; int empty_index; bool solved; sf::Font font; public: Game(); void Init(); bool Check(); void Move(Direction direction); public: virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const; }; 


First of all, connect the Graphics library:

 #include <SFML/Graphics.hpp> 

Immediately declare some constants required for the game:

 const int SIZE = 4; //      const int ARRAY_SIZE = SIZE * SIZE; //    const int FIELD_SIZE = 500; //      const int CELL_SIZE = 120; //     

We also declare our type enum, which determines the direction of movement of the plate:

 enum class Direction { Left = 0, Right = 1, Up = 2, Down = 3 }; 

Well, finally the class itself:

 class Game : public sf::Drawable, public sf::Transformable { protected: int elements[ARRAY_SIZE]; int empty_index; bool solved; sf::Font font; public: Game(); void Init(); bool Check(); void Move(Direction direction); public: virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const; }; 

The most important thing that we have in it is an array of elements containing integer values ​​corresponding to the state of the playing field. The elements in the array correspond to the elements of the playing field from left to right, from top to bottom, that is, the first 4 elements of the array correspond to the first line of the field, the second 4 elements to the second line, etc.

Then, two variables that will be calculated each turn are empty_index (the index in the array corresponding to the free cell) and solved (a sign that the puzzle has been solved).

In addition, the class has a font variable that defines the font that will be used when displaying text in a window.

Now we will write the implementation of the methods of our class.

Game.cpp
 #include "Game.h" Game::Game() { //      font.loadFromFile("calibri.ttf"); Init(); } void Game::Init() { //    for (int i = 0; i < ARRAY_SIZE - 1; i++) elements[i] = i + 1; //        empty_index = ARRAY_SIZE - 1; elements[empty_index] = 0; //     = 0 solved = true; } bool Game::Check() { //    for (unsigned int i = 0; i < ARRAY_SIZE; i++) { if (elements[i] > 0 && elements[i] != i + 1) return false; } return true; } void Game::Move(Direction direction) { //       int col = empty_index % SIZE; int row = empty_index / SIZE; //          int move_index = -1; if (direction == Direction::Left && col < (SIZE - 1)) move_index = empty_index + 1; if (direction == Direction::Right && col > 0) move_index = empty_index - 1; if (direction == Direction::Up && row < (SIZE - 1)) move_index = empty_index + SIZE; if (direction == Direction::Down && row > 0) move_index = empty_index - SIZE; //      if (empty_index >= 0 && move_index >= 0) { int tmp = elements[empty_index]; elements[empty_index] = elements[move_index]; elements[move_index] = tmp; empty_index = move_index; } solved = Check(); } void Game::draw(sf::RenderTarget& target, sf::RenderStates states) const { states.transform *= getTransform(); sf::Color color = sf::Color(200, 100, 200); //     sf::RectangleShape shape(sf::Vector2f(FIELD_SIZE, FIELD_SIZE)); shape.setOutlineThickness(2.f); shape.setOutlineColor(color); shape.setFillColor(sf::Color::Transparent); target.draw(shape, states); //       shape.setSize(sf::Vector2f(CELL_SIZE - 2, CELL_SIZE - 2)); shape.setOutlineThickness(2.f); shape.setOutlineColor(color); shape.setFillColor(sf::Color::Transparent); //        sf::Text text("", font, 52); for (unsigned int i = 0; i < ARRAY_SIZE; i++) { shape.setOutlineColor(color); text.setFillColor(color); text.setString(std::to_string(elements[i])); if (solved) { //      shape.setOutlineColor(sf::Color::Cyan); text.setFillColor(sf::Color::Cyan); } else if (elements[i] == i + 1) { //        text.setFillColor(sf::Color::Green); } //   ,   if (elements[i] > 0) { //      sf::Vector2f position(i % SIZE * CELL_SIZE + 10.f, i / SIZE * CELL_SIZE + 10.f); shape.setPosition(position); //     text.setPosition(position.x + 30.f + (elements[i] < 10 ? 15.f : 0.f), position.y + 25.f); target.draw(shape, states); target.draw(text, states); } } } 


The class constructor loads the font from the external file and calls the game initialization method:

 Game::Game() { //      font.loadFromFile("calibri.ttf"); Init(); } 

The game initialization method fills the array with the elements in the correct order and sets a sign of the solved puzzle:

 void Game::Init() { //    for (int i = 0; i < ARRAY_SIZE - 1; i++) elements[i] = i + 1; //   -     empty_index = ARRAY_SIZE - 1; elements[empty_index] = 0; //     = 0 solved = true; } 

Yes, initially our game will be initialized as solved, and before the game starts we will mix the dice using random moves.

The following method checks if the puzzle is solved and returns the result of the check:

 bool Game::Check() { //    for (unsigned int i = 0; i < ARRAY_SIZE; i++) { if (elements[i] > 0 && elements[i] != i + 1) return false; } return true; } 

And finally, the method that implements the movement of the plate in the game:

 void Game::Move(Direction direction) { //       int col = empty_index % SIZE; int row = empty_index / SIZE; //          int move_index = -1; if (direction == Direction::Left && col < (SIZE - 1)) move_index = empty_index + 1; if (direction == Direction::Right && col > 0) move_index = empty_index - 1; if (direction == Direction::Up && row < (SIZE - 1)) move_index = empty_index + SIZE; if (direction == Direction::Down && row > 0) move_index = empty_index - SIZE; //      if (empty_index >= 0 && move_index >= 0) { int tmp = elements[empty_index]; elements[empty_index] = elements[move_index]; elements[move_index] = tmp; empty_index = move_index; } solved = Check(); } 

The last class method is the method that renders the playing field:

draw
 void Game::draw(sf::RenderTarget& target, sf::RenderStates states) const { states.transform *= getTransform(); sf::Color color = sf::Color(200, 100, 200); //     sf::RectangleShape shape(sf::Vector2f(FIELD_SIZE, FIELD_SIZE)); shape.setOutlineThickness(2.f); shape.setOutlineColor(color); shape.setFillColor(sf::Color::Transparent); target.draw(shape, states); //       shape.setSize(sf::Vector2f(CELL_SIZE - 2, CELL_SIZE - 2)); shape.setOutlineThickness(2.f); shape.setOutlineColor(color); shape.setFillColor(sf::Color::Transparent); //        sf::Text text("", font, 52); for (unsigned int i = 0; i < ARRAY_SIZE; i++) { shape.setOutlineColor(color); text.setFillColor(color); text.setString(std::to_string(elements[i])); if (solved) { //      shape.setOutlineColor(sf::Color::Cyan); text.setFillColor(sf::Color::Cyan); } else if (elements[i] == i + 1) { //        text.setFillColor(sf::Color::Green); } //   ,   if (elements[i] > 0) { //      sf::Vector2f position(i % SIZE * CELL_SIZE + 10.f, i / SIZE * CELL_SIZE + 10.f); shape.setPosition(position); //     text.setPosition(position.x + 30.f + (elements[i] < 10 ? 15.f : 0.f), position.y + 25.f); //    target.draw(shape, states); //    target.draw(text, states); } } } 


In the drawing method, first of all, we apply coordinate transformation by multiplying by the transformation matrix. This is necessary in order to be able to set the coordinates of our playing field. Next, using the SFML RectangleShape objects, draw the playing field frames and the frames of each plate in the game. We also draw text with the plate number on the plates. In addition, if the puzzle is solved, then the color of the dice is made different.

It is time to return to the main function:

main.cpp
 // main.cpp #include <SFML/Graphics.hpp> #include "Game.h" int main() { //    600  600    60    sf::RenderWindow window(sf::VideoMode(600, 600), "15"); window.setFramerateLimit(60); sf::Font font; font.loadFromFile("calibri.ttf"); //     sf::Text text("F2 - New Game / Esc - Exit / Arrow Keys - Move Tile", font, 20); text.setFillColor(sf::Color::Cyan); text.setPosition(5.f, 5.f); //    Game game; game.setPosition(50.f, 50.f); sf::Event event; int move_counter = 0; //       while (window.isOpen()) { while (window.pollEvent(event)) { if (event.type == sf::Event::Closed) window.close(); if (event.type == sf::Event::KeyPressed) { //    -    if (event.key.code == sf::Keyboard::Escape) window.close(); if (event.key.code == sf::Keyboard::Left) game.Move(Direction::Left); if (event.key.code == sf::Keyboard::Right) game.Move(Direction::Right); if (event.key.code == sf::Keyboard::Up) game.Move(Direction::Up); if (event.key.code == sf::Keyboard::Down) game.Move(Direction::Down); //   if (event.key.code == sf::Keyboard::F2) { game.Init(); move_counter = 100; } } } //     ,    if (move_counter-- > 0) game.Move((Direction)(rand() % 4)); //      window.clear(); window.draw(game); window.draw(text); window.display(); } return 0; } 


First we load the font and create a Text object to display a line of text with key assignment. Next, create our game object and set the field position to a point with coordinates (50.50) - this is how we indent from the edge of the window.

I decided to control the game through the keyboard, so for every keystroke of the arrows we call the Move object on the game object - to move the plate in the appropriate direction.

Pressing the F2 key is the beginning of a new game, so in the event handler for this event, we re-initialize the game (which will lead to the placement of the dice in its place), and also set the value of the move counter to 100. This counter is used further to execute moves in random directions, until will not be reset, and the dies do not mix. In this way, we will definitely get a solved puzzle state.

In general, that's all, compile, build, run:



In this article, I showed how you can quickly create a simple C ++ game using the SFML library. However, the architecture of the program itself is far from ideal. In the next article we will try to do something about it.

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


All Articles