Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Сделай свой Арканоид!

Задача про Арканоид

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

Видео: http://www.youtube.com/watch?v=yVf0sqqUMak
История игры: http://ru.wikipedia.org/wiki/Arkanoid

Предлагаю тебе сделать свой небольшой аналог арканоида при помощи Javascript и Canvas.

  • Нужные знания: основы javascript, основы HTML
  • Уровень сложности: начинающий
  • Время: 2-3 недели

С чего начать

HTML — это язык разметки текста, на котором пишут веб-страницы, которые можно просматривать в браузере. Канвас — это HTML-элемент, который можно поместить на страницу и на котором можно рисовать изображения с помощью яваскрипта. Если ты с ним не знаком, начни со статьи http://habrahabr.ru/post/111308/

Если по ходу решения задачи у тебя будут сложности с яваскриптом — теорию можно почитать на http://learn.javascript.ru

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

ООП в яваскрипте

В JS нет полноценного ООП, но есть его имитация на прототипах (да, придется с ними разбираться). Почитать можно тут: http://learn.javascript.ru/prototype

Вот пример 2 классов, Parent и Child который от него наследуется:

function Parent(...) { ... }

Parent.prototype.method = function (...) { ... };
Parent.prototype.method2 = function (...) { ... };

function Child(...) { .... }
inherit(Child, Parent);

// Перекрываем метод родителя
Child.prototype.method = function (...) {
    // вызываем метод родителя
    Parent.prototype.method.call(this, ...);
    ....
};

Child.prototype.method3 = function (...) { ... };

Код функции inherit ты можешь написать сам: в новых браузерах используя Object.create(), в старых — примерно такую функцию:

function inheritOldJs(child, parent) {
    function F() {};
    F.prototype = parent.prototype;
    child.prototype = new F();
    child.prototype.constructor = parent;
}

Объекты

Мы, естественно, будем использовать ООП, а не писать лапшу из функций и переменных. Для хранения информации об находящихся на поле предметах удобно использовать объекты. Нам понадобятся такие классы:

  • бита (Bat) (свойства: координаты, размер)
  • шарик (Ball) (свойства: координаты, угол движения, скорость движения)
  • кирпичик (Brick) (свойства: координаты)

У объектов этих классов есть общие черты:

  • они помещены на игровое поле
  • они имеют координаты и размеры (и некоторые могут двигаться)
  • они участвуют в столкновениях и реагируют на них
  • они рисуются

Значит, стоит унаследовать классы Bat, Ball, Brick от базового класса, например, GameItem. Мы также создадим в них методы:

  • render(ctx) — отрисовывает объект на канвасе
  • move(dt) — будет вызываться, чтобы движущийся объект обновил свои координаты. Неподвижные объекты в этом методе ничего не делают.
  • onCollisionWithBall(ball) — будет вызываться при ударе об объект шариком (например, пробиваемый с 2 ударов кирпичик увеличит счетчик ударов, а пробиваемый с первого раза — исчезнет). Это мы пока оставим на потом.

Кроме этих классов, нам понадобится еще один, который будет реализовывать игровую логику (создавать объекты, дивгать их, давать им команду на отрисовку). Назовем его GameLoop. Если со временем в нем накопится слишком много кода, придется разбить его на несколько классов.

Загрузка карты уровня

Перед началом игры надо создать и расставить на карте кирпичики. Вот простой способ закодировать карту уровня с помощью массива строк: обозначим пробелом пустое место, а буквой A — кирпичик (потом можно использовать другие буквы и цифры для кодирования цвета и типа кирпичика). Тогда массив с картой будет выглядеть примерно так:

var map = [
    "AAAAAAAAAAA",
    "AA AA AA AA",
    "A  A  A  A "
];

Код загрузки (его можно сделать методом loadMap() в классе GameLoop) должен на основе такой карты создавать массив объектов-кирпичиков с нужными координатами.

Шарик и биту можно создать вручную в нужных координатах (в оригинальной игре шарик неподвижен до первого движения биты). Их вполне можно хранить как свойства в классе GameLoop.

Общая логика игры

  • загрузка уровня
  • создание биты и шарика
  • запуск главного цикла

Главный цикл

В играх обычно используется архитектура в виде game loop — бесконечного цикла, который выполняется все время, пока идет игра. Вот примерная последовательность действий в нем:

  • сбор информации о нажатых клавишах
  • перемещение автоматически двигающихся объектов (шарик) с проверкой столкновений
  • перемещение управляемых игроком объектов (бита) с проверкой столкновений
  • если игрок проиграл или выиграл, выход из цикла
  • перерисовка экрана
  • (возможно пауза для ограничения fps)
  • переход к началу цикла

