Две недели назад я озаботился написанием игры на HTML5 и в процессе разработки возникла куча нестандартных решений, которыми я хочу поделиться в этой статье.


Canvas vs HTML


Первоначально была мысль воспользоваться стандартным средством для написания подобных игр — canvas. Но когда обнаружилось, что будет необходимо обрабатывать много событий от мыши/touch, выбор был сделан в пользу простого HTML. Плюсы этого подхода:

  • Простота создания анимаций
  • Удобство обработки событий от отдельных элементов
  • Возможность привязки аттрибутов к отдельным объектам
  • Применение стилей к группам объектов


Перетаскивание ячеек по полю


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



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

var cell = document.elementFromPoint(pageX, pageY);

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

Для удобства обработки перемещений я добавил каждой ячейке аттрибуты row и column. Вот что получилось в итоге:

function onMouseDown(event) {
  var cell = event.target;

  function onMouseMove(mouseEvent) {
    var row = parseInt(cell.getAttribute('row'));
    var column = parseInt(cell.getAttribute('column'));

    var next = document.elementFromPoint(mouseEvent.clientX, mouseEvent.clientY);
    var nextRow = row + Math.sign(parseInt(next.getAttribute('row')) - row);
    var nextColumn = column + Math.sign(parseInt(next.getAttribute('column')) - column);

    if (Math.abs(nextRow - row) + Math.abs(nextColumn - column) == 1) {
      ... // обработка перемещения ячейки
    }
  }

  function onMouseUp(mouseEvent) {
     field.removeEventListener('mousemove', onMouseMove);
     document.body.removeEventListener('mouseup', onMouseUp);
  }

  field.addEventListener('mousemove', onMouseMove);
  document.body.addEventListener('mouseup', onMouseUp);
}


Кстати, parseInt также можно использовать и для получения ширины, высоты и координат из стилей элемента таким образом (если значения указаны в пикселях):

var width = parseInt(element.style.width);

Drag'n'drop и touch-устройства


Touch-устройства — существенная часть потенциальной аудитории игры и перед публикацией обязательно нужно убедиться, что перетаскивание работает везде и всегда. Казалось бы, нужно всего лишь поставить обработчик onMouseDown для события touchstart, onMouseMove для touchmove и onMouseUp для событий touchend и touchcancel, но нет, все не так просто. Во-первых, для определения координат курсора придется добавить такой код:

function onMouseMove(mouseEvent) {
  var clientX, clientY;
  ...
  if (mouseEvent.touches) {
    clientX = mouseEvent.touches[0].clientX;
    clientY = mouseEvent.touches[0].clientY;
  } else {
    clientX = mouseEvent.clientX;
    clientY = mouseEvent.clientY;
  }
  ...
}


А во-вторых, например, под Android браузер по умолчанию считает, что пользователь пытается пролистать страницу и каждый раз отправляет событие touchcancel. К счастью, этого можно избежать, добавив mouseEvent.preventDefault();. Чтобы браузер в Android не подсвечивал элементы по нажатию на поле, следует добавить следующее правило в стили:

body {
  -webkit-tap-highlight-color: transparent;
}


Во избежание других побочных эффектов рекомендую также добавить:

body {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  touch-action: none;
}


Отображение очков


Когда очки пользователя располагаются в поле с выравниванием текста по центру, возникает желание сделать количество цифр фиксированным и добавить незначащие нули. В JavaScript это можно сделать с помощью такого трюка:

scoreIndicator.innerHTML = '00000'.substr((score + '').length) + score;


Math.sign


Удивительно, но такая простая функция поддерживается еще не всеми браузерами. Вот для нее polyfill:

Math.sign = Math.sign || function(x) {
  x = +x;
  if (x === 0 || isNaN(x)) {
    return x;
  }
  return x > 0 ? 1 : -1;
};


Ionicons


Помимо всем известного Font Awesome, который не очень сочетается с «легкими» шрифтами в интерфейсе игры, есть отличный набор иконок Ionicons.



CDN: //code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css

Google Analytics


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

Создаем обьект Google Analytics:

var GoogleAnalyticsObject = 'analytics';

function analytics(){ 
   analytics.l = 1 * new Date();
   analytics.q = analytics.q || [];
   analytics.q.push(arguments);
};

Подключаем библиотеку; это желательно сделать в конце body:

<script src="//www.google-analytics.com/analytics.js"></script>

При полной загрузке страницы инициализируем Google Analytics и отправляем событие pageview:

window.addEventListener('load', function() {
   analytics('create', 'UA-XXXXXXXX-X', 'auto');
   analytics('send', 'pageview');
}

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

analytics('send', 'event', 'game', 'help');


Где разместить игру?


Помимо традиционных Facebook и ВКонтакте, есть еще такие площадки как Chrome Web Store, Firefox Marketplace, а также многочисленные каталоги типа Clay.io, Playdot, GamePix, Spil Games, Softgames. Для Android и iOS страница элементарно встраивается через WebView, для Steam можно воспользоваться nw.js.

В случае с Android до KitKat нужно быть готовым к небольшим подлостям с WebView. Частично проблемы решается с помощью android:hardwareAccelerated="true" (аппаратное ускорение вообще отключено по умолчанию), но CSS Transitions будут работать только по одному из свойств. Можно, конечно, воспользоваться Crosswalk, но это лишние 20 MiB, что неприемлимо для подобной игры.

Facebook Canvas и nginx


При встраивании iframe Facebook посылает POST-запрос. nginx не поддерживет POST для статических файлов и отдает ошибку 405. Решить эту проблему можно одной директивой:

error_page 405 =200 $uri;

Одна версия для всех соцсетей


При интеграции с соцсетями хочется каким-то образом оставить весь код в одном файле, но при этом использовать разные API в зависимости от того, где встраивается iframe с игрой. Здесь можно воспользоваться шаблонизатором на стороне сервера, но проще посмотреть в document.referrer на URL родительской страницы. Вот рабочий пример для ВКонтакте:

var isVKApp = /vk\.com\/app/.test(document.referrer);

if (isVKApp) {
  var script = document.createElement('script');
  script.src = '//vk.com/js/api/xd_connection.js?2';
  document.body.appendChild(script);
}

window.addEventListener('load', function() {
  if (isVKApp) {
    VK.init();
  }
});


Об игре
Соедините две клетки разного цвета так, чтобы их сумма равнялась числу на бесцветной клетке, и получившийся квадрат передвиньте на эту клетку. По мере получения очков сложность игры возрастает. Достигших 900 очков ждет небольшой сюрприз.

Комментарии (0)