Часть №12: Рендеринг текста в игре «Breakout» на C++/OpenGL

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

  Обновл. 20 Дек 2020  | 

 4844

На этом уроке мы добавим в нашу игру «Breakout» последние улучшения: игровые жизни, условие выигрыша и обратную связь в виде обработанного текста. Данный материал в значительной степени опирается на ранее опубликованный урок о рендеринге текста, поэтому настоятельно рекомендуется сначала прочитать его (если вы еще этого не сделали).

Рендеринг текста

В игре «Breakout» весь код рендеринга текста инкапсулируется в класс TextRenderer, который содержит инициализацию библиотеки FreeType, конфигурацию рендера и непосредственно сам код рендера.

#Класс TextRenderer

Заголовочный файл — text_renderer.h:

Файл реализации — text_renderer.cpp:

Вершинный шейдер — файл text_2d.vs:

Фрагментный шейдер — файл text_2d.frag:

Содержимое функций рендера текста почти в точности совпадают с аналогичным кодом из урока о рендеринге текста. Однако код рендеринга глифов немного отличается:

Причина этих отличий заключается в том, что мы используем иную матрицу орфографической проекции. В игре «Breakout» все значения y отсчитываются сверху вниз (при этом координата y со значением 0.0f соответствует верхнему краю экрана). Это означает, что мы должны немного изменить способ вычисления вертикального смещения.

Поскольку теперь мы визуализируем по нисходящим значениям параметра y функции RenderText(), то представление вертикального смещения будет соответствовать расстоянию, на которое глиф перемещается вниз от верхней границы занимаемой им области. На следующей картинке это расстояние обозначено красной стрелочкой:

Чтобы вычислить данное вертикальное смещение, нам нужно получить высоту отведенной под глифы области (длину черной вертикальной стрелки от начала координат). К сожалению, библиотека FreeType не имеет для нас такой метрики. Но мы можем посмотреть в сторону тех глифов, которые всегда касаются этого верхнего края: символы 'H', 'T' или 'X'. А далее вычислим длину данного красного вектора, вычитая Bearing.y из значения Bearing.y любого из этих достигающих верхнего края глифов. Таким образом, мы сдвигаем глиф вниз в зависимости от того, насколько его высота отличается от высоты до верхнего края области:

В дополнение к обновлению расчета переменной ypos мы также немного изменили порядок вершин, чтобы при умножении на текущую матрицу ортографической проекции все вершины по-прежнему были обращены к зрителю лицевой стороной (см. Урок №22. Отсечение граней).

Добавить использование класса TextRenderer в игру очень просто:

Переменная Text типа TextRenderer инициализируется шрифтом OCR A Extended, который вы можете скачать здесь. Если шрифт вам не нравится, смело используйте другой.

Игровые жизни


Вместо того, чтобы сразу же сбросить игру, как только мяч достигнет нижнего края, мы хотели бы дать игроку несколько дополнительных шансов. Реализуем это в виде игровых жизней, где игрок начинает с некоторым стартовым количеством жизней (скажем, 3), и каждый раз, когда мяч касается нижнего края окна, у игрока отнимается 1 жизнь. Только когда общее количество жизней игрока становится равным 0, мы сбрасываем игру. Благодаря этому, игроку будет легче пройти уровень, и в то же время создается некоторое игровое напряжение.

Мы будем вести подсчет жизней игрока с помощью соответствующей переменной, добавленной в класс Game (инициализируемой в конструкторе значением 3):

Затем модифицируем функцию Game::Update(), чтобы вместо сброса игры, уменьшить количество жизней игрока и сбросить игру только после того, как это количество станет равным 0:

Как только игрок закончит игру (переменная Lives станет равна 0), мы сбросим уровень и изменим состояние игры на GAME_MENU, которое обсудим чуть позже.

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

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

Мы преобразуем количество жизней в строку и выведем её в верхнем левом углу экрана. Теперь это будет выглядеть примерно так:

Как только мяч касается нижнего края экрана, общее количество жизней игрока уменьшается, и это мгновенно отображается в верхнем левом углу.

Выбор уровня

Всякий раз, когда игрок находится в игровом состоянии GAME_MENU, мы хотели бы дать ему возможность выбрать уровень, на котором он хочет играть. С помощью кнопок w или s игрок может пролистывать загруженные нами уровни. Далее он должен нажать клавишу Enter, чтобы переключиться из состояния игры GAME_MENU в состояние GAME_ACTIVE.

Позволить игроку выбрать уровень не так уж и сложно. Всё, что нам нужно сделать, — это увеличить или уменьшить переменную Level игрового класса в зависимости от того, нажал ли игрок кнопку w или s:

Мы используем оператор остатка от деления (%), чтобы убедиться, что переменная Level находится в пределах допустимого диапазона значений (от 0 до 3).

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

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

Выглядит великолепно, но запуская проект, вы, вероятно, заметите, что как только вы нажмете клавишу w или s, игра быстро прокручивается по уровням, что затрудняет их выбор. Это происходит потому, что она считывает нажатие кнопки каждый раз, пока мы её не отпустим. Это приводит к тому, что функция ProcessInput() обрабатывает нажатую клавишу более одного раза.

Мы можем решить эту проблему с помощью небольшого трюка, обычно встречающегося в GUI-системах. Хитрость заключается в том, чтобы не только записывать кнопки, которые в данный момент нажаты, но и хранить те кнопки, которые были обработаны один раз, пока они не будут снова отпущены. Затем мы проверяем (перед обработкой), не была ли кнопка обработана, и если — да, то обрабатываем её, после чего сохраняем как обработанную. Как только мы опять хотим обработать ту же самую кнопку, но без нажатия, — мы не обрабатываем её. Это, вероятно, звучит несколько запутанно, но как только вы увидите это на практике, вам сразу всё станет ясно.

Сначала мы должны создать еще один массив значений типа bool, чтобы указать, какие кнопки были обработаны. Мы определим его в классе Game:

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

Теперь, если значение кнопки в массиве KeysProcessed еще не задано, мы обрабатываем кнопку и устанавливаем значение переменной в true. В следующий раз, когда мы достигнем if-условия для той же кнопки (а она уже будет к этому моменту обработана), мы притворимся и не будем замечать текущее нажатие до тех пор, пока кнопка снова не будет отпущена.

Затем в файле program.cpp внутри callback-функции key_callback() нам нужно сбросить статус обработки кнопки, как только она будет отпущена, чтобы при следующем нажатии мы могли её снова обработать:

Снова запуская игру, мы видим изящный экран выбора уровня, который теперь точно выбирает один уровень за одно нажатие кнопки, независимо от того, как долго мы её удерживаем в «нажатом» состоянии.

Условие выигрыша


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

Игрок выигрывает игру, когда все нетвердые блоки будут уничтожены. Мы уже создали функцию IsCompleted() в классе GameLevel для проверки данного условия:

Мы проверяем все кирпичи на игровом уровне, и, если есть хотя бы один неразрушенный нетвердый кирпич, возвращаем false. Всё, что нам нужно сделать — это проверить наличие этого условия в функции Game::Update() и как только она вернет true, мы изменим состояние игры на GAME_WIN:

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

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

Теперь, если вы достаточно хороши, чтобы действительно выиграть игру, вы получите следующее изображение:

И это всё! Последний кусочек игры «Breakout», над которой мы так активно работали, готов. Пришло время испробовать нашу игру. Настраивайте её так, как вам нравится, и покажите всем своим родным и друзьям!

  GitHub / Часть №12: Рендеринг текста в игре «Breakout» на C++/OpenGL — Исходный код

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

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

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

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