Как ты видишь, на каждом шаге игрового цикла происходит перерисовка экрана, и число выполненных шагов в секунду совпадает с fps (числом кадров в секунду). Поскольку монитор обновляется с какой-то фиксированной частотой (для LCD это обычно 60 Гц что соответствует 16 мс на кадр), в конце цикла иногда делают паузу, если он выполнился слишком быстро, так как обновлять картинку в видеобуфере быстрее, чем обновляется монитор, нет смысла. Это называется ограничение fps.

В браузере создать вечный цикл не получится: браузер блокируется на то время, пока выполняется скрипт, потому тут используют другой подход: мы создаем таймер, который срабатывает 60 раз в секунду и вызывает нашу функцию (в котороый мы делаем все вышеперечисленные действия, после чего возвращаем управление браузеру). Такой таймер в старых браузерах делали с помощью setTimeout, но в новых браузерах есть кое-что получше: requestAnimationFrame()

Преимущества использования этой функции:

  • браузер ограничивает частоту вызова этой функции частотой обновления монитора (чаще 60 раз в секунду она твой код не вызвовет)
  • некоторые браузеры на некоторых платформах синхронизируют вызов кода с VSync (паузой между обновлением картинки на мониторе) и тем самым позволяют избежать от эффекта tearing ( http://en.wikipedia.org/wiki/Screen_tearing ). Увы, пока не везде.
  • если вкладка не видна на экране, браузер может снизить частоту вызова твоей функции до 1 раза в секунду (зачем перерисовывать картинку, которую пользователь все равно не увидит?)

Почитать про ее использование можно тут: http://habrahabr.ru/post/114358/

Заметь также, что наш код может по разным причинам вызываться с неравномерными интервалами. Если движущиеся объекты будут сдвигаться на одинаковое расстояние каждый раз, то в итоге движение может получиться дерганым — потому надо измерять время dt, прошедшее с момента последнего вызова нашей функции и рассчитывать на сколько за это время сдвинулся шарик.

Текущее время в миллисекундах можно получить с помощью var t = +new Date() (это короткая запись для var t = (new Date()).getTime()).

Нажатия клавиш

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

Определение столкновений (collision detection)

Эта проблема встречается встречается во многих играх, и есть разные алгоритмы ее решения. В нашем случае все упрощается тем, что двигаются у нас только шарики, они могут столкнуться только с 3 горизонтальными или вертикальными поверхностями:

  • край поля
  • бита
  • края кирпичиков

Таким образом мы можем обойтись без сложных физических моделей.

Частичная перерисовка

Во многих играх экран перерисовывается полностью на каждом шаге игрового цикла. Но в этой игре, если подумать, большая часть игровой области остается неизменной: движутся лишь бита и шарик, и иногда исчезают кирпичики. Логично было бы не греть зря процессор, а завести так называемый dirty regions list - список изменившихся областей экрана, который формируется при движении или изменении объектов, и перерисовывать только указанные в нем части экрана.

Расширения

Позже можно добавить разные цвета и типы кирпичиков, непробиваемые кирпичи и призы (для них стоит завести отдельный класс), которые из них вываливаются.

Анимация и эффекты

Было бы неплохо, если кирпичики исчезали не резко, а плавно, а непробиваемые кирпичики вспыхивали при ударе. Для анимации стоит завести отдельные классы: AnimatedEffect, представляющий собой одну анимацию одного объекта и AnimationList, класс, который существует в одном экземпляре, содержит список выполняющихся эффектов и вызывает их с нужной частотой. Анимация также должна добавлять затронутые регионы в dirty list.

Меню и экраны

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

  • стартовый экран
  • экран проигрыша
  • экран победы

И сделать логику перехода между ними и игровым циклам.

Неймспейсы

Стоит поместить все функции, классы, переменные игры в один неймспейс, например, Arcanoid, таким образом:

// создаем единcтвенную глобальную переменную
var Arcanoid = {};
Arcanoid.SOMETHING = 100;
Arcanoid.SomeClass = function (...) { ... }

Сохранение рекордов

Было бы неплохо подсчитывать очки и сохранять рекорд на сервер, но как проверить, что злодей не отсылает подделанные цифры? Один из вариантов — записывать на клиентской стороне полный лог (все действия и события с временем) игры и проверять на сервере его на правильность. Его конечно, тоже можно подделать, но гораздо сложнее чем просто поменять цифры.

Сетевая игра

Не хочешь попробовать добавить игру по сети?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.