- Класс
sf::Vector2<T>
- Операции с векторами
- Определение длины вектора (расстояний между объектами)
- Нормализация вектора (единичный вектор)
- Преобразования
Radians <=> Degree
- Поворот спрайта с помощью вектора направления и
tan2
- Разработка проекта для проверки функций
- Раздельная компиляция
- Задания для самостоятельной работы
Класс sf::Vector2<T>
На этом уроке мы подробно поговорим о таком важном понятии линейной алгебры как вектор. Параллельно мы будем составлять функции, которые будут использоваться нами при работе с векторами, в том числе, в качестве методов классов. Напоследок, мы разделим наш проект на несколько файлов. Вы уже поняли, что функции могут сделать программу более читабельной и лаконичной, если вынести некоторые часто выполняемые задачи в отдельные блоки и вызывать их в программе по мере необходимости. Подробнее о функциях вы можете прочитать здесь: §13 Функции не возвращающие значение. (Процедуры) и здесь: §14 Функции возвращающие значение.
Для работы с векторами в SFML используется класс Vector2
(в трехмерном пространстве Vector3
). Шаблонный класс Vector2
предназначен для манипулирования 2-мерными векторами. Это простой класс, который определяет математический вектор с двумя координатами (x
и y
). Его можно использовать для представления всего, что имеет два измерения: размер, точку, скорость и т. п. Если Vector2
представляет точку, то это можно представить как вектор, который исходит из начала координат (точки A
и B
на Рис. 1). Параметр шаблона T
является типом координат. Это может быть любой тип, который поддерживает арифметические операции (+
, -
, /
, *
) и сравнения (==
,!=
). Чаще всего – это int
или float
.
Класс sf::Vector2
имеет небольшой и простой интерфейс. К его элементам x
и y
можно обращаться напрямую и он не содержит математических функций.
Операции с векторами
Чтобы построить вектор (А
, Рис. 1) по двум координатам x = 3
и y = 2
необходимо передать значения в конструктор в качестве аргументов:
sf::Vector2f А(3.f, 2.f); sf::Vector2f B(1.f, 3.f);
Перегруженная версия конструктора позволяет в качестве аргумента принимать другой объект этого класса:
sf::Vector2<T>::Vector2(const Vector2<U> &vector)
При этом тип U
должен конвертироваться в тип T
.
Для изменения координаты необходимо обратиться к соответствующему полю:
В.x = 1.f;
Аналогично для копирования координаты:
float y_c = B.y;
Над векторами можно производить арифметические операции по правилам линейной алгебры. Однако, для нас важны не только и не сколько векторы, сколько их проекции на оси координат, что дает нам возможность управлять объектами в двумерном пространстве. На Рис. 1 изображены два вектора A
и B
. Проекции вектора A: Xa
и Ya
. Проекция вектора AB
: |Xa - Xb|
и |Ya - Yb|
(что такое вектор AB см. ниже). Но при этом, правила для арифметических операций, как сказано ранее, уже заложены (инкапсулированы) в классах, так что можно не обращаться к компонентам векторов (например A.x
или A.y
), а выполнять операции непосредственно, как показано в примерах ниже.
Умножение вектора на число:
sf::Vector2f S = A * 5.f;
Получение третьего вектора при сложение двух других:
sf::Vector2f C = A + B;
В задаче, которую мы будем решать ниже, важно не сложение двух векторов, а их разность (A - B
, рис. 1). Разность двух векторов A
и B
образует третий вектор АВ
, который определяет в каком направлении находится вектор B
(иначе говоря, объект B
).
Очень многие классы (SFML) используют функции для работы с координатами. Эти функции получают или возвращают объект класса Vector2
. Если мы получаем или устанавливаем координаты курсора (например, при нажатие на клавишу мыши), то используется Vector2i
(целочисленные координаты):
sf::Vector2i pos = sf::Mouse::getPosition(window); sf::Mouse::setPosition(sf::Vector2i(100, 200), window);
Поскольку функции cmath
работают с действительными числами, то эти координаты разумно преобразовать из целочисленных в действительные (float
):
sf::Vector2f posf = sf::window.mapPixelToCoords(pos);
Но, обычно, ввиду неявного приведения типов, большой необходимости в этом нет.
cmath
возвращают тип double, если аргумент целочисленный. Для работы с типом float
используется постфикс f
. Например, sqrtf
, cosf
.Что касается классов sf::Sprite
, sf::Shape
(включая дочерние), то они используют тип Vector2f
. Например:
sprite.setPosition(sf::Vector2f(100, 200)); sf::Vector2f pos = sprite.getPosition();
Определение длины вектора (расстояний между объектами)
Первая функция , которую мы разработаем на этом уроке – это функция определяющая длину вектора образованного двумя векторами from
и to
. Параметры функции (distance
) – два объекта Vector2f
. В теле функции получаем разность векторов (по существу, проекции на координатные оси). Далее (учитывая, что это катеты прямоугольного треугольника) вычисляем и возвращаем гипотенузу (длину вектора АВ на Рис. 1).
float distance(sf::Vector2f from, sf::Vector2f to) { sf::Vector2f XY = from - to; return hypot(XY.x, XY.y); }
Нормализация вектора (единичный вектор)
Нормализация – это превращение исходного вектора в единичный вектор, т. е. в вектор, длина которого равна 1
. Нормализация имеет большое значение. Она используется, прежде всего, для определения вектора скорости и направления, но также упрощает решение многих задач. Как известно, единица при умножении на число не меняет результата, но вектор, помимо длины, имеет ещё и направление, а это важно. Для нормализации вектора необходимо каждую компоненту координат разделить на длину вектора. Функция norm
возвращает объект класса Vector2
– нормализованный исходный вектор (принимаемый как параметр).
sf::Vector2f norm(sf::Vector2f vec) { float d = hypot(vec.x, vec.y); float norm_x = vec.x / d; float norm_y = vec.y / d; return sf::Vector2f(norm_x, norm_y); }
В задаче, решаемой на этом уроке, нормализация используется для определения вектора направления (см. программу sf.5.1 стр. 108).
Преобразования Radians <=> Degree
В программах часто приходится переводить из одной угловой меры в другую, так как функции SFML (например, setRotation()
) принимают аргумент в градусной мере, а функции библиотеки cmath
принимают и возвращают радианы. В системе СИ радиан (русское: рад, международное: rad) — это угол, соответствующий дуге, длина которой равна её радиусу. Ниже приводятся функции составленные на основании того факта, что a[°] = α[рад] × (180° / π)
, а α[рад] = a[°] × (π / 180°)
.
// Из рад. в градусы: float RadToDeg(float rad) { return rad * 180 / M_PI; } // Из градусов в рад.: float DegToRad(float deg) { return deg * M_PI / 180; }
Поворот спрайта с помощью вектора направления и tan2
Для того, чтобы повернуть спрайт в направлении какого-либо объекта или координаты события (в нашем случае это окружность, появляющаяся в случайных координатах) необходимо определить угол, который будет вычисляться с помощью проекций векторов на оси координат. Как мы уже сказали – это катеты. Следовательно, тригонометрическая функция (библиотеки cmath
), которую мы будем использовать – это арктангенс (atan
), но в её специальной реализации atan2
с двумя аргументами. Фактически atan2
исправляет определение угла по всем координатным четвертям (в отличие от tan
, который принимает только один аргумент) и, таким образом, может использоваться как компонент (получения угла Theta
) при переходе от прямоугольной системы координат к полярной. При этом направление на Е (восток) это начало отсчета 0
, а направление увеличения угла – по часовой стрелке (к югу). Нам удобнее было поменять точку отсчета и начинать отсчет с севера (N). Однако, ввиду особенности реализации функции tan2
, во II четверти будут возвращаться отрицательные углы. Для поворота это не имеет значения, но для вывода в окне имеет. Это следует поправить.
Непосредственно перед вычислением угла нужно определить разность векторов, т. е. вектор направления. Такая функция может быть реализована следующим образом:
float rot(Vector2f from, Vector2f to, char c) { Vector2f vec = to - from; float rotat = RadToDeg(atan2(vec.y, vec.x)); switch (c) { case 'n' : rotat += 90; break; case 'e' : ; break; case 's' : rotat -= 90; break; case 'w' : rotat -= 180; break; } return rotat; }
Символ c
обозначает какая сторона (“горизонта”) будет являться точкой отсчета угла.
Разработка проекта для проверки функций
Идея программы в следующем. В центре окна находится компас со стрелкой. В правом верхнем углу выводится текст – угол положения синего кружка (по умолчанию он находится сверху, стрелка указывает на него в позиции 0 градусов, точно на север). После нажатия клавиши пробел (Space
) синий круг перемещается в новую случайную позицию. Стрелка указывает азимут положения объекта, т.е. всегда указывает на него. В правом верхнем углу выводится новое значение азимута – значение угла в градусах. Если нажать клавишу Up
(стрелка вверх), то из центра компаса в направлении синего шара отправляется снаряд (кружок зеленого цвета), который в момент удара окрашивает шар тоже в зеленый цвет, при этом снаряд возвращается в исходную позицию в центре. Клавиша Delete
сбрасывает все настройки по умолчанию.
Для того, чтобы появилась возможность вывода текста в окне программы, необходимо использовать классы Font
и Text
. Объект класса Font
определяет какой будет использован шрифт и его местоположение. Непосредственно текст в программе выводится с помощью метода setString()
класса Text
. Он поддерживает функцию преобразования класса string
– to_string()
, преобразующую число в строку.
Набор свободных шрифтов DejaVu можно получить на нашем сайте по ссылке.
Программа sf.5.1
#include <SFML/Graphics.hpp> #include <cmath> #include <random> #include <chrono> #include <string> using namespace sf; using namespace std; using namespace chrono; float Rnd(float, float); float distance(Vector2f, Vector2f); float RadToDeg(float); Vector2f norm(Vector2f); float rot(Vector2f, Vector2f, char); int main() { int W = 800; int H = 600; RenderWindow window(VideoMode(W, H), "lesson-5-1"); // Настройки вывода текста Font font; if (!font.loadFromFile("./font/DejaVuSans.ttf")) return -1; Text text("0.0", font); text.setCharacterSize(30); text.setStyle(Text::Bold); text.setFillColor(Color::Black); text.setPosition(600, 50); // Текстура компаса Texture texture; if (!texture.loadFromFile("./pic/compass.png")) return -1; Sprite sprite; sprite.setTexture(texture); sprite.setPosition(W/2, H/2); sprite.setOrigin(250, 250); CircleShape Cir; // Окружность в центре Cir.setFillColor(Color(255, 0, 0)); Cir.setRadius(12.); Cir.setOrigin(12., 12.); Cir.setPosition(W/2, H/2); CircleShape Cir2; // Окружность-снаряд Cir2.setFillColor(Color(0, 255, 255)); Cir2.setRadius(13.); Cir2.setOrigin(13., 13.); Cir2.setPosition(W/2, H/2); Vector2f Cir2_pos; // Стрелка RectangleShape strelka(Vector2f(6, 180)); strelka.setOrigin(3, 150); strelka.setPosition(W/2, H/2); strelka.setFillColor(Color(255, 0, 0)); Vector2f strelka_pos = strelka.getPosition(); float Theta {}; CircleShape CirRnd; // Окружность в случайной позиции CirRnd.setFillColor(Color(0, 0, 255)); CirRnd.setRadius(20.); CirRnd.setOrigin(20., 20.); CirRnd.setPosition(W/2, 25.); Vector2f CirRnd_pos; Clock clock; float speed = 1000.f; float dX {}, dY {}; Vector2f vec; bool f = false; while (window.isOpen()) { float delta = clock.getElapsedTime().asMicroseconds(); clock.restart(); delta = delta / speed; 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::Space) { CirRnd.setFillColor(Color(0, 0, 255)); float X = Rnd(20., 580.); float Y = Rnd(20., 580.); CirRnd.setPosition(X, Y); CirRnd_pos = CirRnd.getPosition(); // Поворот стрелки; Theta = rot(strelka_pos, CirRnd_pos, 'n'); strelka.setRotation(Theta); // Исправляем отрицательные углы if (Theta < 0) Theta = 360 + Theta; // Выводим значение угла text.setString(to_string(Theta)); } // Если нажата клавиша "вверх", то определяем // направление выстрела if (event.key.code == Keyboard::Up) { Cir2_pos = Cir2.getPosition(); CirRnd_pos = CirRnd.getPosition(); vec = norm(CirRnd_pos - Cir2_pos); dX = Cir2_pos.x; dY = Cir2_pos.y; f = true; } // Если нажата клавиша Delete, то // сбрасываем все по умолчанию if (event.key.code == Keyboard::Delete) { Cir2.setPosition(W/2, H/2); strelka.setRotation(0); text.setString("0.0"); CirRnd.setFillColor(Color(0, 0, 255)); CirRnd.setPosition(W/2, 25.); f = false; } } } Cir2_pos = Cir2.getPosition(); // Делаем выстрел, если клавиша Up была нажата if (f && distance(Cir2_pos, CirRnd_pos) > 2) { dX += delta * vec.x; dY += delta * vec.y; Cir2.setPosition(dX, dY); } // Если наш снаряд достиг цели, то... if (CirRnd.getGlobalBounds().contains(Cir2_pos.x, Cir2_pos.y)) { // перекрашиваем синий шар в зеленый цвет CirRnd.setFillColor(Color::Green); // отменяем стрельбу f = false; // возвращаем снаряд на место Cir2.setPosition(W/2, H/2); } window.RenderTarget::clear(Color::White); window.draw(sprite); window.draw(Cir2); window.draw(Cir); window.draw(strelka); window.draw(CirRnd); window.draw(text); window.display(); } return 0; } // Функции, которые мы использовали // Получения случайного действительного числа float Rnd(float z1, float z2) { long seed = system_clock::now().time_since_epoch().count(); static default_random_engine rnd(seed); static uniform_real_distribution<float> D(z1, z2); return D(rnd); } // Определение расстояния float distance(sf::Vector2f from, sf::Vector2f to) { sf::Vector2f XY = from - to; return hypot(XY.x, XY.y); } // Перевод радианы в градусы float RadToDeg(float rad) { return rad * 180 / M_PI; } // Нормализация вектора (получение единичного вектора) Vector2f norm(Vector2f vec) { float d = hypot(vec.x, vec.y); float norm_x = vec.x / d; float norm_y = vec.y / d; return Vector2f(norm_x, norm_y); } // Поворот from в сторону to. Зависит от RadToDeg float rot(Vector2f from, Vector2f to, char c) { // Определяем вектор поворота Vector2f vec = to - from; float rotat = RadToDeg(atan2(vec.y, vec.x)); switch (c) { // Выбираем точку отсчета case 'n' : rotat += 90; break; case 'e' : ; break; case 's' : rotat -= 90; break; case 'w' : rotat -= 180; break; } return rotat; }
Текстура компаса
Новые функции:
-
setRotation()
– глобальное вращение от некоторой начальной позиции. Каждое новое обращение перезаписывает предыдущий поворот (т. е. – это будет новый поворот от той же начальной позиции, в отличие отrotate()
, который осуществляет поворот от текущей позиции). -
getGlobalBounds()
– запрашивает глобальный ограничивающий прямоугольник объекта (границы спрайта) в глобальных координатах. Чтобы убедиться, что объект находится в пределах границ спрайта используется в сочетании с логической функциейcontains(x, y)
:
sprite1.getGlobalBounds().contains(sprite2_pos.x, sprite2_pos.y)
Раздельная компиляция
Код программы получился довольно громоздкий. Использовать его для продолжения разработки стало неудобным. К тому же разработанные нами функции потребуются для решения различных задач в других проектах. Поэтому мы разобьем его на несколько файлов. Как это сделать см.: §10 Функции. Раздельная компиляция. Наш проект после реорганизации будет содержать следующие файлы (исходный текст спрятан под спойлер):
- lesson-sfml-5-1.cpp – главный файл проекта
- lesson-sfml.h – заголовочный файл
- lesson-sfml.cpp – сборник функций
Задания для самостоятельной работы
- Дана карта – клетчатое поле размером 1000×1000 px на прозрачной основе. Ширина и высота ячейки 50×50, т. е. всего 20×20 = 400 клеток. Разработайте на этом поле игру типа игры в фишки. Условие придуймайте сами. Фишка должна перемещаться по клеткам на определенное количество шагов (пермещения только по горизонтали и вертикали). Предусмотреть мины, отнимающие очки или призовые позиции, добавляющие очки. Значения набранных очков выводить в правом верхнем углу. Окрасить траектории и прочие элементы карты только средствами SFML. Версии сеток:
Белая
Черная