Простая игра на SFML

  • Tutorial
Будем делать игру «пятнашки» на языке C++ с использованием библиотеки SFML. Пятнашки — это широко известная головоломка, которая выглядит следующим образом:


На игровом поле размером 4х4 случайным образом расположены 15 плашек с номерами от 1 до 15 и одно свободное место. Передвигать плашки можно только по одной и только на свободное место. Целью игры является выстроение плашек на игровом поле в порядке, соответствующем их номерам.

Итак, начнем.

Запускаем Visual Studio и создаем новый пустой проект. Можете назвать его как хотите, я назвал «15». В этом проекте создаем новый файл main.cpp и пустую функцию main:

// main.cpp
int main()
{
   return 0;
}

Далее скачиваем библиотеку SFML с сайта sfml-dev.org и распаковываем ее. В распакованной библиотеке есть нужные нам папки: include, lib и bin. В свойствах проекта в разделе C/C++ в Additional Include Directories добавляем путь к папке include:


Там же в разделе Linker в Additional Library Directories добавляем путь к папке lib:


А из каталога bin нужно скопировать DLL-файлы и сложить их в каталог с exe-файлом нашего проекта:


Кроме того, в разделе Linker, в подразделе Input, нужно в Additional Dependencies добавить используемые файлы библиотеки. В нашем случае достаточно добавить три файла: sfml-system-d.lib, sfml-window-d.lib и sfml-graphics-d.lib:


Символ -d в названии файла означает, что это отладочная версия и должна использоваться в конфигурации Debug. В настройках релизной версии нужно будет задавать файлы без символа -d в названии.

Хорошая инструкция по подключению библиотеки SFML к проекту Visual Studio находится на сайте библиотеки.

Попробуем теперь задействовать библиотеку в нашем проекте. Создадим окно и запустим цикл обработки событий:

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


Результатом будет квадратное окно размером 600 на 600 пикселей с черным фоном:


Окно можно закрыть обычным способом мышью, либо через клавишу Esc. Обработчик нажатий клавиш клавиатуры также включен в цикл обработки сообщений.

Прежде чем приступить к делу, нам понадобится какой-нибудь шрифт для вывода текста на экран. Я для примера взял шрифт TrueType Calibri.

Теперь можем начинать делать свою игру.

Создаем новый класс Game:


Класс будет отвечать за работу игры и за отрисовку игрового поля. Для этого будем наследовать наш класс от классов Drawable и Transformable библиотеки SFML.

Итак, начинаем описывать наш класс

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


Первым делом подключаем библиотеку Graphics:

#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, определяющий направление перемещения плашки:

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

Самое главное что у нас в нем есть — это массив elements, содержащий целочисленные значения, соответствующие состоянию игрового поля. Элементы в массиве соответствуют элементам игрового поля слева направо, сверху вниз, то есть первые 4 элемента массива соответствуют первой строке поля, вторые 4 элемента — второй строке и т.д.

Далее две переменные, которые будут вычисляться каждый ход — это empty_index (индекс в массиве, соответствующий свободной ячейке) и solved (признак того, что головоломка решена).

Кроме того, в классе задана переменная font, определяющая шрифт, который будет использоваться при выводе текста в окне.

Теперь напишем реализацию методов нашего класса.

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


Конструктор класса загружает шрифт из внешнего файла и вызывает метод инициализации игры:

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

Последний метод класса — это метод, который отрисовывает игровое поле:

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


В методе отрисовки первым делом применяем трансформацию координат, путем умножения на матрицу трансформирования. Это нужно для того, чтобы можно было задавать координаты нашему игровому полю. Далее с помощью объектов RectangleShape библиотеки SFML, рисуем рамки игрового поля и рамки каждой плашки в игре. На плашках также еще отрисовываем текст с номером плашки. Кроме того, если головоломка решена, то цвет плашек делаем другим.

Настало время вернуться к функции main:

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


Вначале подгружаем шрифт и создаем объект Text для вывода на экран строки текста с назначенем клавиш. Далее создаем наш объект игры и устанавливаем позицию поля в точку с координатами (50,50) — так мы делаем отступ от края окна.

Управление игрой я решил делать через клавиатуру, так что на каждое нажатие клавиш стрелок вызываем у объекта игры метод Move — для перемещения плашки в соответствующем направлении.

Нажатие клавиши F2 — это начало новой игры, так что в обработчике этого события заново инициализируем игру (что приведет к расстановке плашек по своим местам), а также выставляем значение счетчика ходов равным 100. Этот счетчик используется дальше для выполнения ходов в случайных направлениях, пока не обнулится, а плашки не перемешаются. Таким образом мы точно получим решаемое состояние головоломки.

Вот в общем-то и все, компилируем, собираем, запускаем:



В этой статье я показал, как можно быстро создать простую игру на C++ с использованием библиотеки SFML. Однако архитектура самой программы далека от идеала. В следующей статье мы попробуем с этим что-нибудь сделать.
Поделиться публикацией

Комментарии 3

    0

    Помню как в детстве мне нравилась эта штука и как я бился над проблемой переставки 14 и 15 (интернетов тогда небыло) и только несколько лет назад узнал, что переставить их невозможно и у этой головоломки, как у пасьянса возможен плохой исход.

      0
      Вообще, об этом Перельман в своих «Занимательная алгебра» и «Живая математика» писал. :)
      0
      Вообще респект за такую статью, ибо я год назад пытался подключить библиотеку SFML, но не получилось. У тебя объяснение более доходчивое

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое