0. Вступление
Арканоид это видеоигра разработанная компанией Taito в 1986 году. С тех пор появился целый класс подобных игр. Суть любого Арканоида состоит в том, что нужно отбивать ракеткой шарик, который в свою очередь может врезаться в кирпичики, уничтожая их. Цель игры - разбить все кирпичики. Я попробовал написать свой собственный арканоид, результат вы можете увидеть на скриншоте сверху или в этом видеоролике:
https://dzen.ru/video/watch/65f7ebdb6086902ffd0e5831
Полный код игры на Java Script:
Далее буду описывать процесс разработки этой видеоигры.
1. Подготовка файлов
Начну с создания папки проекта, в ней будут лежать файлы html, css и js. Работать в основном буду с js. Также здесь есть папка images, в ней будут храниться игровые спрайты.
Далее настраиваю html, подключаю к нему скрипт и стили, создаю элемент canvas, позволяющий рисовать графику с помощью js, на нём и будет происходить вся игра. Ширину и высоту холста настраиваю именно в коде html, т.к. если сделать это в css, то холст просто растянется по странице и будет иметь низкое разрешение.
Теперь настраиваю css. Делаю приятный серый фон у веб-страницы, располагаю холст сверху по вертикали и в центре по горизонтали. Фон холста раскрашиваю в белый.
Затем захожу в файл script.js и создаю константу canvas, которая будет ссылаться на холст. Также указываю, что игра будет в 2Д формате.
Подготовительные мероприятия закончены, далее весь процесс будет происходить в файле скрипта.
2. Создание класса игровых объектов
Всего будет 4 типа игровых объектов: ракетка игрока; шарик; стены; кирпичики. Стены будут выполнять роль преграды для шарика, отскакивая от них он будет делать свою траекторию более интересной и разнообразной. У всех этих игровых объектов будут: координаты по осям x и y; ширина и высота; спрайт. Я создам класс игровых объектов, в экземплярах которого будут храниться эти пять свойств. Перед названиями свойств я пишу "#", таким образом я указываю на то, что эти свойства приватные и обратиться к ним напрямую извне не получится.
Задавать значения этим свойствам я буду при создании объекта с помощью конструктора.
На протяжении игры мне нужно будет узнавать значения этих пяти свойств для каждого игрового объекта, поэтому с помощью get я создаю для них всех геттеры. Также я буду постоянно менять местоположение объектов, поэтому для свойств координат с помощью set создаю сеттеры. Сеттеры для ширины, высоты и картинки мне не нужны, ведь я не собираюсь менять эти значения у объектов.
3. Метод для отслеживания пересечений
Шарик будет врезаться в другие игровые объекты, мне нужно написать для него логику столкновений, а для этого необходим метод отслеживающий пересечение двух игровых объектов. Перед тем как выложить код этого метода, опишу принцип его работы. Начну с того, что у html элемента canvas начало координат находится слева сверху, а ось y направленна вниз.
Получается что если я хочу двигаться вверх, то мне нужно уменьшать y координату объекта, а если вниз, то наоборот её увеличивать. Игровые объекты в моей игре по сути являются прямоугольниками, их координаты это координаты левых верхних точек этих прямоугольников.
Нетрудно догадаться что координаты всех остальных точек прямоугольников можно получить прибавляя к координатам левых верхних точек значения ширины и высоты.
Зная координаты всех точек можно определить, пересекаются ли два прямоугольника. Для этого сначала рассмотрим все случаи, в которых они не пересекаются. А таких случаев всего 4:
- Когда левая сторона первого прямоугольника правее правой стороны второго прямоугольника.
- Когда правая сторона первого прямоугольника левее левой стороны второго прямоугольника.
- Когда нижняя сторона первого прямоугольника выше верхней стороны второго прямоугольника.
- Когда верхняя сторона первого прямоугольника ниже нижней стороны второго прямоугольника.
Если хотя-бы одно из этих четырёх условий верно, то два прямоугольника абсолютно точно не пересекаются. Во всех же остальных случаях они будут пересекаться.
Теперь для класса игровых объектов создам статический метод, который будет всё это реализовывать.
4. Игровой цикл
В игре будут постоянно происходить какие-то события, которые мне нужно будет обрабатывать каждый кадр в реальном времени, а после рисовать результат на холсте. Это называется игровым циклом. В Java Script есть очень удобная функция для создания игрового цикла: setInterval. Она повторно выполняет кусок кода через определённые промежутки времени.
Я создам функцию oneFrameGameCycle, которая будет обрабатывать один кадр игрового цикла. Эту функцию я буду выполнять каждые 25 миллисекунд. То есть частота кадров в моей игре будет 1000 / 25 = 40.
Также я сразу разделил комментариями oneFrameGameCycle на две части. В первой части я буду обрабатывать события, например столкновения игровых объектов. А во второй буду рисовать новый кадр, чтобы игрок увидел последствия произошедших игровых событий. Перед отрисовкой нового кадра нужно стереть старый, для этого я просто нарисую белый прямоугольник размером с холст, он "забелит" прошлый кадр.
5. Управление ракеткой
Теперь создам переменные игрока и шарика. Для шарика отдельно создам переменную его скорости и массив из двух чисел, который является единичным вектором направления шарика. Кирпичиков и стен в игре будет много, поэтому для них я создам массивы. Важно отметить, что размер спрайтов игровых объектов и размер самих игровых объектов должен совпадать.
Теперь нужно каждый кадр рисовать все существующие объекты. Поэтому в конце oneFrameGameCycle пишу этот код:
Объекты уже видны на холсте, но они статичны. Нужно "заставить" их двигаться, начну с ракетки игрока. Создам две булевые переменные отвечающие за направление движения ракетки. С помощью addEventListener можно отслеживать различные события, в том числе нажатия и отпускания клавиш на клавиатуре. В зависимости от нажатой или отпущенной клавиши буду менять направление движения ракетки.
В теле игрового цикла в зависимости от направления движения ракетки буду менять её координаты.
6. Перемещение шарика
Отлично, ракетка уже двигается. Но мяч всё ещё зависает в воздухе, пора это исправить. Для начала я напишу код, который будет отвечать за отталкивание мячика ракеткой, если они сталкиваются боком (то есть либо левая сторона мячика левее левой стороны ракетки, либо правая сторона мячика правее правой стороны ракетки). Это условие будет проверяться сразу после движения ракетки, поэтому скорость ракетки при проверке тоже нужно учитывать.
На следующей схеме красная пунктирная линия обозначает левую сторону мячика в случае если он слева от ракетки и правую сторону мячика если он справа от ракетки. Красная сплошная линия обозначает тоже самое, но с учётом скорости движения ракетки.
Если координата x мячика меньше координаты x ракетки и красная сплошная линия левее чёрной линии, это всё при условии что мячик и ракетка пересекаются, то "телепортирую" мячик так, чтобы его правый край прислонился к левому краю ракетки. Аналогичный алгоритм для правой стороны ракетки. Таким образом ракетка будет передвигать мяч если столкнётся с ним боком.
Для скорости ракетки добавлю переменную playerSpeed. Также сделаю ограничение для движения игрока вбок. Границы находятся на расстоянии 40 пикселей от границ холста потому что я буду добавлять сбоку стены, а их ширина как раз 40 пикселей.
Теперь нужно сделать так, чтобы шарик отскакивал от поверхностей других игровых объектов. Для этого нужно знать, какая сторона шарика столкнулась с поверхностью. Создам четыре булевые переменные для каждой стороны. Далее напишу вспомогательную процедуру, проверяющую столкнулся ли шарик с указанным в качестве параметра объектом. Если да, то проверяю, какой стороной.
Этой процедурой буду проходиться по всем объектам, от которых будет отскакивать шарик.
После всех проверок я буду знать, какими сторонами шарик столкнулся с другими игровыми объектами на данном кадре. Всего можно выделить 4 случая:
На этом рисунке пунктирные линии обозначают игровые объекты, с которыми столкнулся шарик. Столкнувшиеся стороны я закрасил красным. Чёрная стрелочка указывает на изменение направления после столкновения, серая стрелочка указывает на направление до столкновения.
У первых трёх случаев есть по 4 варианта, в итоге получаем 3 * 4 + 1 = 13 возможных видов столкновения шарика. В коде это будет выглядеть довольно громоздко:
Остаётся только вернуть булевые переменные к исходным значениям для проверки на следующем кадре и сдвинуть шарик в выбранном направлении. Но перед этим кое-что подмечу. Как уже стало понятно, шарик только выглядит шариком, на самом деле он, как и остальные игровые объекты является квадратом и столкновения для него я прописывал как для квадрата.
Если просто умножить значения ballDirectionVector на ballSpeed и прибавить это к координатам шарика, то получится, что по диагонали он будет двигаться быстрее, чем по вертикали или горизонтали. Ведь, например, вектор (1, 1) имеет длину больше чем (1, 0). Это можно легко высчитать по теореме Пифагора.
Поэтому я буду каждый кадр вычислять длину направляющего вектора как гипотенузу. Катетами будут значения ballDirectionVector, если поделить их на ранее вычисленную гипотенузу, то получившийся вектор будет единичным, то есть его длина будет равна 1.
Таким образом шарик будет всегда двигаться с одинаковой скоростью независимо от направления. Хотя на данный момент он может двигаться только по вертикали, горизонтали и диагонали. Это делает его траекторию довольно однообразной и в некоторых случаях повторяющейся. Было бы неплохо, чтобы после столкновения шарик отскакивал с небольшой случайной погрешностью в градусах:
Чёрные стрелочки обозначают изменение направления движения после отскока, а серые стрелочки возможную погрешность. Правда нужно ввести ограничение, чтобы шарик не мог слишком сильно отклоняться по горизонтали или вертикали, иначе может случиться ситуация, в которой шарик будет скакать туда сюда, не меняя одну из своих координат. Я решил что минимальные значения ballDirectionVector будут 0,2.
7. Размещение объектов на уровне
Теперь нужно разместить кирпичики и стены. Можно конечно сделать это вручную, но проще будет реализовать это с помощью цикла и двумерного массива.
Разделим холст на клеточки с размером 40x40 пикселей. Получается 1280 / 40 = 32 клетки в ширину и 720 / 40 = 18 клеток в высоту. С помощью массива из 18 строк, у каждой из которых длина 32 символа будем проходиться по этой сетке. В зависимости от символа в строке будем в соответствующей клетке уровня размещать объект, например кирпичик.
Пусть '#' это стены, '*' это кирпичики. Тогда двумерный массив уровня может выглядеть так:
Звёздочки следует помещать по горизонтали через одну, так как ширина одного кирпичика 80 пикселей.
Я специально поместил этот двумерный массив в другой массив levels, в котором будут расположения игровых объектов и для других уровней. Теперь создам функцию для размещения объектов, она в качестве параметра будет принимать как раз такой двухмерный массив.
Я сделал для кирпичиков разные спрайты, они отличаются только цветом. Это нужно просто для разнообразия. Каждый новый кирпичик будет иметь другой цвет.
Вызываю эту функцию в конце скрипта передавая параметром levels[0], открываю браузером index.html и получаю в итоге такую картину:
8. Добавление очков и мелкие правки
Игра почти готова. Напишу код, чтобы шарик, врезаясь в кирпичик, уничтожал его и увеличивал счётчик очков (эту переменную я объявил в начале, где и остальные).
Для удобства проверки, врезался ли шарик именно в кирпичик, я вынес проверку на пересечение объектов из процедуры ballCollision.
Буду рисовать слева сверху надпись: число очков:
Теперь пропишу перезапуск уровня, если шарик улетает за границы холста. А также переход на следующий уровень если все кирпичики на этом уничтожены. При проигрыше игрок будет терять все очки.
Увеличиваю частоту кадров до 100 ФПС, чтобы можно было повышать скорость шарика и не бояться, что он начнёт пролетать сквозь другие объекты. И ещё сделаю зелёный фон вместо белого.
9. Звук и "последние штрихи"
Далее добавлю в игру звуки. Создам папку sounds и помещу туда несколько коротких mp3 файлов. html-элемент audio нужен для встраивания звукового контента в документ, создам несколько таких элементов для каждого звука.
Далее с помощью метода play буду проигрывать эти звуки при столкновении шарика с другим игровым объектом. Но есть небольшая проблема, метод play не будет проигрывать звук пока предыдущий проигрыш этого звука не закончится. Таким образом, если у шарика произойдёт два очень близких по времени столкновения с одним звуком, то этот звук проиграется лишь один раз, ведь второе столкновение произойдёт до конца его проигрыша.
Для исправления этой проблемы я буду обращаться к свойству currentTime, которое хранит в себе значение времени воспроизведения в секундах. Перед новым воспроизведением я буду обнулять его. Но, в таком случае возникает новая проблема. Когда ракетка толкает боком шарик или когда шарик врезается в углы, звук множество раз обнуляется и воспроизводится заново, из-за этого вместо стука слышно неприятное дребезжание. Нужно сделать ограничение, чтобы звук не мог повторяться слишком часто (не чаще чем каждые 0.02 секунды).
Код отвечающий за смену и перезапуск уровней уберу в отдельную функцию которая будет выполняться каждую секунду. Там же напишу код, который будет ускорять мячик в зависимости от набранных очков (максимальная скорость 12). Сделаю чтобы уровни загружались случайно и чтобы мячик при появлении первую секунду имел скорость 0.
Осталось только добавить несколько уровней и игра готова.
10. Заключение
На этом статья подходит к концу. Игра получилась простой, но интересной. Если у кого-то есть идеи по улучшению кода, обязательно пишите. Спасибо за внимание!