Часть №1: Создание игры «Тетрис» на С++/SFML

  Дмитрий Бушуев  | 

  |

  Обновл. 20 Июл 2021  | 

 79930

 ǀ   21 

На написание данной статьи меня побудил ролик на YouTube, в котором автор за 4 минуты с нуля, используя графическую библиотеку SFML, создает рабочий прототип Тетриса. Всё это происходило в ускоренном воспроизведении под веселую вариацию оригинальной тетрис-мелодии «Коробейники». За всё это время автор не произнес ни слова, а сам ролик содержал несколько довольно неочевидных моментов, которые требовали пояснений. В связи с этим я решил взять на себя ответственность и восполнить данный пробел.

Первые шаги

Как вы уже знаете, практически любое SFML-приложение начинается с минимального каркаса:

Далее мы создаем спрайт для представления различных элементов фигур в нашей игре и присваиваем ему файл текстуры — tiles.png. Сам файл текстуры у меня располагается по пути C:\dev\SFML_Tutorial\images\. Текстура являет собой 8 разноцветных квадратов, горизонтально стоящих друг за другом. Размер каждого такого квадрата составляет 18×18 пикселей.

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

Если мы скомпилируем и запустим наш проект, то увидим следующее:

Теперь нужно выделить из всего спрайта отдельный квадратик, чтобы можно было его использовать для построения нужных нам фигур Тетриса. Для этого воспользуемся методом setTextureRect():

В результате мы получим следующее:

Работа с фигурками Тетриса


Следует вопрос: «А как мы можем программным образом представить наши фигурки?». Давайте рассмотрим наши фигурки более детально. Из Википедии можно узнать, что наши фигурки называются тетрамино, т.к. каждая такая фигура состоит из 4 квадратов. Всего же в Тетрисе используется 7 таких фигурок-тетрамино:

Поэтому вполне логично описать их в виде целочисленного двумерного массива с 7-ю строками (по числу фигур) и 4-мя столбцами, задающими форму каждой конкретной фигуры. Возникает еще один вопрос: «А каким образом мы будем задавать форму тетрамино?».

Ответ вы найдете ниже:

Стоит отметить, что нам не важен порядок следования закрашенных квадратов. Важен лишь сам факт: закрашен квадрат или нет. Поэтому первой фигуре с множеством «точек» (3, 5, 4, 6) в соответствие можно поставить как набор (5, 3, 4, 6), так и набор (3, 5, 6, 4).

Игровое поле также можно представить в виде целочисленного массива field[M][N], где M — это высота поля, а N — его ширина. Добавим эти элементы в наш код:

Привязка тетрамино к координатам игрового поля

Итак, у нас есть массив, в котором содержатся все тетрамино. Следующим шагом будет их отображение на игровом поле. А для этого нужно каким-то образом связать локальные координаты, которыми задаются фигурки-тетратимино, с глобальными координатами игрового поля. Для решения данной задачи в качестве примера рассмотрим 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[]:

Затем с помощью первого цикла for мы переведем «локальные» координаты каждого отдельного кусочка тетрамино в «глобальные», а затем с помощью второго цикла for отобразим это всё на игровом поле. При этом стоит учитывать, что размеры отдельного кусочка составляют 18×18 пикселей:

Результат выполнения программы:

  GitHub / Часть №1: Создание игры «Тетрис» на С++/SFML — Исходный код

Заключение

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

До встречи!


Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (96 оценок, среднее: 4,82 из 5)
Загрузка...

