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

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

  |

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

 33658

 ǀ   28 

Вот мы и подошли к заключительной части нашей серии уроков по созданию Тетриса средствами C++\SFML (первая часть здесь, а вторая здесь). Сегодня мы рассмотрим реализацию механизма проверки тетрамино на выход за пределы игрового поля, сделаем генерацию рандомных типов фигурок и их цветов, добавим возможность ускорить падение тетрамино, будем уничтожать полученную из тетрамино линию и установим рамки игрового поля вместе с фоном.

Проверка границ и случайная генерация тетрамино

Функция проверки тетрамино на выход за границы игрового поля довольно-таки простая и состоит из двух условий if():

   первое условие if() проверяет, не вышли ли мы за границы поля слева, справа и снизу. Т.к. мы движемся сверху вниз, то выйти за границу сверху мы не можем, соответственно, проверять её нет смысла.

   второе условие if() проверяет, свободна ли сейчас ячейка, расположенная на пути движения, или занята другими тетрамино.

Код:

Теперь реализуем рандомную генерацию тетрамино с помощью генератора случайных чисел. Для этого подключаем заголовочный файл time.h:

А в теле функции main() дописываем следующую строку:

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

Рассмотрим этот код детально. Сначала мы перемещаем координаты массива a[] во вспомогательный массив b[]:

Далее на игровой доске делаем «шаг влево» или «шаг вправо»:

А затем проверяем, не вышли ли мы за пределы игрового поля. Если вышли, то возвращаем старые координаты из вспомогательного массива b[]:

То же самое проделываем и с секцией «Вращение»:

А теперь секция «Тик таймера». Здесь мы добавим цикл for, в котором, при помощи генератора случайных чисел, будем задавать тип и цвет тетрамино:

Не забываем, что нужно объявить переменные, которые будут определять горизонтальное перемещение, вращение и цвет тетрамино:

Ловим баг


Далее у автора видео идет довольно спорный момент. Он, видимо, посчитав, что алгоритм первоначального задания тетрамино теперь перенесен в секцию «Движение тетрамино вниз (Тик таймера)», легким взмахом руки удаляет из проекта следующий кусок кода:

Из-за чего мы получаем неприятный баг, о котором я расскажу ниже. А пока, продолжая объяснение видеоролика, отредактируем нижеследующую секцию кода «Отрисовка»:

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

Багом является одинокий квадратик, который постоянно присутствует на поле. Данная ошибка возникает из-за того, что в самом начале игры, при создании первой фигурки-тетрамино, не выполняется код, который отвечает за следующее:

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

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

Код, расположенный внутри блока if(beginGame), вызывается только один раз при старте приложения. Благодаря ему задается первоначальный тип тетрамино. В результате этих действий стартовый тетрамино отображается корректно, и бага больше нет:


Ускоряем движение тетрамино вниз

Если вы уже знаете, куда поставить тетрамино, и не хотите ждать, пока оно своим ходом доберется до этого положения, то логичным будет ускорить падение фигурки по нажатию на кнопку «стрелка вниз», как это и реализовано в многочисленных вариантах тетриса. Для этого добавим проверку на нажатие соответствующей клавиши на клавиатуре. И, если условие выполняется, уменьшим задержку между «тиками» до 0.05 секунды, тем самым заставив тетрамино падать быстрее:

При этом нужно не забыть вернуть первоначальное значение задержки, иначе новые тетрамино также будут двигаться вниз с ускорением:

Разукрашиваем тетрамино


До этого момента все тетрамино имели одинаковый цвет. Пришло время их немного разукрасить:

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


Уничтожение полученной из тетрамино линии

Рассмотрим вопрос о реализации функции проверки на наличие заполненной тетрамино линии, которую по правилам тетриса нужно удалять. В этом нам поможет цикл for, с помощью которого мы снизу вверх пройдемся по массиву field[][], который представляет наше игровое поле, и проверим каждый элемент массива на равенство нулю.

