Предисловие
Был вечер четверга, когда нам с коллегой spiff пришла в голову идея написать OpenSource игру на прогрессирующем в наше время HTML5, как говорится, from scratch и just for fun. Так как мы работаем в области системного программирования и опыта разработки web-приложений у нас было совсем немного, было решено реализовать достаточно простой клон, всемирно известной и популярной игры для первых телефонов Nokia — RapidRoll.
Спустя неделю мы выпустили первый стабильный релиз, и готовы поделиться первым полученным опытом.
Шаг 1. Дизайн-документ
Любая игра, которая завоюет мир, на наш взгляд, должна начинаться с дизайн-документа. Обычно он состоит из следующих пунктов:
- схема игры (игровой процесс, цель игры и что мешает её достижению);
- интерфейс (основные элементы графики);
- игровая механика (устройство игрового мира, физика, взаимодействие объектов и т.д.);
- программные алгоритмы (высокоуровневое описание основных игровых алгоритмов);
- звуки и музыка;
- игровой мир (игровые персонажи и их взаимодействие);
- участники и сроки;
Смысл игры заключается в удержании Лошарика в видимой части экрана, путем его перемещения по поднимающимся платформам, предотвращая его падение или уход за верхнюю границу экрана.
Ссылка на дизайн-документ
Шаг 2. Архитектура
Архитектура приложения отображена на следующей диаграмме:

