§6 Классы sf::Transform и sf::Transformable. Переходим на ООП

Знакомство с классами трансформации

Различные преобразования, такие как изменение позиции, масштабирование и вращение в SFML выполняют два класса sf::Transform и sf::Transformable. Оба класса имеют схожие методы, но класс sf::Transform является низкоуровневым, по сравнению с sf::Transformable. Это, с одной стороны, делает работу в данном классе более гибкой: можно легко объединить любой вид операции, например, перемещение, за которым следует вращение с последующим масштабированием. Но есть и другая сторона – как только был получен результат преобразования, то становится невозможным вернуться назад и, скажем, изменить только вращение без изменения позиции и масштабирования. Вернуться назад конечно же можно, но это потребует откат преобразований с извлечением исходных коэффициентов преобразований. Это утомительная операция, и она требует хранения всех отдельных компонентов конечного преобразования.
Для того, чтобы избавить разработчика от такой рутинной работы был написан класс sf::Transformable. Он инкапсулирует преобразования за простым пользовательским интерфейсом. Можно установить или получить любой из компонентов, не беспокоясь о других. Он также предоставляет составное преобразование (как sf::Transform) и поддерживает его в актуальном состоянии.
Рассмотрим как осуществляется составное преобразование на примере функции:

Transform& sf::Transform::rotate(float angle, const Vector2f ¢er)

Функция rotate класса Transform принимает два аргумента: угол поворота (angle) и координаты центра вращения (center). В результате можно “заставить” объект вращаться вокруг произвольного центра. Этой возможностью мы воспользовались в первой программе урока. В ней реализована модель Солнечной системы – вращение шести планет (Меркурий, Венера, Земля, Марс, Юпитер и Сатурн) вокруг общего центра (Солнца) по круговым орбитам. Модель не дает точные положения планет на конкретную эпоху, поскольку для реализации планетария потребовалось бы привлечения большого количества сложных формул, описывающих движении планет в поле гравитации. Таких программ довольно много, в том числе свободных, например, Stellarium, KStars и SkyChart для Linux.

Реализации компьютерных астрономических вычислений посвящена книга “Астрономия на персональном компьютере”. Монтенбрук, Пфлегер. СПБ.: Питер, 2002. Книга распространялась с CD-диском на котором были записаны исходники программ на языке C++ и базы данных (астрономические каталоги).

Поскольку наши вычисления будут неточны, то реальный масштаб системы не соблюдается. В этой программе не используется таймер. Анимация происходит за счет перерисовки окна на каждом шаге главного цикла программы. Радиус орбиты определяется следующим образом: R_pl = D * k, где D – расстояние в астрономических единицах (средний радиус Земной орбиты, см. здесь), а k – коэффициент (== 40) определяющий размер модели. Для вычисления угла поворота использовалась формула для определения угловой скорости: Omega = 2 * pi / T, где T – период обращения, выраженный в земных годах (см. здесь). Для уменьшения скорости вращения требуется использовать уменьшающий коэффициент. В результате, угол вычисляется следующим образом: angle = A * om, где om – коэффициент скорости вращения планет (== 0.1), а A – угловая скорость планеты (в рад / год).
Программа sf.6.1

#include <SFML/Graphics.hpp>
#include <array>
using namespace sf;
using namespace std;

int main() {
	int W = 900;
	int H = 900;
	RenderWindow window(VideoMode(W, H), "lesson-6-0");
	array<CircleShape, 6> ss;
	// Каждому объекту CircleShape должен соответствовать
	// свой объект Transform
	array<Transform, 6> T;
	// Параметры планет:
	//				Рад.    R     G     B   Расст.  Угл. ск.
	float data[6][6] {{2. , 255., 255., 255.,   .39, 26.2 },  // mer
					  {3. , 155., 222., 214.,   .72, 10.3 },  // ven
					  {3.2,  37., 131., 223.,  1.  ,  6.3 },  // ear
					  {2.5, 200.,  60.,  60.,  1.52,  3.5 },  // mar
					  {15., 194., 120.,  43.,  5.2 ,   .5 },  // sat
					  {10. , 194., 176.,  43., 10.  ,  .21}}; // jup
	// sun
	CircleShape sun;
	sun.setRadius(5);
	sun.setOrigin({5, 5});
	sun.setFillColor(Color::Yellow);
	sun.setPosition(W / 2.f, H / 2.f);

	const float k = 40.f; // расстояние от Солнца
	const float om = .1f; // скорость вращения
	int i = 0;
	// Создаем массив планет
	for (auto &w : ss) {
		float r = data[i][1];
		float g = data[i][2];
		float b = data[i][3];
		float R = data[i][0];
		float D = data[i][4];
		float R_pl = D * k;
		w.setRadius(R);
		w.setOrigin({R, R});
		w.setFillColor(Color(r, g, b));
		w.setPosition(W / 2.f, H / 2.f - R_pl);
		i++;
	}

	while (window.isOpen()) {
    	Event event;
        while (window.pollEvent(event)) {
            if (event.type == Event::Closed)
                window.close();

            if (event.type == Event::KeyPressed) {
            	if (event.key.code == Keyboard::Escape)
            		window.close();
            }
        }

		window.clear();
		window.draw(sun);
		for (int i = 0; i < 6; i++) {
			// Получаем угол
			float A = data[i][5];
			float angle = A * om;
			// Получаем трансформацию
			T[i].rotate(angle, { W / 2.f, H / 2.f });
			// 
			window.draw(ss[i], T[i]);
		}
        window.display();
	}
    return 0;
}

