На написание данной статьи меня побудил ролик на YouTube, в котором автор за 4 минуты с нуля, используя графическую библиотеку SFML, создает рабочий прототип Тетриса. Всё это происходило в ускоренном воспроизведении под веселую вариацию оригинальной тетрис-мелодии «Коробейники». За всё это время автор не произнес ни слова, а сам ролик содержал несколько довольно неочевидных моментов, которые требовали пояснений. В связи с этим я решил взять на себя ответственность и восполнить данный пробел.
Первые шаги
Как вы уже знаете, практически любое SFML-приложение начинается с минимального каркаса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <SFML/Graphics.hpp> using namespace sf; int main() { RenderWindow window(VideoMode(320, 480), "The Game!"); // Главный цикл приложения: выполняется, пока открыто окно while (window.isOpen()) { // Обрабатываем события в цикле Event event; while (window.pollEvent(event)) { // Пользователь нажал на «крестик» и хочет закрыть окно? if (event.type == Event::Closed) // тогда закрываем его window.close(); } // Установка цвета фона - белый window.clear(Color::White); // Отрисовка окна window.display(); } return 0; } |
Далее мы создаем спрайт для представления различных элементов фигур в нашей игре и присваиваем ему файл текстуры — tiles.png. Сам файл текстуры у меня располагается по пути C:\dev\SFML_Tutorial\images\
. Текстура являет собой 8 разноцветных квадратов, горизонтально стоящих друг за другом. Размер каждого такого квадрата составляет 18×18 пикселей.
Ниже представлен код, который подгружает эту текстуру в программу:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#include <SFML/Graphics.hpp> using namespace sf; int main() { RenderWindow window(VideoMode(320, 480), "The Game!"); // Создание и загрузка текстуры Texture texture; texture.loadFromFile("C:\\dev\\SFML_Tutorial\\images\\tiles.png"); // Создание спрайта Sprite sprite(texture); // Главный цикл приложения: выполняется, пока открыто окно while (window.isOpen()) { // Обрабатываем события в цикле Event event; while (window.pollEvent(event)) { // Пользователь нажал на «крестик» и хочет закрыть окно? if (event.type == Event::Closed) // тогда закрываем его window.close(); } // Установка цвета фона - белый window.clear(Color::White); // Отрисовка спрайта window.draw(sprite); // Отрисовка окна window.display(); } return 0; } |
Если мы скомпилируем и запустим наш проект, то увидим следующее:
Теперь нужно выделить из всего спрайта отдельный квадратик, чтобы можно было его использовать для построения нужных нам фигур Тетриса. Для этого воспользуемся методом setTextureRect():
1 2 3 4 5 6 7 8 9 10 |
// … texture.loadFromFile("C:\\dev\\SFML_Tutorial\\images\\tiles.png"); // Создание спрайта Sprite sprite(texture); // Вырезаем из спрайта отдельный квадратик размером 18х18 пикселей sprite.setTextureRect(IntRect(0, 0, 18, 18)); // … |
В результате мы получим следующее:
Работа с фигурками Тетриса
Следует вопрос: «А как мы можем программным образом представить наши фигурки?». Давайте рассмотрим наши фигурки более детально. Из Википедии можно узнать, что наши фигурки называются тетрамино, т.к. каждая такая фигура состоит из 4 квадратов. Всего же в Тетрисе используется 7 таких фигурок-тетрамино:
Поэтому вполне логично описать их в виде целочисленного двумерного массива с 7-ю строками (по числу фигур) и 4-мя столбцами, задающими форму каждой конкретной фигуры. Возникает еще один вопрос: «А каким образом мы будем задавать форму тетрамино?».
Ответ вы найдете ниже:
Стоит отметить, что нам не важен порядок следования закрашенных квадратов. Важен лишь сам факт: закрашен квадрат или нет. Поэтому первой фигуре с множеством «точек» (3, 5, 4, 6)
в соответствие можно поставить как набор (5, 3, 4, 6)
, так и набор (3, 5, 6, 4)
.
Игровое поле также можно представить в виде целочисленного массива field[M][N]
, где M
— это высота поля, а N
— его ширина. Добавим эти элементы в наш код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <SFML/Graphics.hpp> using namespace sf; const int M = 20; // высота игрового поля const int N = 10; // ширина игрового поля int field[M][N] = { 0 }; // игровое поле // Массив фигурок-тетрамино int figures[7][4]= { 1,3,5,7, // I 2,4,5,7, // S 3,5,4,6, // Z 3,5,4,7, // T 2,3,5,7, // L 3,5,7,6, // J 2,3,4,5, // O }; |
Привязка тетрамино к координатам игрового поля
Итак, у нас есть массив, в котором содержатся все тетрамино. Следующим шагом будет их отображение на игровом поле. А для этого нужно каким-то образом связать локальные координаты, которыми задаются фигурки-тетратимино, с глобальными координатами игрового поля. Для решения данной задачи в качестве примера рассмотрим Z-тетрамино. Напомню, что отсчет координат игрового поля начинается с верхнего левого угла:
Видно, что, например, квадрат №5 будет иметь координаты (1;2)
, квадрат №6 будет иметь координаты (0;3)
и т.д. Но как описать данные факты математически? Чтобы разобраться с этим вопросом, сделаем небольшое лирическое отступление. Для этого возьмите листик и начните выписывать в строку числа от 0 до 40 так, чтобы в каждой строке было ровно 10 чисел.
У вас должно получиться следующее:
0 | 1 | 2 | 3 | … | 9 |
10 | 11 | 12 | 13 | … | 19 |
20 | 21 | 22 | 23 | … | 29 |
30 | 31 | 32 | 33 | … | 39 |
… | … | … | … | … | … |
Сразу бросается в глаза, что в числах каждой отдельно взятой строки содержится одинаковое количество десятков. Например, в первой строке (числа 0, 1, 2, 3…) у нас 0 десятков, во второй строке (числа 10, 11, 12, 13…) у нас по одному десятку в каждом числе, в третьей строке (числа 20, 21, 22, 23…) у нас уже по 2 десятка в каждом числе и т.д.
Если теперь рассмотреть столбцы, то номера столбцов соответствуют остатку от деления на 10, содержащемуся в числе из этого столбца. Например, возьмем столбец №1 и число 21. Если мы 21 разделим на 10, то получим в результате 2 с остатком 1 (21 = 2 * 10 + 1
), что соответствует номеру столбца. Если возьмем столбец №9 и число 39, также разделим его на 10, то получим в результате 3 с остатком 9 (39 = 3 * 10 + 9
), что также соответствует номеру столбца. В итоге, если продолжать этот процесс, то наша исходная таблица примет следующий вид:
0 | 1 | 2 | 3 | … | 9 | |
0 | 0=0*0+0 | 1=0*0+1 | 2=0*0+2 | 3=0*0+3 | … | 9=0*0+9 |
10 | 10=1*10+0 | 11=1*10+1 | 12=1*10+2 | 13=1*10+3 | … | 19=1*10+9 |
20 | 20=2*10+0 | 21=2*10+1 | 22=2*10+2 | 23=2*10+3 | … | 29=2*10+9 |
30 | 30=3*10+0 | 31=3*10+1 | 32=3*10+2 | 33=3*10+3 | … | 39=3*10+9 |
… | … | … | … | … | … | … |
Здесь уже становится понятно, что остаток от деления на 10 задает расположение числа по горизонтали, а количество десятков (частное от деления на 10) — задает расположение по вертикали. Если вспомнить, что в программировании оператор %
— это остаток от деления, а оператор /
— частное от деления (деление нацело), то, например, число 32 в нашей таблице будет иметь координаты (2;3)
= (32 % 10; 32 / 10)
.
Вернемся к нашей исходной задаче:
Т.к. здесь в каждой строке не 10 чисел, а 2 числа, то делить нужно на 2, а не на 10. И тогда квадратик №5 будет иметь координаты (1;2)
= (5 % 2; 5 / 2)
.
Отображение тетрамино на игровом поле
А сейчас попробуем отобразить наши фигурки на игровом поле. Для этого, прежде всего, нужно создать структуру Point
, которая будет представлять точку с целыми координатами + два вспомогательных массива a[]
и b[]
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Массив фигурок-тетрамино int figures[7][4]= { 1,3,5,7, // I 2,4,5,7, // Z 3,5,4,6, // S 3,5,4,7, // T 2,3,5,7, // L 3,5,7,6, // J 2,3,4,5, // O }; struct Point { int x, y; } a[4],b[4]; |
Затем с помощью первого цикла for мы переведем «локальные» координаты каждого отдельного кусочка тетрамино в «глобальные», а затем с помощью второго цикла for отобразим это всё на игровом поле. При этом стоит учитывать, что размеры отдельного кусочка составляют 18×18 пикселей:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// … while (window.isOpen()) { // Обрабатываем события в цикле Event event; while (window.pollEvent(event)) { // Пользователь нажал на «крестик» и хочет закрыть окно? if (event.type == Event::Closed) // тогда закрываем его window.close(); } int n = 3; // задаем тип тетрамино for (int i = 0; i < 4; i++) { a[i].x = figures[n][i] % 2; a[i].y = figures[n][i] / 2; } // Задаем цвет фона - белый window.clear(Color::White); for (int i = 0; i < 4; i++) { // Устанавливаем позицию каждого кусочка тетрамино sprite.setPosition(a[i].x * 18, a[i].y * 18); // Отрисовка спрайта window.draw(sprite); } // Отрисовка окна window.display(); } return 0; } |
Результат выполнения программы:
GitHub / Часть №1: Создание игры «Тетрис» на С++/SFML — Исходный код
Заключение
На этом предлагаю пока остановиться. На следующем уроке мы разберемся с реализацией поворотов тетрамино и их движением по горизонтали и вертикали. Надеюсь, что вам понравилось.
До встречи!
При компиляции в строке 27-й выскочило предупреждение: «преобразование «int» в «float», возможна потеря данных»
Нагуглил: Координаты в SFML представляют собой числа с плавающей запятой, а не целые числа.
Стоит ли делать преобразование?
правильно ли обьявлять Event event; в цикле, не лучше ли его вынести за while?
как можно сделать чтобы фигуры падали с разных частей игрового поля, а не только из левого угла как на примере?
sprite.setPosition(a[i].x * 18, a[i].y * 18) — это координаты, в которых появляется наша фигура. Т.е. что-бы наша фигура появилась правее всё, что нужно сделать — добавить к значению х число, на которое мы хотим изменить положение
sprite.setPosition(a[i].x * 18 + 18, a[i].y * 18), к примеру, сдвинет фигуру на 18 вправо
Это неверно, будет сдвигаться вся визуализация игры, а не выпадение фигурок и игра будет некорректной.
Неточность — квадрат №6 будет иметь координаты (3;0)
6%2=0
6/2 =3
квадрат №6 будет иметь координаты (0;3)
А материал крайне интересный хотя и сложно пока иметь дело с координатами.
все верно написано,
когда делишь с остатком (%) получаешь координату Х
когда делишь что бы узнать частное ( / ) получаешь координату Y
2D система координат выглядит вот так:
0.0 —————▷ X
¦
¦
¦
¦
▽
Y
а как убрать тетрамино (O) маленький один квадрат, чтоб фигур было не 7 а шесть,
вроде убрано но все равно квадрат появляется первым
>>вроде убрано но все равно квадрат появляется первым
Не может такого быть.
Загрузите свой код, например, сюда — https://ideone.com/
и выложите ссылку. Посмотрим, что там за магия 🙂
У меня тоже получился похожий результат, но стиль кода из примера местами режет глаз. Я бы даже сказала, противоречит тому, чему нас тут учили в уроках по С++:
1) используются глобальные константы и даже переменные
2) не "говорящие" названия используемых констант и переменных, один массив a[4] чего стоит!
3) куча используемых литералов (2, 18, 4), которые тоже просятся в константы
4) поголовно используется тип int, хотя для перечисления четырёх кубиков вполне хватило бы какого-то менее "тяжёлого" типа.
И, наконец, зачем нужен массив b[4]? по крайней мере для этого урока он лишний.
Я осознаю, что автор руководствовался вдохновившим видеороликом, но мне кажется, что перечисленные мной моменты желательно было бы адаптировать.
Спасибо за конструктивную критику.
Действительно, в коде есть места, которые требуют доработки. Но на это (отчасти) и был расчет. Что пользователь, который уже освоил бОльшую часть статей на данном сайте, сначала попробует выполнить проект в первозданном виде, убедится в его работоспособности и дальше уже будет изменять его так, как ему захочется.
Еще одна причина, по которой не стал переделывать код, это то, что статья выходила частями. Я подумал, что могут найтись пользователи (обычно, так и бывает), которым будет невтерпеж увидеть полностью завершенный вариант проекта, а не ждать окончания всего цикла уроков. Из-за того, что код данных статей полностью соответствует видеоролику, то пользователи смогут без проблем самостоятельно продолжить написания игры.
Теперь, касаемо непосредственно ваших замечаний:
— Всё верно, имена переменных (и других объектов) всегда должны быть осмысленными. Иначе потом можно в них легко запутаться;
— Это же относится и к т.н. "магическим числам" по типу (2, 18, 4). Их желательно выносить в константы, чтобы потом в случае чего (например, изменились размеры текстур/спрайтов/ширины поля/и т.д.) не рыться по всему коду, выискивая вхождения каждого такого числа.
-Насчет массива b[4] — действительно, он объявлен заранее, чтобы пригодиться нам в дальнейшем. Чуть позже я попрошу Администратора сделать необходимые коррективы в статье.
Насчет адаптации Ваших замечания — скорее всего это будет сделано в виде предисловия/упоминания в статье. Данный пример создания игры не ставит перед собой задачу создать идеальный код, а только лишь показывает возможности языка C++ в связке с SFML. Статья чем-то схожа со школьным учителем, который на доске от руки рисует немного косые линии и окружности (больше похожие на овалы) или объясняет про векторы в стиле — "ну это такие стрелочки…".
P.S.: Еще раз спасибо за замечания и проявленный к статье интерес. Всегда буду рад ответить на ваши вопросы 🙂
Для меня одной не очевидно, где взять картинку с кубиками, чтобы каждый был по 18 пикселей?
В конце статьи есть ссылка на исходные коды программы. В папке с исходными кодами есть еще одна папка — images. В ней будет файл — tiles.png. Это и есть тот файл текстуры, о котором идет речь в статье (можете попробовать открыть его любой программой для просмотра картинок), т.е. набор разноцветных квадратиков 18х18 пикселей. 🙂
Спасибо, а то я делала картинку из фото экрана, подогнав размеры окошка…
Здравствуйте. Скажите пожалуйста, будет ли продолжение урока создания тетриса?
Добрый день. Конечно будет. Вторая часть статьи уже передана на корректуру. В ближайшее время она появится на сайте.
Ох-ох-ох! Что то понимание отрисовки даётся мне тяжко) Ну и вот эта запись a[i].x — я вот не понимаю её) а — массив — i элемент, а вот точка x мне не понять) Эх) Буду разбираться)
Вот смотрите. У вас же a[i] — это массив структур типа Point. А у каждой структуры Point есть свои внутренние поля: int x; int y. Вот как раз таки доступ к этим полям и осуществляется через точку: a[i].x или a[i].y
Советую почитать https://ravesli.com/urok-61-struktury/
🙂
Вот спасибо большое, перечитаю ещё пару раз, вдруг дойдёт)
Разобрался! Спасибо большое за ваш труд) И за ответ)
Очень буду ждать ваши следующие работы. Очень понятным языком, очень приятно читать. Прям вот мана небесная ещё и прожёванная)
Надеюсь у вас не возникнут проблем с написанием статей, а то как это мы без таких знаний? Английский то ещё до сих пор хромает на обе ноги, если не в коляске инвалидной катается))
Всегда пожалуйста 🙂