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

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

  Обновл. 15 Фев 2020  | 

 5624

 ǀ   11 

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

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

Функция проверки тетрамино на выход за границы игрового поля довольно-таки простая и состоит из двух условий 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):

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


Заключение

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

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

Исходный код. Создание Тетриса — Часть №3


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

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

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

  1. Аватар Даня:

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

  2. Аватар Анатолий:

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

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

  3. Аватар AndreyOlegovich.ru:

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

  4. Аватар Влад:

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

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

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

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

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

  5. Аватар Анастасия:

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

    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)

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

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

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

  6. Аватар Василий:

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

    1. Аватар Василий:

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

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

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

        1. Аватар AndreyOlegovich.ru:

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

Добавить комментарий

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