Как видно, диаграмма напоминает модель MVC. Имеются следующие основные классы:
- Game. Основной класс игры, отвечает за взаимодействие остальных классов;
- GameModel. Модель игры, отвечает за формирование игровых сцен;
- KeyboardController. Один из возможных контроллеров для управления игрой с клавиатуры;
- HTML5View. Одно из возможных представлений игры. Отрисовывает сцены, создаваемые моделью на HTML5 canvas, на основе ресурсов.
- Resources. Хранит ресурсы игры, такие как звуки, спрайты и т.д.
Имплементация скелета архитектуры представлена ниже:
function GameModel() { this.setView = function(v) { view = v; }; this.getView = function() { return view; }; this.moveLeft = function() { ... }; this.moveRight = function() { ... }; this.next = function() { // build next/new frame ... view.update(); //update view }; } function HTML5View(canvas) { var model; this.setModel = function(m) { model = m; }; this.getModel = function() { return model; }; var context = canvas.getContext("2d"); this.update = function() { // clear view ... // drawing model to canvas ... }; } function KeyboardController() { var model = null; this.setModel = function(m) { model = m; }; this.getModel = function() { return model; }; this.keydown = function(event) { if (event.keyCode == LEFT_KEY_CODE) { model.moveLeft(); } else if (event.keyCode == RIGHT_KEY_CODE) { model.moveRight(); } }; document.onkeydown = this.keydown; } function Game(model, view, ctrl) { view.setModel(model); ctrl.setModel(model); model.setView(view); this.run = function() { setInterval(model.next, 1000 / DEFAULT_FPS); }; } function main(canvas) { var view = new HTML5View(canvas); var model = new GameModel(); var ctrl = new KeyboardController(); var game = new Game(model, view, ctrl); game.run(); }
Шаг 3. Первый прототип
Спустя сутки был получен первый рабочий прототип, размером в 150 строк кода на HTML5.

В код каркаса были добавлены алгоритмы генерации платформ, перемещения Лошарика и мира. И уже можно было играть, хотя были проблемы с физикой и проверкой попадания на платформы.
Основной “фишкой” прототипа стал алгоритм генерации платформ — новые платформы генерируются после уничтожения самой верхней и это обеспечивает возможность динамического изменения количества платформ на экране и расстояния между ними. Код алгоритма представлен ниже:
var generatePlatforms = function() { do { var type = (Math.random() 0.6) ? PLATFORM_TYPE.SOLID : PLATFORM_TYPE.KILLER; var baseline = pCounter > 0 ? platforms[pCounter - 1].y : 0; platforms[pCounter++] = { x: Math.floor(Math.random() * (WIDTH - DEFAULT_PLATFORM_WIDTH)), y: Math.floor(baseline + (Math.random() * (DEFAULT_MAX_PLATFORM_INTERVAL - DEFAULT_MIN_PLATFORM_INTERVAL + 1)) + DEFAULT_MIN_PLATFORM_INTERVAL), w: DEFAULT_PLATFORM_WIDTH, h: DEFAULT_PLATFORM_HEIGHT, type: type }; } while (platforms[pCounter - 1].y < (HEIGHT + DEFAULT_PLATFORM_HEIGHT)); };
Шаг 4. Второй прототип
Во втором прототипе был пересмотрен алгоритм проверки попадания на платформы и переписан на метод проверки пересечения отрезков. Сначала на основе предыдущего Лошарика строится новый, с учетом предполагаемого перемещения в пространстве. Координаты нового, текущего Лошарика и проверяемой платформы передаются в функцию, которая ищет точки пересечения P1 и P2 и при их нахождении возвращает true, с учетом этого, принимается решение — присоединять Лошарика к платформе или нет.
Позже функция была оптимизирована и выполнялась только для тех платформ, которые могут попасть в пересечение.
В результате, на свет появился такой вот код:
var isRollCrossPlatform = function(platform, rollPrev, rollNext) { // we should to check only platforms between prev roll and next roll if (platform.y <= rollPrev.y || platform.y > rollNext.y + rollNext.h) return false; var s1 = { x1: platform.x, y1: platform.y, x2: platform.x + platform.w, y2: platform.y }; var s2 = { x1: rollPrev.x, y1: rollPrev.y + rollPrev.h - worldSpeed, x2: rollNext.x, y2: rollNext.y + rollNext.h }; var s3 = { x1: rollPrev.x + rollPrev.w, y1: rollPrev.y + rollPrev.h - worldSpeed, x2: rollNext.x + rollNext.w, y2: rollNext.y + rollNext.h }; var zn1 = (s2.y2 - s2.y1) * (s1.x2 - s1.x1) - (s2.x2 - s2.x1) * (s1.y2 - s1.y1); var zn2 = (s3.y2 - s3.y1) * (s1.x2 - s1.x1) - (s3.x2 - s3.x1) * (s1.y2 - s1.y1); if (Math.abs(zn1) < Math.EPS && Math.abs(zn2) < Math.EPS) return false; var ch11 = (s2.x2 - s2.x1) * (s1.y1 - s2.y1) - (s2.y2 - s2.y1) * (s1.x1 - s2.x1); var ch21 = (s1.x2 - s1.x1) * (s1.y1 - s2.y1) - (s1.y2 - s1.y1) * (s1.x1 - s2.x1); if ((ch11/zn1 <= 1.0 && ch11/zn1 >= 0.0) && (ch21/zn1 <= 1.0 && ch21/zn1 >= 0.0)) { return true; } var ch12 = (s3.x2 - s3.x1) * (s1.y1 - s3.y1) - (s3.y2 - s3.y1) * (s1.x1 - s3.x1); var ch22 = (s1.x2 - s1.x1) * (s1.y1 - s3.y1) - (s1.y2 - s1.y1) * (s1.x1 - s3.x1); if ((ch12/zn2 <= 1.0 && ch12/zn2 >= 0.0) && (ch22/zn2 <= 1.0 && ch22/zn2 >= 0.0)) { return true; } return false; };
Во второй прототип были добавлены различные типы платформ: движущиеся, убивающие и таящие и возможность перемещения за боковые границы экрана.
Шаг 4. Третий прототип
Был реализован алгоритм подсчета и отрисовки количества очков, которые кратны пройденному расстоянию в свободном падении, а так же, счетчик жизней и возможность эту самую жизнь потерять.

Шаг 5. Релиз!
Прошла почти неделя с начала проекта и у нас уже был полностью функциональный рабочий прототип, которому до релиза оставалось изменить лишь один класс — HTML5View и добавить немного дополнительных возможностей:
- Фон мира заменен на полноценный рельеф, двигающийся равномерно платформам;
- Таблица рекордов;
- Кнопка “Мне нравится” для интеграции с ВКонтакте;
- Реализована минимальная анимация для Лошарика в виде двух спрайтов (“Лошарик двигается влево”, “Лошарик двигается вправо”);
- Добавлен счетчик FPS;
Вся отрисовка графики в виде canvas фигур, была заменена на отрисовку спрайтов (изображений).
context.fillRect(x, y, width, height) -> context.drawImage(roll, x, y, width, height)
Значение счетчика FPS было решено получать на основе последних 10 фреймов и общего времени их отрисовки. В конечном счете код счетчика выглядит так:
// draw current fps var fps = (frameCounter / totalTime) * 1000.0; context.fillText(fps.toFixed(2) + " fps", 4, HEIGHT - 6); // calc FPS if (frameCounter > FPS_REFRESH_INTERVAL) { frameCounter = 0; totalTime = 0; } frameCounter++; var currentTime = new Date().getTime(); totalTime += (currentTime - lastTime); lastTime = currentTime;
Кроме того, в релизе проработаны коэффициенты физики игры — гравитация, ускорение по X для более уверенного управления и изменены моменты переключения сложности (уровни) для более продолжительной игры.

Статистика
За одну неделю был получен рабочий релиз Лошарика, содержащий 1130 строк кода:
- 163 на PHP для серверной части;
- 50 на CSS для меню и кнопок;
- 71 на HTML5 для отображения;
- 846 на JS для движка игры.
Планы на Лошарика 2.0
- Узнаваемый логотип;
- Новые бонусы (парашют, ядро, бронежилет, ...);
- Улучшенная физика и производительность;
- Монстры;
- Новые типы платформ (деревянные/хрупкие, ...);
- Полностью измененный дизайн и графика;
- Звуки (HTML5 audio);
- Социальная составляющая (Twitter / FB / Google+);
- Сборка для Google web-store (Chrome app);
- Сборки для: Andriod app / WAC app / IPhone app (WebGL implementation);
Вместо заключения
Нам хотелось бы сделать небольшое объявление. Как видно, Лошарику для завоевания мира требуется наличие в команде хорошего дизайнера. Если у кого-то возникнет желание поучаствовать в интересном OpenSource проекте и прокачать свои навыки, милости просим — можете оставлять комментарии или обращаться к spiff.
P.S. Еще раз ссылка на ПОИГРАТЬ!
P.P.S. Лошарик на Googlecode.
UPDATE: спасибо хабрапользователю qmax за патч, добавляющий вращение Лошарику!