Если элемент field[i][j] не равен нулю (т.е. эта позиция поля уже занята), то увеличиваем счетчик на единицу (count++) и присваиваем данный элемент самому себе (field[k][j] = field[i][j];). При этом, мы как бы с двух сторон ползем вверх по массиву field[][], увеличивая сначала индекс i и, если у нас есть пробелы в текущей строке (count < N), индекс k. И опять присваиваем каждый элемент строки самому себе. Тем самым текущая строка не изменяется.

Но если условие if (count < N) не выполняется (т.е. в данной строке все места заняты), то индекс k не меняется. И тогда, при очередном присваивании field[k][j] = field[i][j];, индекс k не будет равен индексу i. А это означает, что элементы верхней строки будут присвоены элементам нижней строки. Тем самым наше игровое поле «сдвинется» вниз на одну строку:

Картинка для понимания процесса:

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


Установка текстуры фона


Осталось дело за малым: установить фоновую текстуру и рамку игрового поля. Для этого создадим две дополнительные переменные текстур: texture_background и texture_frame; и соответствующие им две переменные спрайтов: sprite_background и sprite_frame.

Добавим отрисовку новых спрайтов в секцию «Отрисовка». Т.к. наши фигуры появляются от края окна, а текстура фона немного смещена к центру, то нужно немного сместить тетрамино, чтобы всё выглядело красиво. Для этого воспользуемся функцией sprite.move(28, 31):

Окончательная версия нашего Тетриса:

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

Заключение

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