Объектно-ориентированный подход

До этого момента, при составлении программ, мы использовали процедурный стиль программирования, основанный на использовании функций. Следующую программу мы оформим в стиле объектно-ориентированного программирования. Этого стиля мы будем придерживаться и в дальнейшем (подразумевается, что вы уже знакомы с ООП)

Подробно с этой парадигмой вы можете познакомиться в курсе 11 класса здесь (в ближайшее время этот раздел будет пересмотрен и дополнен, так как вопрос освещается в нём не полностью). Если вам этого окажется мало, то воспользуйтесь списком литературы.

Начнем с создания главного файла. Назовем его по номеру программы.
Программа sf.6.2 (файл lesson-sfml-6-2.cpp)

#include <SFML/Graphics.hpp>
#include "lesson-sfml.h"

using namespace sf;

int main() {
	RenderWindow window(VideoMode(W, H), "lesson-6-2");
	Game game;
	while (window.isOpen()) {
		Event event;
        while (window.pollEvent(event)) {
            if (event.type == Event::Closed)
                window.close();
            if (event.type == Event::KeyPressed) {
            	if (event.key.code == Keyboard::Escape)
            		window.close();
            	if (event.key.code == Keyboard::Left) {
            		game.motion(Direction::Left);
            	}
            	if (event.key.code == Keyboard::Right) {
            		game.motion(Direction::Right);
            	}
            	if (event.key.code == Keyboard::Up) {
            		game.motion(Direction::Up);
            	}
            	if (event.key.code == Keyboard::Down) {
            		game.motion(Direction::Down);
            	}
            	if (event.key.code == Keyboard::F11) {
            		game.init();
            	}
            }
        }

        window.RenderTarget::clear(Color::White);
		window.draw(game);
		window.display();
	}
    return 0;
}

Как видите, в нем нет ничего примечательного, кроме определения объекта game абстрактного типа Game. Это название нашего класса в котором мы будет реализовывать механизм нашей игры (“движок”). В обработчиках событий вызываются методы класса. Методы – это те же функции, но реализованы они как члены (member, функции-члены) класса. Все константы мы вынесли в заголовочный файл. Там же сделаем объявления классов и функций. В результате его содержимое будет таковым:
Программа sf.6.2 (файл lesson-sfml.h)

#ifndef LESSON_SFML_H_
#define LESSON_SFML_H_
#include <SFML/Graphics.hpp>
#include <array>

const int W = 800;		// ширина окна (px)
const int H = 800;		// высота окна (px)
const int mas_w = 16;	// кол-во ячеек по ширине
const int mas_h = 16;	// кол-во ячеек по высоте
const float cel = 50;	// размер ячейки (px)
int Rnd(int, int);
float Rnd(float, float);
enum class Direction {
	Left = 0, Right = 1, Up = 2, Down = 3
};
class Game : public sf::Drawable, public sf::Transformable  {
protected:
	sf::Font font;
	sf::Texture setka; // текстура карты
	sf::Texture arr;   // текстура фишки
	std::array<std::array<int, mas_w>, mas_h> map_game;
	std::array<std::array<sf::RectangleShape, mas_w>, mas_h> rect;
public:
	Game(); // конструктор
	sf::Sprite area; // карта
	sf::Sprite sp;   // фишка
	void init();
	void motion(Direction direction);
	virtual void draw(sf::RenderTarget &target, sf::RenderStates states) const;
};
#endif /* LESSON_SFML_H_ */

Обратите внимание, что данный файл должен включаться во все файлы нашего проекта. В этой программе используются уже известные нам вещи. Много нового вы здесь не найдете. Сосредоточимся пока на общем оформлении задачи.
В чем суть программы? На клетчатом поле, размер которого 800х800 пикселей, передвигается фишка со стрелкой (стрелка всегда указывает направление движения). Размер ячейки 50×50. Фишка перемещается по ячейкам при нажатии клавиш Up, Down, Left, Right. Также задействованы клавиши Delete (закрытие окна) и F11 (новая игра). Стратегию игры я пока не раскрываю, но на карте присутствуют ячейки, окрашенные в серый цвет. Сегодня они не будут являться препятствием для игрока, но, в последствии, их нужно будет обходить. Как вы увидели, в заголовочном файле объявлены два массива. В массиве случайных чисел (map_game, он пока не задействован) будут сохраняться значения, которые будут использоваться для реализации стратегии. В текущей версии игры, фишка свободно перемещается по всей карте, в том числе, и за её пределы (коллизии мы разрешим на следующем уроке, но вы можете выполнить эту задачу самостоятельно, это не так сложно сделать). Реализация нашей игры находится в файле Game.cpp, в котором собраны все методы класса.
Программа sf.6.2 (файл Game.cpp)

