Графіка і анімація - найбільш фундаментальні аспекти будь відеоігри, тому я почну цю статтю з короткого огляду 2D API Canvas, а потім розповім про реалізацію основної анімації в грі Snail Bait. З цієї статті ви дізнаєтеся:
- як малювати зображення і графічні примітиви на полотні;
- досягти гладкою анімації без мерехтіння;
- реалізувати ігровий цикл;
- контролювати швидкість анімації в кадрах в секунду;
- прокручувати фон гри;
- використовувати паралакс для імітації трьох вимірів;
- реалізувати рух на основі часу.
Кінцевий результат усього програмування, описаного в цій статті, показаний на малюнку 1.
Малюнок 1. Прокрутка фону і контроль частоти кадрів
Горизонтальна прокрутка фону і платформ. Платформи знаходяться на передньому плані, тому вони рухаються помітно швидше, ніж фон, створюючи м'який ефект паралакса. На початку гри фон прокручується справа наліво. В кінці рівня фон і платформи змінюють напрямок руху.
Бігун на даному етапі розробки не рухається. Крім того, в грі ще немає виявлення зіткнень, тому коли під бігуном немає платформи, він повисає в повітрі.
Значки над полотном гри зліва згодом будуть вказувати кількість залишилися життів (як показано на малюнку 1 в першій статті циклу ). Поки ж в цьому місці відображається поточна швидкість анімації в кадрах в секунду.
Перш ніж продовжити, спробуйте пограти в гру, як вона представлена на малюнку 1 - вам буде легше зрозуміти її код. (Реалізація Snail Bait для цієї статті приведена в розділі завантаження ).
2D-контекст Canvas є великий графічний API, який дозволяє реалізувати всі від текстових редакторів до відеоігор-платформер. На момент написання цієї статті API містить більше 30 методів, але для Snail Bait використовується всього жменька з них, показана в таблиці 1.
Таблиця 1. Методи 2D-контексту Canvas, використовувані в грі Snail Bait
Метод Опис drawImage () Створює зображення або його частину в певному місці полотна. Можна також намалювати інший полотно або кадр з елемента video. save () Зберігає атрибути контексту в стеці. restore () Витягує атрибути контексту з стека і застосовує їх до контексту. strokeRect () Малює прямокутний контур. fillRect () Заповнює прямокутник. translate () Перетворення системи координат. Це потужний метод, який використовується в багатьох сценаріях. Вся прокрутка в Snail Bait здійснюється тільки шляхом виклику цього методу.
Всі елементи Snail Bait, за винятком платформ, - це растрові зображення. Фон, бігун і всі предмети і монстри - зображення, які малюються за допомогою методу drawImage ().
В кінцевому підсумку Snail Bait буде застосовувати spritesheet - загальну сторінку, яка містить всю графіку гри, - але поки я буду використовувати окремі зображення фону і бігуна. Бігуна я малюю за допомогою функції, показаної в лістингу 1 .
Лістинг 1. Промальовування бігуна
function drawRunner () {context.drawImage (runnerImage, // зображення STARTING_RUNNER_LEFT, // полотно вліво calculatePlatformTop (runnerTrack) - RUNNER_HEIGHT); // полотно вгору}
Функція drawRunner () передає у drawImage () три аргументи: зображення і ліву і верхню координати для його розміщення на полотні. Ліва координата - це константа, тоді як верхня координата визначається платформою, на якій знаходиться бігун.
Аналогічним чином намалюємо фон, як показано в лістингу 2.
Лістинг 2. Промальовування фону
function drawBackground () {context.drawImage (background, 0, 0); }
Функція drawBackground () з лістингу 2 малює фонове зображення в точці полотна з координатами (0,0). Пізніше ми змінимо цю функцію для прокрутки фону.
Для малювання платформ, які не є растровими зображеннями, потрібно більш широке використання API Canvas, як показано в лістингу 3.
Лістинг 3. Малювання платформ
var platformData = [// Екран 1 .......................................... ............. {left: 10, width: 230, height: PLATFORM_HEIGHT, fillStyle: 'rgb (150,190,255)', opacity: 1.0, track: 1, pulsate: false,},. ..], ... function drawPlatforms () {var data, top; context.save (); // Збереження поточного стану контексту context.translate (-platformOffset, 0); // Перетворення системи координат // для всіх платформ for (var i = 0; i <platformData.length; ++ i) {data = platformData [i]; top = calculatePlatformTop (data.track); context.lineWidth = PLATFORM_STROKE_WIDTH; context.strokeStyle = PLATFORM_STROKE_STYLE; context.fillStyle = data.fillStyle; context.globalAlpha = data.opacity; context.strokeRect (data.left, top, data.width, data.height); context.fillRect (data.left, top, data.width, data.height); } Context.restore (); // Відновлення збереженого стану контексту}
Код JavaScript, наведений в лістингу 3 , Визначає масив з ім'ям platformData. Кожен об'єкт цього масиву є метадані, які описують окрему платформу.
Функція drawPlatforms () використовує методи strokeRect () і fillRect () контексту Canvas для малювання прямокутників платформ. Характеристики цих прямокутників - які зберігаються в об'єктах масиву platformData - використовуються для завдання стилю заливки контексту і атрибута globalAlpha, який задає ступінь непрозорості всього, що згодом буде зображено на полотні.
Виклик context.translate () зрушує систему координат полотна - зображену на малюнку 2 - на вказану кількість пікселів в горизонтальному напрямку. Це перетворення і установка атрибутів - тимчасові, тому що вони виконуються між викликами методів context.save () і context.restore ().
Малюнок 2. Система координат Canvas за замовчуванням
За замовчуванням початок системи координат знаходиться в верхньому лівому кутку полотна. Його можна переміщати за допомогою методу context.translate ().
Ми поговоримо про прокрутку фону за допомогою методу context.translate () в розділі Прокрутка фону . Але на даний момент ви знаєте майже все, що потрібно знати про Canvas HTML5, щоб реалізувати Snail Bait. В іншій частині цього циклу статей я зупинюся на інших аспектах розробки ігор HTML5, починаючи з анімації.
На початок
По суті, анімація реалізується просто: створюється послідовність зображень, що справляє враження, що об'єкти в деякому роді оживають. Це означає, що необхідно реалізувати цикл, в якому повторюється промальовування зображення.
Традиційно анімаційні цикли реалізуються на JavaScript за допомогою методу setTimeout () або, як показано в лістингу 4, setInterval ().
Лістинг 4. Реалізація анімації за допомогою методу setInterval ()
setInterval (function (e) {// Не робіть цього для анімацій, де важливо час animate (); // Функція, що змальовує поточний кадр анімації}, 1000/60); // Приблизно 60 кадрів в секунду (fps)
код з лістингу 4 , Безсумнівно, зробить анімацію, багаторазово викликаючи функцію animate (), яка зображує наступний кадр; проте результати можуть вас не влаштувати, тому що методи setInterval () і setTimeout () нічого не знають про анімацію. (Примітка. Вам доведеться реалізувати функцію animate (); вона не входить в API Canvas.)
В лістингу 4 я встановив інтервал 1000/60 мс, що відповідає приблизно 60 кадрів в секунду. Це моя емпірична оцінка оптимальної частоти анімації, і вона може виявитися не дуже доброю; проте так як методи setInterval () і setTimeout () нічого не знають про анімацію, їм доводиться вказувати частоту кадрів. Було б краще, якби частоту кадрів вибирав браузер, який, безсумнівно, краще за мене знає, коли малювати наступний кадр анімації.
Існує ще більш серйозний недолік використання методів setTimeout і setInterval (). Ми передаємо їм тимчасові інтервали в мілісекундах, але ці методи не забезпечують точність до мілісекунди; насправді, згідно специфікації HTML, вони - в цілях економії ресурсів - можуть значно перевищити заданий інтервал.
Щоб уникнути цих проблем не слід використовувати методи setTimeout () і setInterval () для анімацій, де важливо час; використовуйте метод requestAnimationFrame ().
Метод requestAnimationFrame ()
У специфікації Управління часом в анімаціях на основі сценаріїв (див. Розділ ресурси ) W3C визначає метод роботи з об'єктом window, званий requestAnimationFrame (). На відміну від setTimeout () або setInterval (), метод requestAnimationFrame () спеціально призначений для анімації. Він не страждає недоліками, властивими методам setTimeout () і setInterval (). До того ж він простий в застосуванні, як показує лістинг 5 .
Лістинг 5. Реалізація анімації за допомогою методу requestAnimationFrame ()
function animate (time) {// Цикл анімації draw (time); // Функція, що змальовує поточний кадр анімації requestAnimationFrame (animate); // Продовження анімації}; requestAnimationFrame (animate); // Запуск анімації
Методу requestAnimationFrame () передається посилання на функцію зворотного виклику, і коли браузер готовий намалювати наступний кадр анімації, він виконує цей зворотний виклик. Для продовження анімації також виконується зворотний виклик requestAnimationFrame ().
Як видно з лістингу 5 , Браузер передає в функцію зворотного виклику параметр time. Ви можете запитати, що саме означає параметр time? Це поточний час? Час, за яке браузер повинен намалювати наступний кадр анімації?
Як не дивно, але ніякого визначення цього часу не існує. Єдине, що можна з упевненістю сказати, це те, що для кожного браузера цей параметр завжди означає одне і те ж; його можна використовувати для обчислення часу між кадрами, як я покажу в розділі Розрахунок частоти анімації в кадрах / с .
Поліфілл requestAnimationFrame ()
Багато в чому HTML5 - це мрія програміста. Розробники можуть використовувати HTML5, вільний від пропрієтарних API, для реалізації програм, що працюють на різних платформах в всюдисущих браузерах. Специфікація швидко прогресує, постійно вбираючи нові технології і вдосконалюючи існуючу функціональність.
Однак нова технологія часто прокладає шлях в специфікацію через існуючу функціональність браузерів. Постачальники постачають таку функціональність префіксом, щоб не плутати її з реалізаціями в інших браузерах; наприклад, метод requestAnimationFrame () спочатку був реалізований в Mozilla як mozRequestAnimationFrame (). Потім він з'явився в WebKit, де ця функція називається webkitRequestAnimationFrame (). Нарешті, W3C стандартизувала його як requestAnimationFrame ().
Реалізації з префіксами постачальників і різна підтримка стандартних реалізацій ускладнює використання нової функціональності, тому спільнота HTML5 придумало те, що називаеться polyfill. Поліфілл визначає рівень підтримки браузером певної функції і або надає прямий доступ до неї, якщо вона реалізована в браузері, або замінює тимчасової реалізацією, найбільш близькою до стандартної функціональності.
Поліфілли прості в застосуванні, але можуть бути складними для реалізації. У лістингу 6 показана реалізація поліфілла для функції requestAnimationFrame ().
Лістинг 6. Поліфілл requestNextAnimationFrame ()
// З книги Core HTML5 Canvas window.requestNextAnimationFrame = (function () {var originalWebkitRequestAnimationFrame = undefined, wrapper = undefined, callback = undefined, geckoVersion = 0, userAgent = navigator.userAgent, index = 0, self = this; // Обхідний шлях для бага Chrome 10, через який Chrome залишають поза передачею час // в функцію анімації if (window.webkitRequestAnimationFrame) {// Визначення оболонки wrapper = function (time) {if (time === undefined) {time = + new Date ();} self.callback (time);}; // Перехід originalWebkitRequestAnimationFrame = window.webkitRequestAnimationFrame; window.webkitRequestAnimationFrame = function (callback, element) {self.callback = callback; // Браузер викликає оболонку, а та робить зворотний виклик originalWebkitRequestAnimationFrame (wrapper, element);}} // Обхідний шлях для Gecko 2.0, г де баг в mozRequestAnimationFrame () обмежує // анімацію 30-40 кадрами / с. if (window.mozRequestAnimationFrame) {// Перевірка версії Gecko. Gecko використовується не-Firefox браузерами. Gecko 2.0 // відповідає Firefox 4.0. index = userAgent.indexOf ( 'rv:'); if (userAgent.indexOf ( 'Gecko')! = -1) {geckoVersion = userAgent.substr (index + 3, 3); if (geckoVersion === '2.0') {// Змушує оператор return вийти з циклу через функцію setTimeout (). window.mozRequestAnimationFrame = undefined; }}} Return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback, element) {var start, finish; window.setTimeout (function () {start = + new Date (); callback (start); finish = + new Date (); self.timeout = 1000/60 - (finish - start);}, self.timeout); }; }) ();
Поліфілл, реалізований в лістингу 6 , Додає функцію з ім'ям requestNextAnimationFrame () до об'єкта window. Включення Next в ім'я функції дозволяє відрізняти її від вихідної функції requestAnimationFrame ().
Функція, яку поліфілл привласнює requestNextAnimationFrame (), це або requestAnimationFrame (), якщо браузер підтримує її, або реалізація з префіксом постачальника. Якщо браузер не підтримує ні того, ні іншого, функція пропонує спеціальну реалізацію з використанням setTimeout () для найкращої можливої імітації методу requestAnimationFrame ().
Майже вся складність поліфілла пов'язана з обходом двох багів і складанням коду перед оператором return. Перший баг відноситься до Chrome 10, який передає невизначене значення часу. Другий баг, що відноситься до Firefox 4.0, обмежує частоту кадрів до 35-40 кадрів в секунду.
Реалізація поліфілла requestNextAnimationFrame () цікава, але її не обов'язково розуміти; досить знати, як його використовувати, що я і проілюструю в наступному розділі.
На початок
Тепер, коли ми розібралися з графікою і анімацією, прийшов час привести Snail Bait в рух. Для початку включимо код JavaScript requestNextAnimationFrame () в HTML-код гри, як показано в лістингу 7.
Лістинг 7. HTML
<html> ... <body> ... <script src = 'js / requestNextAnimationFrame .js'> </ script> <script src = 'game .js'> </ script> </ body> </ html>
У лістингу 8 показаний цикл анімації гри, який зазвичай називають ігровим циклом.
Лістинг 8. Ігровий цикл
var fps; function animate (now) {fps = calculateFps (now); draw (); requestNextAnimationFrame (animate); } Function startGame () {requestNextAnimationFrame (animate); }
Функція startGame (), яка викликається обробником подій onload фонового зображення, починає гру, викликаючи поліфілл requestNextAnimationFrame (). Коли потрібно намалювати перший кадр анімації гри, браузер викликає функцію animate ().
Функція animate () обчислює частоту кадрів анімації, враховуючи поточний час. (Детальніше про значення time см. В розділі Метод requestAnimationFrame () .) Зрозумівши частоту кадрів, animate () викликає функцію draw (), яка малює наступний кадр анімації. Далі animate () викликає метод requestNextAnimationFrame () для продовження анімації.
Обчислення швидкості анімації в кадрах / с
У лістингу 9 показано, як Snail Bait обчислює частоту кадрів і оновлює індикатор частоти кадрів, зображений на малюнку 1 .
Лістинг 9. Розрахунок частоти кадрів і оновлення елемента fps
var lastAnimationFrameTime = 0, lastFpsUpdateTime = 0, fpsElement = document.getElementById ( 'fps'); function calculateFps (now) {var fps = 1000 / (now - lastAnimationFrameTime); lastAnimationFrameTime = now; if (now - lastFpsUpdateTime> 1000) {lastFpsUpdateTime = now; fpsElement .innerHTML = fps.toFixed (0) + 'fps'; } Return fps; }
Частота кадрів - це просто час, що минув з моменту відображення попереднього кадру анімації, тому правильніше було б назвати це інтервалом анімації. Можна прийняти більш суворий підхід і підтримувати середню частоту кадрів протягом декількох кадрів, але я не бачу необхідності в цьому; час, що минув з моменту демонстрації останнього кадру анімації, - це саме те, що нам знадобиться в розділі Рух на основі часу .
лістинг 9 ілюструє також важливий прийом анімації: рішення задачі зі швидкістю, відмінною від швидкості анімації. Якщо оновлювати індикатор кадрів в секунду на кожному кадрі анімації, то він буде нечитабельним, тому що завжди буде знаходитися в русі; замість цього я оновлюю індикатор один раз в секунду.
Маючи ігровий цикл і частоту кадрів, можна приступати до прокручування фону.
На початок
Фон Snail Bait, показаний на малюнку 3, повільно прокручується в горизонтальному напрямку.
Малюнок 3. Фонове зображення
Фон прокручується плавно, тому що лівий і правий краї фону ідентичні, як показано на малюнку 4.
Малюнок 4. Ідентичні краю забезпечують гладкі переходи (зліва: правий край; праворуч: лівий край)
Snail Bait нескінченно прокручує фон, прорисовуючи його двічі, як показано на малюнку 5. Спочатку, як показано у верхній частині малюнка 5, фонове зображення зліва знаходиться на екрані цілком, тоді як справа воно повністю за кадром. Згодом фон прокручується, що ілюструється на середньому і нижньому скріншотах на малюнку 5.
Малюнок 5. Прокрутка справа наліво: напівпрозорі області відповідають частинам зображення, що знаходяться за межами екрану
У лістингу 10 наведено код, відповідний малюнку 5 . Функція drawBackground () малює зображення двічі, завжди на одних і тих же місцях. Удавана прокрутка є результатом постійного перетворення системи координат полотна вліво, що створює враження прокрутки фону вправо.
(Ось як можна примирити позірна суперечність між перетворенням вліво, але прокруткою вправо: уявіть собі, що полотно - це порожня рамка поверх довгого листа паперу. Папір - це система координат, а її перетворення вліво відповідає руху вліво під рамкою [полотном]. В результаті здається, що полотно переміщається вправо.)
Лістинг 10. Прокрутка фону
var backgroundOffset; // Встановлюється перед викликом drawBackground () function drawBackground () {context.translate (-backgroundOffset, 0); // Спочатку на екрані: context. drawImage (background, 0, 0); // Спочатку поза екраном: context. drawImage (background, background.width, 0); context.translate (backgroundOffset, 0); }
Функція setBackground () зміщує контекст полотна на -backgroundOffset пікселів в горизонтальному напрямку. Якщо значення backgroundOffset позитивно, фон прокручується вправо; якщо негативно, - вліво.
Після перекладу фону drawBackground () двічі малює фонове зображення, а потім повертає контекст туди, де він був перед викликом drawBackground ().
Залишається одне, здавалося б, тривіальне обчислення: розрахунок значення backgroundOffset, яке визначає, наскільки зрушувати систему координат полотна для кожного кадру анімації. Хоча сам розрахунок дійсно тривіальний, він має велике значення, тому я зупинюся на цьому докладніше.
На початок
Частота кадрів анімації буде змінюватися, але не можна допустити, щоб це впливало на швидкість руху. Наприклад, Snail Bait прокручує фон зі швидкістю 42 пікселя / с незалежно від базової частоти кадрів анімації. Анімація повинна бути заснована на часі - тобто значення швидкостей повинні вказуватися в пікселях в секунду - і не повинна залежати від частоти кадрів.
При використанні руху, заснованого на часу, легко обчислити кількість пікселів, на які об'єкт повинен переміщатися за один кадр: розділивши швидкість руху на поточну частоту кадрів. Швидкість (в пікселях в секунду), поділена на частоту кадрів (в кадрах в секунду), дає число пікселів на кадр, тобто кількість пікселів, на яке необхідно перемістити щось для поточного кадру.
У лістингу 11 показано, як Snail Bait використовує рух, заснований на часу, для обчислення зсуву фону:
Лістинг 11. Завдання швидкості зсуву фону
var BACKGROUND_VELOCITY = 42, // пікселя / с bgVelocity = BACKGROUND_VELOCITY; function setBackgroundOffset () {var offset = backgroundOffset + bgVelocity / fps; // Рух на основі часу if (offset> 0 && offset <background.width) {backgroundOffset = offset; } Else {backgroundOffset = 0; }}
Функція setBackgroundOffset () обчислює кількість пікселів, на яке переміщається фон для поточного кадру, шляхом ділення швидкості руху фону на поточну частоту кадрів. Потім це значення додається до поточного зміщення фону.
Для безперервної прокрутки фону setBackgroundOffset () скидає зміщення фону в 0, коли воно стає менше 0 або більше ширини фону.
На початок
Кожен, хто коли-небудь сидів на місці пасажира в рухомому автомобілі і милувався пейзажем крізь телефонні стовпи, знає, що близькі предмети рухаються швидше, ніж віддалені. Це називається параллаксом.
Гра Snail Bait - це 2D-платформер, але в ній використовується м'який ефект паралакса - платформи здаються ближчими до спостерігача, ніж фон. У грі паралакс реалізується шляхом помітно більш швидкої прокрутки платформ в порівнянні з фоном.
Малюнок 6 ілюструє, як Snail Bait реалізує паралакс. Верхній скріншот показує фон в певний момент часу, а нижній - через кілька кадрів анімації. На цих двох скріншотах видно, що за один і той же час платформи просунулися набагато далі, ніж фон.
Малюнок 6. Паралакс: платформи (поблизу) прокручуються швидше, ніж фон (далеко)
На лістингу 12 показані функції, які регулюють швидкість і зміщення платформ.
Лістинг 12. Налаштування швидкості і зміщення платформ
var PLATFORM_VELOCITY_MULTIPLIER = 4.35; function setPlatformVelocity () {// Платформи рухаються в 4,35 рази швидше, ніж фон platformVelocity = bgVelocity * PLATFORM_VELOCITY_MULTIPLIER; } Function setPlatformOffset () {platformOffset + = platformVelocity / fps; // Рух на основі часу}
Згадаю лістинг 8 з ігровим циклом Snail Bait. Цей цикл складається з функції animate (), що викликається браузером, коли потрібно намалювати наступний кадр анімації гри. Ця функція animate (), в свою чергу, викликає функцію draw (), яка малює наступний кадр анімації. У лістингу 13 показаний код функції draw () на даному етапі розробки.
Лістинг 13. Функція draw ()
function setOffsets () {setBackgroundOffset (); setPlatformOffset (); } Function draw () {setPlatformVelocity (); setOffsets (); drawBackground (); drawRunner (); drawPlatforms (); }
Функція draw () задає швидкість платформ і зміщення фону і платформ. Потім вона малює фон, бігуна і платформи.
На початок
У наступній статті я покажу, як инкапсулировать код Snail Bait в об'єкт JavaScript, щоб уникнути конфліктів просторів імен. Я також покажу, як призупинити гру, в тому числі як автоматично припиняти її, коли вікно втрачає фокус, і як перезапустити гру з відліком часу, коли вікно відновлює фокус. Ви також побачите, як управляти бігуном з клавіатури. По ходу справи ви навчитеся використовувати в ігровому циклі CSS-переходи і функціональність interject. До скорої зустрічі.
На початок
Опис Ім'я Розмір Метод завантаження Приклад коду j-html5-game2.zip 737 КБ HTTPІнформація про методи завантаження
навчитися
Отримати продукти і технології
- Replica Island : Завантажте Відкритий вихідний код цього популярного платформера для Android.
обговорити
- Візьміть участь в діяльності спільноти developerWorks . Спілкуйтеся з іншими користувачами developerWorks, переглядаючи блоги, форуми, групи і вики розробників
Девід Гирі (David Geary), автор книги Core HTML5 Canvas , Є також співзасновником групи користувачів HTML5 Денвера і автором восьми книг по Java-програмування, в тому числі бестселерів по Swing і JavaServer Faces. Девід часто виступає на конференціях, в тому числі JavaOne, Devoxx, Strange Loop, NDC і OSCON, і тричі заслужив титул "рок-зірки JavaOne". Для developerWorks він написав цикли статей JSF 2 fu и GWT fu . За новинами Девіда можна стежити в Twitter: @davidgeary.
При першому вході в developerWorks для Вас буде створений профіль і Вам потрібно буде вибрати Коротке ім'я. Воно буде виводитися поруч з контентом, опублікованими Вами в developerWorks.
Псевдонім повинно мати довжину від 3 символів до 31 символу. Ваше ім'я в системі має бути унікальним. Як ім'я з міркувань приватності можна використовувати контактний e-mail.
Вся введена інформація захищена.
На початок
ArticleTitle = Розробка 2D-ігор на HTML5: Графіка і анімація
Ви можете запитати, що саме означає параметр time?Це поточний час?
Час, за яке браузер повинен намалювати наступний кадр анімації?