Быстрый и простой API, поддержка браузерами — это то, что делает Canvas привлекательным. Но, как это часто бывает, простота одновременно является и слабой стороной. Без труда, например, можно вывести прямоугольник, окружность, линию или навесить изображение. Но разработать на этой простой основе полезный контент — задача чуть сложнее.
На примере разработки игры, показан подход к анимации и управлению игровым объектом.
Побудительным мотивом поиграться с разработкой игры на Canvas, стали два этих примера:
Примеры наглядные, но, погоняв пару минут шары и разорвав полдюжины сеток, азарт пропал. Нет сюжета, нет цели, нет смысла — в итоге прикольно, но не интересно.
Работа с Canvas была описана в публикации HTML-страница на Canvas. В целом, точно такой же подход, включая архитектуру и средства разработки, применялся и здесь.
Совместимость проверялась на FF (Windows, Linux Mint), CR и IE (Windows). Проверка на доступных гаджетах тоже была, но без особого результата (об этом в конце).
Код на GitHub:
Arcad
Spots
Tens
Tetr
Демо на GitHub (с автономным режимом):
Arcad
Spots
Tens
Tetr
Игра состоит: из игровой модели, функционала управления моделью и функционала отображения модели. Конечная суть игры сводится к изменению модели.
Модель состоит из игровых объектов. Объекты из базовых примитивов: прямоугольников, окружностей, линий.
Модель и функционал, обеспечивающий динамику модели, ничего не знают о том, как модель будет показана. Функционал Canvas знает только то, как отображать статическое состояние модели, на момент отрисовки. В процессе отрисовки, изменения модели недопустимы. К счастью, за этим следить не надо (обеспечивается однопоточностью event loop-а).
Canvas API нативный, а потому быстрый, и может в реальном времени отобразить большое количество базовых примитивов. Поэтому количество (десятки и даже сотни) простых объектов не столь критичны для Canvas. А вот сложные вычислительные объекты, реализованные на JavaScript, это то, чего следует избегать. То-же самое можно отнести и к реализации алгоритмов изменения модели. Чем они будут проще и нативней (нативней, в данном случае, будут учитывать Canvas API), тем лучше. Если, по замыслу игры, в ней надо реализовать сложные и/или длительные по времени выполнения вычислительные алгоритмы, то, для таких целей, можно использовать web worker-ы (здесь не рассмотрено).
К проектированию модели надо отнестись внимательно с самого начала. Если в процессе разработки придётся значительно изменять модель, это повлечёт за собой переделку управляющего и отображающего кода.
Из тетриса получилось хорошее демо, чтобы протестировать подход (здесь я далеко не первый).
Концепция модели:
Описание колодца и размера ячейки:
Базовый объект (ячейка):
Объект (блок):
Видна избыточность модели блока, но за счёт этого решаются две задачи: 1. блок со всеми состояниями формируется за один раз (дальше управлять блоком просто); 2. сохраняются цвета ячеек при вращении блока.
Блоки создаются в режиме игры:
Новый блок:
Здесь можно добавить блоки любой конфигурации. Главное присвоить блоку новый тип и добавить этот тип в массив в функции APELSERG.MODEL.GetBlockType().
Управление происходит с клавиатуры типовым образом:
Блоки падают по таймеру (а не по window.requestAnimationFrame). В массиве APELSERG.CONFIG.SET.LevelTick хранится величина периода времени для текущего уровня:
Для проверки — может ли двигаться или вращаться блок в заданном направлении — создаётся виртуальный блок и проверяются условия. Если условия проверки выполняются, то реальный блок перемещается на новое место. Функция проверки:
Смещение блока происходит простым персчётом номеров ячеек:
При достижении дна — ячейки из блока перемещаются в пул колодца функцией APELSERG.MODEL.DropBlockToPool(). При этом начисляются очки.
Функционал, который реализован, но, на мой взгляд, не очень удался (в настройках его нет):
За отрисовку модели на Canvas отвечает функция APELSERG.CANVA.WellRewrite(). Она очень простая и хорошо документированная. Всё что она делает — очищает Canvas и последовательно отрисовывает примитивы модели.
После того, как модель ожила, стало ясно, что для целостной игры одной ожившей модели мало. Так появились модули UI:
Это типовой динамический DOM, возможно не самый удачный (код простой, описывать не буду).
Для поддержания интереса к игре, необходимо хранить настройки, результаты и, возможно, полезен автономный режим. Для хранения используется localStorage. Технологически, всё реализовано типовым образом, но полезно проследить связь с глобальными объектами APELSERG.CONFIG.SET и APELSERG.CONFIG.RESULT.
Несколько замечаний:
Конфигурация храниться в двух объектах:
Имя для хранения должно быть уникальным и формируется из комбинации нескольких статических переменных:
Конфигурация сохраняется при её изменении. Делается это просто (даже на отдельную функцию не потянуло). В функции APELSERG.UI.ApplySettings() (модуль UI), добавлены две строчки:
При старте приложения проверяется наличие в localStorage сохранённой конфигурации и, если конфигурация была сохранена, она восстанавливается:
LocalStorage может быть пустым или не использоваться вовсе. Пустым localStorage бывает: 1. при первом запуске; 2. если не был сохранён; 3. если был очищен. Очистка конфигурации бывает нужна, в основном, в процессе разработки. Например, конфигурация изменилась — были добавлены или убраны переменные, а приложение продолжает работать, как будто не видит изменений, так как из хранилища восстанавливается старый объект конфигурации.
Если запуск приложения был произведён с локального диска, то локальное хранилище отключается. Так сделано, потому что браузеры не очень хорошо поддерживают этот режим. Но остальная функциональность приложения сохраняется. Запуск с локального диска контролируется при старте:
Результаты хранятся в APELSERG.CONFIG.RESULT. Функционально, хранение результатов идентично хранению конфигурации.
Режим автономной работы (Application Cache или AppCache) позволяет продолжать работу с веб-приложением при отключённом интернете. В общем виде, настройка разных условий автономного режима, может быть достаточно сложной. Но, в нашем случае, это одна из самых простых процедур.
Надо подготовить файл манифеста для автономного режима (game_arcad_plain.appcache.txt):
Надо добавить в HTML элемент веб-страницы ссылку на этот файл:
Тонкий момент с расширением «txt». Рекомендуется расширение «appcache» или «manifest» с MIME-типом «text/cache-manifest». В демо так сделано, потому что былолениво любопытно.
"# 001" нужен для перезагрузки файлов на клиенте по инициативе сервера. Если на сервере обновили файлы, то они не попадут на клиента, пока не изменится файл манифеста. А что в нём можно изменить? — комментарий на "# 002".
После того, как была разработана первая игра, остальные игры штамповались по образцу. Процентов 80 кода оставалось идентичным, а изменения касались, в основном, только модели (модель и код управления стали проще). Поэтому описывать эти игры отдельно не имеет смысла, за исключением нескольких нюансов:
Что хорошо:
Что не очень хорошо:
HTML5 Canvas
HTML5 Local Storage
HTML5 Application Cache
На примере разработки игры, показан подход к анимации и управлению игровым объектом.
Побудительным мотивом поиграться с разработкой игры на Canvas, стали два этих примера:
Примеры наглядные, но, погоняв пару минут шары и разорвав полдюжины сеток, азарт пропал. Нет сюжета, нет цели, нет смысла — в итоге прикольно, но не интересно.
Вводная часть
Работа с Canvas была описана в публикации HTML-страница на Canvas. В целом, точно такой же подход, включая архитектуру и средства разработки, применялся и здесь.
Совместимость проверялась на FF (Windows, Linux Mint), CR и IE (Windows). Проверка на доступных гаджетах тоже была, но без особого результата (об этом в конце).
Код на GitHub:
Arcad
Spots
Tens
Tetr
Демо на GitHub (с автономным режимом):
Arcad
Spots
Tens
Tetr
Игра — это управление моделью
Игра состоит: из игровой модели, функционала управления моделью и функционала отображения модели. Конечная суть игры сводится к изменению модели.
Модель состоит из игровых объектов. Объекты из базовых примитивов: прямоугольников, окружностей, линий.
Модель и функционал, обеспечивающий динамику модели, ничего не знают о том, как модель будет показана. Функционал Canvas знает только то, как отображать статическое состояние модели, на момент отрисовки. В процессе отрисовки, изменения модели недопустимы. К счастью, за этим следить не надо (обеспечивается однопоточностью event loop-а).
Canvas API нативный, а потому быстрый, и может в реальном времени отобразить большое количество базовых примитивов. Поэтому количество (десятки и даже сотни) простых объектов не столь критичны для Canvas. А вот сложные вычислительные объекты, реализованные на JavaScript, это то, чего следует избегать. То-же самое можно отнести и к реализации алгоритмов изменения модели. Чем они будут проще и нативней (нативней, в данном случае, будут учитывать Canvas API), тем лучше. Если, по замыслу игры, в ней надо реализовать сложные и/или длительные по времени выполнения вычислительные алгоритмы, то, для таких целей, можно использовать web worker-ы (здесь не рассмотрено).
К проектированию модели надо отнестись внимательно с самого начала. Если в процессе разработки придётся значительно изменять модель, это повлечёт за собой переделку управляющего и отображающего кода.
Разработка модели
Из тетриса получилось хорошее демо, чтобы протестировать подход (здесь я далеко не первый).
Концепция модели:
- Блоки, состоящие из цветных ячеек, падают в колодец и переходят в общий пул. Заполненный ряд пула удаляется. Игра заканчивается, когда колодец заполнен и для нового блока нет свободного места.
- Блоки можно вращать и двигать по горизонтали. Вращение и движение возможно только в свободном пространстве колодца. При вращении блока и при заполнении пула колодца, цвет ячеек сохраняется.
- Базовый объект — ячейка. Ячейка имеет размер в пикселях. Этот размер является основной базой, на основании которой происходит вся отрисовка на Canvas.
- Колодец — виртуальная сетка, состоящая из ячеек. Управление блоком и пулом — в изменении номера колонки и ряда ячейки в колодце.
Описание колодца и размера ячейки:
APELSERG.CONFIG.SET.CellSize = 20; // размер базового объекта в пикселях
APELSERG.CONFIG.SET.WellColumn = 5; // ширина колодца в базовых объектах
APELSERG.CONFIG.SET.WellRow = 20; // глубина колодца в базовых объектах
Базовый объект (ячейка):
APELSERG.MODEL.Cell = function (cellRow, cellCol, cellColor) {
this.Row = cellRow; // номер ряда в колодце
this.Col = cellCol; // номер колонки в колодце
this.Color = cellColor; // цвет
}
Объект (блок):
APELSERG.MODEL.Block = function (blockType) {
this.type = blockType; // номер внешнего вида блока
this.idx = 0; // текущее состояние вращения блока (как повёрнут)
this.cells = [[], [], [], []]; // четыре возможных состояния вращения
}
Видна избыточность модели блока, но за счёт этого решаются две задачи: 1. блок со всеми состояниями формируется за один раз (дальше управлять блоком просто); 2. сохраняются цвета ячеек при вращении блока.
Блоки создаются в режиме игры:
APELSERG.MODEL.GetNewBlock = function() {
var newBlock = APELSERG.CONFIG.PROC.NextBlock; // ранее сформированный блок нужен для предпросмотра
APELSERG.CONFIG.PROC.NextBlock = APELSERG.MODEL.GetBlock(); // новый блок - в предпросмотр
if (!APELSERG.MODEL.CheckBlockCross(newBlock)) { // проверка, что колодец не заполнен
APELSERG.CONFIG.PROC.GameStop = true; // завершить игру
window.clearTimeout(APELSERG.CONFIG.PROC.TimeoutID); // сбросить таймер
APELSERG.CONFIG.SetResult(); // записать результат
}
return newBlock;
}
Новый блок:
APELSERG.MODEL.GetBlock = function() {
var blockType = APELSERG.MODEL.GetBlockType(); // выбрать тип случайным образом
var newBlock = new APELSERG.MODEL.Block(blockType); // пустой объект для блока
var newColor = "";
// начальная позиция
var baseRow = 1;
var baseCol = Math.round(APELSERG.CONFIG.SET.WellColumn / 2);
switch (blockType) {
case 101:
//-- [1]
newColor = APELSERG.MODEL.GetCellColor();
newBlock.cells[0][0] = new APELSERG.MODEL.Cell(baseRow, baseCol, newColor);
newBlock.cells[1][0] = new APELSERG.MODEL.Cell(baseRow, baseCol, newColor);
newBlock.cells[2][0] = new APELSERG.MODEL.Cell(baseRow, baseCol, newColor);
newBlock.cells[3][0] = new APELSERG.MODEL.Cell(baseRow, baseCol, newColor);
break;
// Далее описание всех типов блоков
Здесь можно добавить блоки любой конфигурации. Главное присвоить блоку новый тип и добавить этот тип в массив в функции APELSERG.MODEL.GetBlockType().
Управление моделью
Управление происходит с клавиатуры типовым образом:
window.addEventListener('keydown', function (event) {
...
}
Блоки падают по таймеру (а не по window.requestAnimationFrame). В массиве APELSERG.CONFIG.SET.LevelTick хранится величина периода времени для текущего уровня:
APELSERG.MAIN.Animation = function (makeStep) {
if (makeStep) {
APELSERG.MODEL.BlockShift('DOWN'); // окончание игры срабатывает здесь - устанавливается флаг после проверки нового блока
}
APELSERG.CANVA.WellRewrite(APELSERG.CONFIG.PROC.CellPool);
if (!APELSERG.CONFIG.PROC.GameStop && !APELSERG.CONFIG.PROC.GamePause) {
APELSERG.MAIN.RequestAnimationFrame(function () {
APELSERG.MAIN.Animation(true);
});
}
}
APELSERG.MAIN.RequestAnimationFrame = function (callback) {
if (APELSERG.CONFIG.PROC.FastDownFlag) { // сброс как последовательный DOWN
APELSERG.CONFIG.PROC.TimeoutID = window.setTimeout(callback, 10);
}
else {
APELSERG.CONFIG.PROC.TimeoutID = window.setTimeout(callback, APELSERG.CONFIG.SET.LevelTick[APELSERG.CONFIG.SET.Level]);
}
}
Для проверки — может ли двигаться или вращаться блок в заданном направлении — создаётся виртуальный блок и проверяются условия. Если условия проверки выполняются, то реальный блок перемещается на новое место. Функция проверки:
APELSERG.MODEL.CheckBlockCross = function(block) {
var canShift = true;
//-- проверить границы колодца
//--
for (var n = 0 in block.cells[block.idx]) {
if (block.cells[block.idx][n].Col < 1 || block.cells[block.idx][n].Col > APELSERG.CONFIG.SET.WellColumn ||
block.cells[block.idx][n].Row < 1 || block.cells[block.idx][n].Row > APELSERG.CONFIG.SET.WellRow) {
canShift = false;
break;
}
}
//-- проверить границы пула
//--
if (canShift) {
for (var n = 0 in block.cells[block.idx]) {
for (var q = 0 in APELSERG.CONFIG.PROC.CellPool) {
var cell = APELSERG.CONFIG.PROC.CellPool[q];
if (block.cells[block.idx][n].Col == cell.Col && block.cells[block.idx][n].Row == cell.Row) {
canShift = false;
break;
}
}
if (!canShift) {
break;
}
}
}
return canShift;
}
Смещение блока происходит простым персчётом номеров ячеек:
APELSERG.MODEL.ShiftBlockColumn = function(block, num) {
for (var x = 0 in block.cells) {
for (var n = 0 in block.cells[x]) {
block.cells[x][n].Col = block.cells[x][n].Col + num;
}
}
}
При достижении дна — ячейки из блока перемещаются в пул колодца функцией APELSERG.MODEL.DropBlockToPool(). При этом начисляются очки.
Функционал, который реализован, но, на мой взгляд, не очень удался (в настройках его нет):
- APELSERG.CONFIG.PROC.FastDownFlag = false. Если установить в true, то падение будет не моментальным, а визуализированным.
- APELSERG.CONFIG.SET.ShowFullRow = false. Если установить в true, то будет показан заполненный ряд перед удалением.
- APELSERG.CONFIG.SET.SlideToFloor = false. Если установить в true, то блок после сброса будет «скользить» по полу и перейдёт в пул только по тику таймера.
Отрисовка на Canvas
За отрисовку модели на Canvas отвечает функция APELSERG.CANVA.WellRewrite(). Она очень простая и хорошо документированная. Всё что она делает — очищает Canvas и последовательно отрисовывает примитивы модели.
Интерфейс пользователя
Настройки, результаты, помощь
После того, как модель ожила, стало ясно, что для целостной игры одной ожившей модели мало. Так появились модули UI:
- APELSERG.UI. Для обеспечения интерфейсов к настройкам, результатам и помощи.
- APELSERG.LANG. Для обеспечения простой локализации.
Это типовой динамический DOM, возможно не самый удачный (код простой, описывать не буду).
Локальное хранилище
Для поддержания интереса к игре, необходимо хранить настройки, результаты и, возможно, полезен автономный режим. Для хранения используется localStorage. Технологически, всё реализовано типовым образом, но полезно проследить связь с глобальными объектами APELSERG.CONFIG.SET и APELSERG.CONFIG.RESULT.
Несколько замечаний:
- Для каждого домена используется свой localStorage.
- Стоит с большой осторожностью применять localStorage.clear() — очистит весь localStorage для текущего домена.
Конфигурация храниться в двух объектах:
- CONFIG.SET — статические настройки, которые применяются в момент старта приложения (сохраняются в localStorage).
- CONFIG.PROC — динамические настройки, которые применяются в процессе работы приложения (не сохраняются в localStorage).
Имя для хранения должно быть уникальным и формируется из комбинации нескольких статических переменных:
APELSERG.CONFIG.SET.Version = "0-1-0"
APELSERG.CONFIG.SET.LocalStorageName = "APELSERG-ArcadPlain";
APELSERG.CONFIG.GetLocalStorageConfigName = function () {
return APELSERG.CONFIG.SET.LocalStorageName + "-Config-" + APELSERG.CONFIG.SET.Version;
}
Конфигурация сохраняется при её изменении. Делается это просто (даже на отдельную функцию не потянуло). В функции APELSERG.UI.ApplySettings() (модуль UI), добавлены две строчки:
var configName = APELSERG.CONFIG.GetLocalStorageConfigName();
localStorage[configName] = JSON.stringify(APELSERG.CONFIG.SET);
При старте приложения проверяется наличие в localStorage сохранённой конфигурации и, если конфигурация была сохранена, она восстанавливается:
APELSERG.CONFIG.GetConfigOnLoad = function () {
if (APELSERG.CONFIG.PROC.LoadFromWeb) {
var configName = APELSERG.CONFIG.GetLocalStorageConfigName();
if (localStorage[configName] !== undefined) {
APELSERG.CONFIG.SET = JSON.parse(localStorage[configName]);
}
}
}
LocalStorage может быть пустым или не использоваться вовсе. Пустым localStorage бывает: 1. при первом запуске; 2. если не был сохранён; 3. если был очищен. Очистка конфигурации бывает нужна, в основном, в процессе разработки. Например, конфигурация изменилась — были добавлены или убраны переменные, а приложение продолжает работать, как будто не видит изменений, так как из хранилища восстанавливается старый объект конфигурации.
Если запуск приложения был произведён с локального диска, то локальное хранилище отключается. Так сделано, потому что браузеры не очень хорошо поддерживают этот режим. Но остальная функциональность приложения сохраняется. Запуск с локального диска контролируется при старте:
window.location.protocol == "file:" ? APELSERG.CONFIG.PROC.LoadFromWeb = false : APELSERG.CONFIG.PROC.LoadFromWeb = true;
Результаты хранятся в APELSERG.CONFIG.RESULT. Функционально, хранение результатов идентично хранению конфигурации.
Автономная работа
Режим автономной работы (Application Cache или AppCache) позволяет продолжать работу с веб-приложением при отключённом интернете. В общем виде, настройка разных условий автономного режима, может быть достаточно сложной. Но, в нашем случае, это одна из самых простых процедур.
Надо подготовить файл манифеста для автономного режима (game_arcad_plain.appcache.txt):
CACHE MANIFEST # Ver 0.1.0 # 001 game_tetr_plain.htm game_tetr_plain_canva.js game_tetr_plain_config.js game_tetr_plain_lang.js game_tetr_plain_main.js game_tetr_plain_model.js game_tetr_plain_model_blocks.js game_tetr_plain_ui.js
Надо добавить в HTML элемент веб-страницы ссылку на этот файл:
<html manifest="game_arcad_plain.appcache.txt">
Тонкий момент с расширением «txt». Рекомендуется расширение «appcache» или «manifest» с MIME-типом «text/cache-manifest». В демо так сделано, потому что было
"# 001" нужен для перезагрузки файлов на клиенте по инициативе сервера. Если на сервере обновили файлы, то они не попадут на клиента, пока не изменится файл манифеста. А что в нём можно изменить? — комментарий на "# 002".
Другие игры
После того, как была разработана первая игра, остальные игры штамповались по образцу. Процентов 80 кода оставалось идентичным, а изменения касались, в основном, только модели (модель и код управления стали проще). Поэтому описывать эти игры отдельно не имеет смысла, за исключением нескольких нюансов:
- Поскольку в этих играх есть постоянно движущийся объект (шарик), анимация выполнена на window.requestAnimationFrame.
- Шарик движется медленнее/быстрее не за счёт изменения интервала времени, а за счёт изменения приращения координат по X и Y. Интервалы времени не измеряются (а, по-хорошему бы, надо).
- Отскок считается от центра шара, с учётом диаметра. Ускорение и направление выбирается случайным образом. При обратном движении, шар ракетку не видит. Толщина блока/ракетки не могут быть тоньше размера шара, иначе, на больших скоростях, могут быть сквозные проскоки.
Краткие тестовые выводы
Что хорошо:
- Анимация на Canvas проста. Модель делает работу с Canvas ещё проще.
- Локальное хранилище и режим автономной работы просты в использовании.
Что не очень хорошо:
- Canvas плохо дружит с тач-скринами. Даже если приложение реагирует на тач-скрин, всё-равно поведение на клик мыши на 20-ти дюймовом дисплее, и на тыканье пальцем в экран смартфона будут отличаться. Поэтому демо-игры предназначены, в основном, для использования на десктопных системах, а управление ориентировано на клавиатуру. Позитив в том, что имея, относительно несложное, работающее десктопное приложение, можно приступить к его дальнейшей адаптации.
- Canvas, периодически, немного лагает. Механизмы для борьбы отсутствуют.
Полезные ссылки
HTML5 Canvas
HTML5 Local Storage
HTML5 Application Cache