Комментариев: 21

  1. Сергей Федоров:

    При компиляции в строке 27-й выскочило предупреждение: «преобразование «int» в «float», возможна потеря данных»
    Нагуглил: Координаты в SFML представляют собой числа с плавающей запятой, а не целые числа.
    Стоит ли делать преобразование?

  2. Ayras:

    правильно ли обьявлять Event event; в цикле, не лучше ли его вынести за while?

  3. int:

    как можно сделать чтобы фигуры падали с разных частей игрового поля, а не только из левого угла как на примере?

    1. Egor:

      sprite.setPosition(a[i].x * 18, a[i].y * 18) — это координаты, в которых появляется наша фигура. Т.е. что-бы наша фигура появилась правее всё, что нужно сделать — добавить к значению х число, на которое мы хотим изменить положение
      sprite.setPosition(a[i].x * 18 + 18, a[i].y * 18), к примеру, сдвинет фигуру на 18 вправо

      1. Egor:

        Это неверно, будет сдвигаться вся визуализация игры, а не выпадение фигурок и игра будет некорректной.

  4. Rock:

    Неточность — квадрат №6 будет иметь координаты (3;0)
    6%2=0
    6/2 =3

    квадрат №6 будет иметь координаты (0;3)

    А материал крайне интересный хотя и сложно пока иметь дело с координатами.

    1. Daniel:

      все верно написано,
      когда делишь с остатком (%) получаешь координату Х
      когда делишь что бы узнать частное ( / ) получаешь координату Y

      2D система координат выглядит вот так:

      0.0 —————▷ X
      ¦
      ¦
      ¦
      ¦

      Y

  5. Дмитрий:

    а как убрать тетрамино (O) маленький один квадрат, чтоб фигур было не 7 а шесть,

    вроде убрано но все равно квадрат появляется первым

    1. Фото аватара Дмитрий Бушуев:

      >>вроде убрано но все равно квадрат появляется первым
      Не может такого быть.
      Загрузите свой код, например, сюда — https://ideone.com/
      и выложите ссылку. Посмотрим, что там за магия 🙂

  6. Анастасия:

    У меня тоже получился похожий результат, но стиль кода из примера местами режет глаз. Я бы даже сказала, противоречит тому, чему нас тут учили в уроках по С++:
    1) используются глобальные константы и даже переменные
    2) не "говорящие" названия используемых констант и переменных, один массив a[4] чего стоит!
    3) куча используемых литералов (2, 18, 4), которые тоже просятся в константы
    4) поголовно используется тип int, хотя для перечисления четырёх кубиков вполне хватило бы какого-то менее "тяжёлого" типа.
    И, наконец, зачем нужен массив b[4]? по крайней мере для этого урока он лишний.
    Я осознаю, что автор руководствовался вдохновившим видеороликом, но мне кажется, что перечисленные мной моменты желательно было бы адаптировать.

    1. Фото аватара Дмитрий Бушуев:

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

      Еще одна причина, по которой не стал переделывать код, это то, что статья выходила частями. Я подумал, что могут найтись пользователи (обычно, так и бывает), которым будет невтерпеж увидеть полностью завершенный вариант проекта, а не ждать окончания всего цикла уроков. Из-за того, что код данных статей полностью соответствует видеоролику, то пользователи смогут без проблем самостоятельно продолжить написания игры.

      Теперь, касаемо непосредственно ваших замечаний:
      — Всё верно, имена переменных (и других объектов) всегда должны быть осмысленными. Иначе потом можно в них легко запутаться;
      — Это же относится и к т.н. "магическим числам" по типу (2, 18, 4). Их желательно выносить в константы, чтобы потом в случае чего (например, изменились размеры текстур/спрайтов/ширины поля/и т.д.) не рыться по всему коду, выискивая вхождения каждого такого числа.
      -Насчет массива b[4] — действительно, он объявлен заранее, чтобы пригодиться нам в дальнейшем. Чуть позже я попрошу Администратора сделать необходимые коррективы в статье.

      Насчет адаптации Ваших замечания — скорее всего это будет сделано в виде предисловия/упоминания в статье. Данный пример создания игры не ставит перед собой задачу создать идеальный код, а только лишь показывает возможности языка C++ в связке с SFML. Статья чем-то схожа со школьным учителем, который на доске от руки рисует немного косые линии и окружности (больше похожие на овалы) или объясняет про векторы в стиле — "ну это такие стрелочки…".

      P.S.: Еще раз спасибо за замечания и проявленный к статье интерес. Всегда буду рад ответить на ваши вопросы 🙂

  7. Анастасия:

    Для меня одной не очевидно, где взять картинку с кубиками, чтобы каждый был по 18 пикселей?

    1. Фото аватара Дмитрий Бушуев:

      В конце статьи есть ссылка на исходные коды программы. В папке с исходными кодами есть еще одна папка — images. В ней будет файл — tiles.png. Это и есть тот файл текстуры, о котором идет речь в статье (можете попробовать открыть его любой программой для просмотра картинок), т.е. набор разноцветных квадратиков 18х18 пикселей. 🙂

      1. Анастасия:

        Спасибо, а то я делала картинку из фото экрана, подогнав размеры окошка…

  8. Василий:

    Здравствуйте. Скажите пожалуйста, будет ли продолжение урока создания тетриса?

    1. Фото аватара Дмитрий Бушуев:

      Добрый день. Конечно будет. Вторая часть статьи уже передана на корректуру. В ближайшее время она появится на сайте.

  9. Penguin:

    Ох-ох-ох! Что то понимание отрисовки даётся мне тяжко) Ну и вот эта запись a[i].x — я вот не понимаю её) а — массив — i элемент, а вот точка x мне не понять) Эх) Буду разбираться)

    1. Фото аватара Дмитрий Бушуев:

      Вот смотрите. У вас же a[i] — это массив структур типа Point. А у каждой структуры Point есть свои внутренние поля: int x; int y. Вот как раз таки доступ к этим полям и осуществляется через точку: a[i].x или a[i].y

      Советую почитать https://ravesli.com/urok-61-struktury/
      🙂

      1. Penguin:

        Вот спасибо большое, перечитаю ещё пару раз, вдруг дойдёт)

      2. Penguin:

        Разобрался! Спасибо большое за ваш труд) И за ответ)

        Очень буду ждать ваши следующие работы. Очень понятным языком, очень приятно читать. Прям вот мана небесная ещё и прожёванная)

        Надеюсь у вас не возникнут проблем с написанием статей, а то как это мы без таких знаний? Английский то ещё до сих пор хромает на обе ноги, если не в коляске инвалидной катается))

        1. Фото аватара Дмитрий Бушуев:

          Всегда пожалуйста 🙂

Добавить комментарий для Дмитрий Бушуев Отменить ответ

Ваш E-mail не будет опубликован. Обязательные поля помечены *