И до скорых встреч!

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

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

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

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

    Дмитрий, большое спасибо за урок!
    Хорошо, что акцент сделан именно на функциональности библиотеки SFML а не на ООП (классы, прегрузки и т.д.). Все кто внимательно изучил курс, сейчас вполне смогут перестроить программу в стиль ООП. Но нужно ли это именно для текущего проекта? К тому же в мире много критики ООП.
    «Чем быстрее вы забудете ООП, тем лучше для вас и ваших программ»  😉

    Очень хорошо, что не все шаги описаны пунктуально-идеально, иначе создание программы сводится к конструктору лего, а подумать стоит. Для всех кто изучает курс очень советую читать коментарии других учеников.

    Следующий шаг, наверное, совместить SFML и MFC для установки дополнительных настроек.
    Единственный удручающий момент по использованию SMFL только один — каждое изменение состояния влечет за собой ПОЛНУЮ перерисовку экрана. Но, видимо, для такой простой программы это не существенно. Ресурсы современных копьютеров позволяют. Или всё не так однозначно?

  2. Finchi:

    Большое спасибо за уроки.
    Не в обиду автору кода, не все мне в нем понравилось. Поэтому решил сделать все по своему с самого начала и добавил "конец игры".

    main.cpp

    Tetramino.h

    Tetramino.cpp

  3. Сергей:

    И еще такой момент, что при условии когда тетрамино пересекает координату M внизу, то оно не остается внизу а респаунится в точке 0, вместо того чтобы оставаться внизу и появляться другой фигуре одновременно

  4. Сергей:

    Подскажите пожалуйста, не могу понять почему при возвращении 0 функции чек, не выполняются условия к возвращению к предыдущим координатам?

  5. Алексей:

    А возможно ли увеличить размер фигур тетриса без изменения масштаба окна? Если да то как это сделать?

  6. Виктор:

    Здравствуйте! Очень понравился ваша игра и решил сделать свою. Пишу первый раз такой проект (игру Тетрис) . Я имею одну проблему, не могу понять как можна увеличить игровое поле для тетромина .
    Пытался изменить координаты поля

    Пробовал что-то изменять в массиве и в функциях которые отвечают за пределы выхода поля тетрамина , ничего не помогло. Буду очень благодарен за помощь 🙂

  7. Nook:

    Здравствуйте! Очень хороший урок, а Вы не подскажите, как сделать так, что когда окошко растягиваешь, все внутри не растягивалась?

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

      Можно добавить в цикл while (window.pollEvent(event)) строчки:

      1. nook:

        Спасибо большое.

  8. Alex:

    Добрый день,
    Понравился урок и Ваша реализация тетриса.
    Решил несколько модифицировать код в учебных целях и сделал автоповорот фигур по таймеру во время падения. Получилось уже достаточно сложно и забавно 🙂

    Поставил перед собой следующую задачу сделать более плавный автоповорот падающих фигур (как пример на 30*), но не нашёл способ этого сделать. Добавление функции split.Rotation(30) после

    Не приводит к нужному результату, так как все 4 splite поворачиваются по отдельности на 30 градусов. Меня же интересует поворот этой фигуры целиком на 30 градусов.

    По объединению splite никакой информации не нашёл. Есть ли какой-либо способ это реализовать?

    P.S.: Я понимаю, что фигуры не должны падать на поле под углом 30, 60 и т.д. градусов, но в процессе их падения хотелось бы сделать вот такую "анимацию". В полёте фигуры вращаются с шагом в 30 градусов, а падают всегда в правильной ориентации.

    Как это можно реализовать?

  9. Alexandr:

    В общем переписывал программу в своём стиле, чтоб легче было ориентироваться, полностью под ООП не переписывал, но класс для tetramino добавил( только для своего удобства). По ходу решил такие проблемы: подпрыгивание tetramino при нарушении боковых границ, возврат в начало поля при вращении tetramino у левой границы, добавил конец игры(можно переписать под обновление поля), избавился от "магических чисел". В любом случае код сырой, но главное что стабильный и работает.

    main.cpp

    Tetramino.h

    Tetramino.cpp

    1. Илья:

      А как ты добавил конец игры?

  10. Alexandr:

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

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

      Если мне не изменяет память, то за установку блоков отвечает код из самого конца нашей программы:

      Но перед этим, в коде есть несколько проверок на выход за границы игрового поля при помощи функции check(). Если проверка не проходит, то возвращаются старые координаты a[i].x и a[i].y. Вроде так 🙂

  11. Даня:

    Здравствуйте!
    Как я понял в игре нет конца игры. И получается, что если не двигать тетрамино, они сначала ставятся друг на друга, а потом накладываются друг на дружку. При попытке вывести тетрамино вправо, вывести можно только "палку" (4 вертикальный блока). Можно конечно каждый раз перезапускать, но огрешность есть. Не могли бы Вы написать недостающий код?

    1. DirtyCode:

      Ось метод для перевірки кінця гри:

    2. DirtyCode:

      Обновлено:

      1. Павел:

        Не очень понятно куда и как его надо вставить(

  12. Анатолий:

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

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

  13. AndreyOlegovich.ru:

    Большое спасибо за статьи. Тетрис работает.

  14. Влад:

    Огромное спасибо вам. Все просто и ясно, прям спасли меня.
    Но вот этот момент не совсем понятен

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

      >if (field[i][j] == 0) continue;
      >sprite.setPosition(j * 18, i * 18);
      >window.draw(sprite);

      В логической модели нашего игрового поля пустая клетка имеет нулевое значение. Это значит, что на никакого спрайта на ней не должно находиться. Поэтому, если мы попали на такую клетку, то просто пропускаем итерацию. Если же это не пустая клетка — то с помощью sprite.SetPosition() устанавливаем на нее спрайт и отрисовываем его.

      P.S.: И вам большое спасибо за уделенное внимание 🙂

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

    Здравствуйте. Попробую объяснить покороче, что мне не понравилось:

    1) Нет пояснения, что проверку if (!check) нужно добавить и в блоке с тиком таймера. Вертикальные координаты меняются как раз там. В коде проверка есть, но пояснения этому не наблюдается.

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

    3) Очень не хватает нормального пояснения изменению блока "Отрисовка". Я имею в виду появление блока

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

    4) В пояснении про проверку полной линии ничего не сказано про индекс k, из-за этого всё пояснение трудно понять (но я справилась). Почему начинаем с k=M-1, а не с k=M тоже не совсем очевидно.
    У меня в итоге всё получилось, как и в статье, но из-за этих упущений осталось смазанное впечатление. Вроде бы и работает, но нет уверенного понимания назначения и порядка некоторых строк кода, а значит и нет ощущения, что я бы смогла написать такое самостоятельно. Здесь мало комментариев, поэтому могу предположить, что я такая не одна. Скопировать и вставить код можно, а вот разобраться полностью, чтобы потом при случае повторить — совсем другое дело. Мне не хватило пояснений.

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

      Добрый день.
      1)В самом начале статьи идет подробное описание алгоритма и назначение функции check(). Что она нужна для проверки выхода тетрамино за границы игрового поля.

      Далее, рассматривается добавление функции check() в секцию кода "горизонтальное перемещение" (с добавлением необходимых комментариев). Что вполне логично.

      Затем идет указание, что аналогичное добавление нужно сделать и для следующей секции (дословно — "То же самое проделываем с секцией «Вращение»"). Что также вполне объяснимо и понятно, т.к. при вращении фигуры тетрамино может выйти за границы поля.

      Следующая секция — "Движение тетрамино вниз (тик таймера)". Снова встречается функция check(). И тут читатель перестаёт понимать, зачем мы функцию check() и в эту секцию добавили? Ну такое себе…

      2)Чуть позже добавлю в статью более подробное описание алгоритма данного пункта.

      3)Насчет "исчезновения" второй части кода с циклом for. Она как была там, так и осталась. Никуда не исчезала.

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

      4)Насчет индекса k=M-1 и проверки линии. Мы же начинаем со дна нашего игрового поля и поднимаемся вверх (см. соответствующие картинки в этом пункте). Если, например, есть вот такое объявление массива — int array[M] размера M, то array[M-1] — это будет последний его элемент (т.к., в C++ нумерация элементов массива начинается с 0, а не с 1). Аналогичная ситуация и с двумерными массивами (читать — таблицами). Если есть объявление двумерного массива — int array[M][N], то мы получим таблицу из M строк и N столбцов. Как обратиться к элементам последней строки такой таблицы? Да так же, как и в случае с одномерным массивом, а именно — array[M-1][i], где 0<= i < N. Вот откуда и берется индекс k=M-1. Подобные моменты относятся к базовым (и самым простым) понятиям C++.
      (Советую еще раз посмотреть уроки #75, #76, #78 на Ravesli.com)

      >>но я справилась
      Это похвально.
      ——————————————————————————

      В итоге:
      >>У меня в итоге всё получилось, как и в статье, но из-за этих упущений осталось смазанное впечатление.Вроде бы и работает, но нет уверенного понимания назначения и порядка некоторых строк кода, а значит и нет ощущения, что я бы смогла написать такое самостоятельно.

      Да нет здесь каких-то таких уж криминальных упущений. Есть некоторые моменты, которые, в принципе, можно было бы и расписать несколько подробнее. Есть моменты, связанные с ограниченными возможностями средств для вёрстки статьи. Были моменты, в которых вы не смогли разобраться из-за недостатка опыта. Одно наложилось на другое, в итоге получили то, что получили. И это нормально.

  16. Василий:

    Здравствуйте. У меня возникла небольшая проблема: при повороте фигуры у левого края, она возвращается в начальное положение (0,0).
    В других частях стакана работает все хорошо. Как думаете в чем может быть проблема? Кстати, у меня изначально(до добавления функции check() ) фигура не проходила за левую границу стакана, а через правую проходила.

    1. Василий:

      Я разобрался с этой проблемой))

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

        Так опишите хотя бы в двух словах: в чем была проблема и как решили? 🙂

        1. AndreyOlegovich.ru:

          Я подозреваю, что этот баг вызван недостаточно полной проверкой первого появления тетрамино. Это скорее актуально для второй части статьи, так как при появлении beginGame всё меняется. Попробуйте запустить код из ваших исходников для части 2 и сделать пару вращений — Вы увидите, что тетрамино вернётся наверх так как выполнится (a[0].x == 0), мне пришлось временно добавлять дополнительную проверку (a[0].x == 0 || a[0].y == 0) или что-то в таком духе.

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

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