#include <SFML/Graphics.hpp>
#include "lesson-sfml.h"

Game::Game() { // Конструктор
	font.loadFromFile("./font/DejaVuSans.ttf");
	setka.loadFromFile("./pic/setka_800x800x50x50.png");
	arr.loadFromFile("./pic/ar.png");
	init();
}

void Game::init() { // Инициализация
	for (int i = 0; i < mas_h; i++)
		for (int j = 0; j < mas_w; j++) {
			int rnd = Rnd(0, 5); // Призы на поле 5
			map_game[i][j] = rnd;
			rect[i][j].setPosition(cel * i, cel * j);
			rect[i][j].setSize(sf::Vector2f(cel, cel));
			if (rnd)
				rect[i][j].setFillColor(sf::Color::Transparent);
			else
				rect[i][j].setFillColor(sf::Color(0, 0, 0, 128));
		}
	area.setTexture(setka);
	sp.setTexture(arr); // фишка
	float gw = sp.getLocalBounds().width;
	float gh = sp.getLocalBounds().height;
	sp.setOrigin(gw / 2, gh - gw / 2);
	sp.setPosition(gw / 2, H - gw / 2);
	sp.setRotation(0);
}

void Game::motion(Direction direction) { // Динамика
	switch (direction) {
	case Direction::Up:
		sp.setRotation(0);
		sp.move(0, -1 * cel);
		break;
	case Direction::Down:
		sp.setRotation(180);
		sp.move(0, cel);
		break;
	case Direction::Left:
		sp.setRotation(-90);
		sp.move(-1 * cel, 0);
		break;
	case Direction::Right:
		sp.setRotation(90);
		sp.move(cel, 0);
		break;
	}
}
// Вывод окна
void Game::draw(sf::RenderTarget &target, sf::RenderStates states) const {
	sf::Transform t;
	states.transform *= t;
	target.draw(area, states);
	for (auto &i : rect)
		for (auto &j : i)
			target.draw(j, states);
	target.draw(sp, states);
}

Далее небольшая теория, чтобы понять как работает класс. В объявлении мы указали, что класс Game является наследником двух классов SFML: sf::Drawable и sf::Transformable. Назначение последнего мы выяснили. Класс sf::Drawable является абстрактным базовым классом для рендеринга объектов таких классов, как sf::Text, sf::Shape, sf::VertexArray с которыми мы уже знакомы. sf::Drawable – это очень простой класс, который позволяет отрисовывать объекты производных классов в окнах или текстурах определенных sf::RenderTarget.

sf::RenderTarget является базовым классом для всех целей рендеринга (sf::RenderWindow, sf::RenderTexture).

Все, что нам нужно сделать в нашем производном классе (Game), это переопределить виртуальную функцию рисования:

virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const

Наследование от sf::Drawable не является обязательным, но оно допускает приятный синтаксис «window.draw(object)», вместо «object.draw(window)», который характерен для других классов SFML.
Параметр этой функции states – объект класса sf::RenderState. Он определяет “состояние, моду” (states) рендеринга target (целевого объекта). Всего их существует четыре:

  • blend mode
  • transform
  • texture
  • shader
Мы выбрали states на основе трансформации (положения в пространстве). Здесь мы поставим последнюю точку в освещении класса sf::Transform. Это позволит нам понять набор инструкций в функции draw:

sf::Transform t;
states.transform *= t;

Означает это следующее. Вначале мы определяем объект класса sf::Transform (t). На самом деле, он представляет собой матрицу (преобразования, перехода) размерностью 3х3 (см. здесь). С помощью этой матрицы, собственно, и осуществляется вся трансформация (повороты, перемещения, масштабирования и т. д.). В данном случае, объект t получает матрицу по умолчанию. В результате умножения матриц мы переходим к пользовательскому пространству (инициализируем states). Теперь мы будем рисовать в “нашем” 2D-мире.
Вот как выглядить окно программы после запуска:

Файлы проекта:
Карта
Фишка
Новые функции:

  • getLocalBounds() – Возвращает (ограничивающий спрайт) прямоугольник, который находится в локальных координатах, это означает, что он игнорирует преобразования (перемещение, вращение, масштаб, …), которые применяются к сущности. Другими словами, эта функция возвращает границы объекта в системе координат объекта.

Задания для самостоятельной работы

  • Создайте пульсирующую окружность с помощью метода scale.
  • Создайте имитацию сонара (радара): в окне (темно-синий или темно-зеленый фон) случайным образом расставляются “невидимые” объекты. Когда (зеленый или голубой) луч сонара проходит над этими объектами они начинают светиться (ярко зеленым или синим цветом), тем самым, обнаруживая себя.
Ссылки
  1. C++ Programming Language. Object-Oriented Programming (OOP) in C++ (En)

Оцените материал
1 Звезда2 Звезды3 Звезды4 Звезды5 Звезд (1 оценок, среднее: 5,00 из 5)
Загрузка...

Print Friendly, PDF & Email

Comments are closed.