Pull to refresh

История Лошарика

Reading time 6 min
Views 2.7K

Предисловие


imageБыл вечер четверга, когда нам с коллегой spiff пришла в голову идея написать OpenSource игру на прогрессирующем в наше время HTML5, как говорится, from scratch и just for fun.

Так как мы работаем в области системного программирования и опыта разработки web-приложений у нас было совсем немного, было решено реализовать достаточно простой клон, всемирно известной и популярной игры для первых телефонов Nokia — RapidRoll.

Спустя неделю мы выпустили первый стабильный релиз, и готовы поделиться первым полученным опытом.



Шаг 1. Дизайн-документ


Любая игра, которая завоюет мир, на наш взгляд, должна начинаться с дизайн-документа. Обычно он состоит из следующих пунктов:
  • схема игры (игровой процесс, цель игры и что мешает её достижению);
  • интерфейс (основные элементы графики);
  • игровая механика (устройство игрового мира, физика, взаимодействие объектов и т.д.);
  • программные алгоритмы (высокоуровневое описание основных игровых алгоритмов);
  • звуки и музыка;
  • игровой мир (игровые персонажи и их взаимодействие);
  • участники и сроки;

Смысл игры заключается в удержании Лошарика в видимой части экрана, путем его перемещения по поднимающимся платформам, предотвращая его падение или уход за верхнюю границу экрана.

Ссылка на дизайн-документ

Шаг 2. Архитектура


Архитектура приложения отображена на следующей диаграмме:

image

Как видно, диаграмма напоминает модель 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.

image

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

Основной “фишкой” прототипа стал алгоритм генерации платформ — новые платформы генерируются после уничтожения самой верхней и это обеспечивает возможность динамического изменения количества платформ на экране и расстояния между ними. Код алгоритма представлен ниже:

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, с учетом этого, принимается решение — присоединять Лошарика к платформе или нет.

image

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

В результате, на свет появился такой вот код:

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. Третий прототип


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

image

Шаг 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 для более уверенного управления и изменены моменты переключения сложности (уровни) для более продолжительной игры.

image

Статистика


За одну неделю был получен рабочий релиз Лошарика, содержащий 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 за патч, добавляющий вращение Лошарику!
Tags:
Hubs:
+94
Comments 64
Comments Comments 64

Articles