Предисловие
Был вечер четверга, когда нам с коллегой 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 за патч, добавляющий вращение Лошарику!