Моделирование объектов для анимации на Canvas

от автора

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

Изображение - сила 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-ы (здесь не рассмотрено).

К проектированию модели надо отнестись внимательно с самого начала. Если в процессе разработки придётся значительно изменять модель, это повлечёт за собой переделку управляющего и отображающего кода.

Разработка модели

Из тетриса получилось хорошее демо, чтобы протестировать подход (здесь я далеко не первый).

Концепция модели:

  • Блоки, состоящие из цветных ячеек, падают в колодец и переходят в общий пул. Заполненный ряд пула удаляется. Игра заканчивается, когда колодец заполнен и для нового блока нет свободного места.
  • Блоки можно вращать и двигать по горизонтали. Вращение и движение возможно только в свободном пространстве колодца. При вращении блока и при заполнении пула колодца, цвет ячеек сохраняется.
  • Базовый объект — ячейка. Ячейка имеет размер в пикселях. Этот размер является основной базой, на основании которой происходит вся отрисовка на 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

ссылка на оригинал статьи http://habrahabr.ru/post/259465/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *