HTML 5 в браузерах и HTML 5 для Windows 8 Metro в настоящее время серьезные кандидаты для разработки современных игр.
Используя Canvas, вы имеете доступ к аппаратным ускорениям пространства, и если использовать некоторые советы и приемы, то сможете достигнуть великолепных 60 кадров в секунду.
Это действительно важно в играх, потому что чем плавнее происходит игра, тем лучше ощущения игрока.
Целью данной статьи является показать вам некоторые трюки, которые помогут получить максимальную мощность от HTML 5 Canvas.
В следующих главах Я буду использовать примеры, чтобы продемонстрировать концепции о которых Я хочу рассказать. Образецом будет 2D туннельный эффект, который я написал для Coding4Fun и представил на TechDays 2012 году во Франции (http://video.fr.msn.com/watch/video/techdays-2012-session-technique-coding4fun/zqy7cm8l).
Этот эффект в основном вдохновлен кодом Commodore Amiga, который я написал, когда был молодым демомейкером в 80-х.
Используется только canvas и JavaScript (когда-то исходный код был только ассемблере 68000):
Готовый код можно взять тут: http://www.catuhe.com/msdn/canvas/tunnel.zip
Данная статья не направлена на объяснение того, как устроен тyнель, но Вы можете начать именно с него и посмотреть на достижения в области оптимизации в реальном времени.
Использование закадрового холста, для чтения данных изображения
Первое, о чем я хочу рассказать — это то, как Canvas, может помочь вам читать данные изображения. Действительно, каждая игра, использует графические спрайты или фон. Canvas имеет очень полезный метод, для отображения изображения: DrawImage. Эта функция может быть использована для создания спрайта на холсте, потому что вы можете определить начальные и конечные координаты.
Но иногда этого не достаточно. Например, этого недостаточно, если вы хотите применить некоторые эффекты на исходном изображении. Или когда исходное изображение не является простым растровым а более сложный ресурс для вашей игры (например, карту, где вы должны считывать данные с формы).
В таких случаях необходимо получить доступ к внутренним данным изображения. Но у изображения нет необходимых нам тегов. И тогда на помощь приходит Canvas!
Конечно, каждый раз, читая данные изображения, Вы можете использовать закадровый Canvas. Основная идея здесь заключается в загрузке изображения и когда оно загружено, Вы просто должны направить это в Canvas (не включено в DOM). Вы можете получить каждый пиксель исходного изображения, читая пиксель в Canvas (на самом деле всё просто).
Ниже продемонстрирован код для этого метода(используется 2D туннельный эффект для чтения текстуры данных):
var loadTexture = function (name, then) {
var texture = new Image();
var textureData;
var textureWidth;
var textureHeight;
var result = {};
// on load
texture.addEventListener('load', function () {
var textureCanvas = document.createElement('canvas'); // off-screen canvas
// Setting the canvas to right size
textureCanvas.width = this.width; //<-- "this" - это изображение
textureCanvas.height = this.height;
result.width = this.width;
result.height = this.height;
var textureContext = textureCanvas.getContext('2d');
textureContext.drawImage(this, 0, 0);
result.data = textureContext.getImageData(0, 0, this.width, this.height).data;
then();
}, false);
// Loading
texture.src = name;
return result;
};
Используя этот код, Вы должны обратить внимание, на то, что загрузка текстуры асинхронная и поэтому вы должны использовать функцию-параметр, чтобы продолжить ваш код:
// Texture
var texture = loadTexture("soft.png", function () {
// Launching the render
QueueNewFrame();
});
Используйте функции аппаратного масштабирования
Современные браузеры и Windows 8 поддерживают аппаратное ускорение canvas. Это позволяет использовать нам, например GPU для масштабирования холста.
В случае 2D туннельного эффекта, алгоритм требует, обработки каждого пикселя холста. Так, например, для 1024x768, canvas необходимо обработать 786432 пикселей. А чтобы все происходило плавно, нужно повторять это 60 раз в секунду, что соответствует 47185920 пикселей в секунду!
Очевидно, что уменьшение размера полотна, поможет вам в сокращении количества пикселей которые нужно обработать, и это кардинально улучшает общую производительность.
И снова на помощь приходит canvas! В коде расположенном ниже, показано как использовать аппаратное ускорение для масштабирования рабочей области в буфер и взаимодействовать с внешними размеры объекта DOM:
canvas.width = 300;
canvas.style.width = window.innerWidth + 'px';
canvas.height = 200;
canvas.style.height = window.innerHeight + 'px';
Стоит отметить разницу между размером DOM объекта (canvas.style.width и canvas.style.height), и размером рабочего буфера хослта (canvas.width и canvas.height).
Когда есть разница между этими двумя размерами, оборудование используется для масштабирования рабочего буфера и в нашем случае это отличная вещь: мы можем работать на меньшем размере холста, позволив GPU изменяет масштаб, чтобы соответствовать объекту DOM (с использованием фильтров, для придания плавности).
В данном случае, операции производятся в разрешении 300x200px, а графический процессор будет масштабировать его до размеров вашего окна.
Эта особенность широко поддерживается всеми современными браузерами так что вы можете брать это в рассчёт.
Оптимизируйте ваши циклы отрисовки
Когда вы пишете игру, вы должны использовать цикл, который рисует компоненты игры (фон, спрайты, очки и т.д..). Этот цикл является основой вашего кода и должен быть наиболее оптимизированным, чтобы убедиться, что ваша игра шла без задержек и плавно.
RequestAnimationFrame
Одним из интересных добавлений в HTML 5 является функция window.requestAnimationFrame. Вместо использования window.setInterval создающей таймер, который вызывает рендеринг каждого цикла (1000/16) миллисекунд (для достижения хорошего 60 кадров в секунду), Вы можете позволять это браузеру с поддержкой requestAnimationFrame. Вызов этого метода указывает, на то, что вы хотите получить от браузера как можно быстрее связанные с графикой вещи.
Браузер будет включать ваш запрос внутри своего собственного рендеринга графики, и будет синхронизировать ваш рендеринг со своим, включая анимации (CSS, переходы и т.д. ...). Это решение также интересно, потому что ваш код не будет вызван, если окно не отображается (свернуто, не входит в область видимости, и т.д.).
Это может помочь повысить производительность, так как браузер может оптимизировать параллельный рендеринг (например, если ваш цикл рендеринга слишком медленный), а также способствовать более плавной анимации.
Код довольно очевидный (обратите внимание на использование конкретных префиксов в браузерах):
var intervalID = -1;
var QueueNewFrame = function () {
if (window.requestAnimationFrame)
window.requestAnimationFrame(renderingLoop);
else if (window.msRequestAnimationFrame)
window.msRequestAnimationFrame(renderingLoop);
else if (window.webkitRequestAnimationFrame)
window.webkitRequestAnimationFrame(renderingLoop);
else if (window.mozRequestAnimationFrame)
window.mozRequestAnimationFrame(renderingLoop);
else if (window.oRequestAnimationFrame)
window.oRequestAnimationFrame(renderingLoop);
else {
QueueNewFrame = function () {
};
intervalID = window.setInterval(renderingLoop, 16.7);
}
};
Чтобы использовать эту функцию, Вы должны просто добавить его вызов в конце вашего цикла рендеринга, чтобы зарегистрировать следующий кадр:
var renderingLoop = function () {
...
QueueNewFrame();
};
Доступ к DOM (Document Object Model)
Для оптимизации основных циклов, Вы должны следовать, по крайней мере, одному золотому правилу: не иметь доступа к DOM. Даже если современные браузеры оптимизированы в этой области, чтение свойства DOM объекта по-прежнему является тормозом в работе цикла.
Например, в моём коде, используя профайлер Internet Explorer 10(панель разработчика открывается по клавише F12), результат налицо:
Как вы можете видеть, основное время занимает доступ к ширине и высоте холста.
Начальный исходный кода был таким:
var renderingLoop = function () {
for (var y = -canvas.height / 2; y < canvas.height / 2; y++) {
for (var x = -canvas.width / 2; x < canvas.width / 2; x++) {
...
}
}
};
Вы можете удалить canvas.width и canvas.height свойства с 2 переменными объявленными заранее.
var renderingLoop = function () {
var index = 0;
for (var y = -canvasHeight / 2; y < canvasHeight / 2; y++) {
for (var x = -canvasWidth / 2; x < canvasWidth / 2; x++) {
...
}
}
};
Просто, не так ли? Возможно это трудно понять, но Вам стоит это попробовать!
Предварительные вычисления
Судя по данным профайлера, функция Math.atan2 достаточно медленная. Все из-за того, что нет хорошей оптимизации со стороны CPU для JavaScript, чтобы ускорить выполнение нужно модифицировать код.
В общем случае, если вы можете заранее выполнять некоторые длительные операции — делайте это. Здесь, Я вычисляю результат Math.atan2:
var atans = [];
var index = 0;
for (var y = -canvasHeight / 2; y < canvasHeight / 2; y++) {
for (var x = -canvasWidth / 2; x < canvasWidth / 2; x++) {
atans[index++] = Math.atan2(y, x) / Math.PI;
}
}
Массив atans можно заполнять внутри цикла, это может существенно повысить производительность.
Избегайте использования Math.round, Math.floor и ParseInt
Последним стопорным моментом является использование ParseInt:
При использовании холста, вы должны оперировать пикселями, используя целочисленными координатами (х, у). Действительно, все ваши вычисления производятся с использованием чисел с плавающей точкой, и вы должны преобразовать их в целое число для того чтобы использовать их.
JavaScript позволяет использовать Math.round, Math.floor или даже ParseInt для преобразования числа в целое. Но эта функция делает некоторую лишнюю работу (например, чтобы проверить диапазоны или проверить, является ли это вообще числом. ParseInt вовсе в самом начале преобразует параметр в строку!). В моем цикле рендеринга, я хочу использовать наиболее быстрый способ для выполнения этого преобразования.
Вспоминая моих старый ассемблерный код, я использовал небольшую хитрость: вместо использования ParseInt, вы просто должны перенести ваш номер вправо со значением 0. Выполнение перемещения значения из дробного регистра в целочисленный и используется аппаратное преобразование. Сдвиг значения вправо с нулем 0 позволит сохранить его без изменений, и поэтому вы можете получить обратно своё целочисленное значение.
Изначальный исходный код был:
u = parseInt((u < 0) ? texture.width + (u % texture.width) : (u >= texture.width) ? u % texture.width : u);
А оптимизированный выглядит так:
u = ((u < 0) ? texture.width + (u % texture.width) : (u >= texture.width) ? u % texture.width : u) >> 0;
Конечно, используя данное решения, Вы должны быть уверены, что значение является числом.
Конечный результат
После всех оптимизаций вы получите подобный результат:
Как можно заметить, мы оптимизировали только основные функции.
Посмотрим как выглядит тунель в самом начале (без оптимизации):
И после применения всего вышеописанного: