Статьи

Раскройте возможности HTML 5 Canvas для игр

Браузеры HTML5 и приложения HTML5 для Windows 8 в стиле metro стали серьезными кандидатами для разработки современных игр.

Благодаря холсту у вас есть доступ к аппаратно ускоренному пространству, где вы можете рисовать контент вашей игры, и с некоторыми советами и рекомендациями вы сможете добиться великолепного 60 кадров в секунду рендеринга. Это понятие текучести действительно важно в играх, потому что чем плавнее игра, тем лучше чувство игрока.

Цель этой статьи — дать вам несколько советов о том, как максимально использовать возможности HTML 5 canvas. Этот эффект в основном основан на некотором коде Commodore AMIGA, который я написал, когда был молодым демомейкером в 80-х годах.

Теперь он использует только canvas и Javascript (где оригинальный код был основан только на ассемблере 68000):

<вставьте пакет tunnel.zip сюда, как эта страница >

Полный код доступен здесь: http://www.catuhe.com/msdn/canvas/tunnel.zip

Цель этой статьи не в том, чтобы объяснить, как разрабатывается туннель, а в том, как вы можете начать с данного кода и оптимизировать его для достижения производительности в реальном времени.

Использование закадрового холста для чтения данных изображения

Прежде всего я хочу поговорить о том, как вы можете использовать холст, чтобы помочь вам прочитать данные изображения. Действительно, в каждой игре вам нужна графика для ваших спрайтов или фона. У холста есть действительно полезный метод для рисования изображения: drawImage. Эта функция может использоваться для рисования спрайта на холсте, потому что вы можете определить исходный и целевой прямоугольник.

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

В этих случаях вам необходимо получить доступ к внутренним данным картинки. Но у тега Image нет возможности прочитать его содержимое. И здесь вам поможет холст!

Действительно, каждый раз, когда вам нужно прочитать содержимое рисунка, вы можете использовать закадровый холст. Основная идея здесь состоит в том, чтобы загрузить изображение, и когда изображение загружено, вам просто нужно отобразить его на холсте (не входит в DOM). Затем вы можете получить каждый пиксель исходного изображения, прочитав пиксель холста (что действительно просто).

Код для этого метода следующий (используется в эффекте 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" is the image
        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;
};

Чтобы использовать этот код, вы должны принять во внимание, что загрузка текстуры асинхронна, и поэтому вы должны использовать параметр then для передачи функции для продолжения вашего кода:

// Texture
var texture = loadTexture("soft.png", function () {
    // Launching the render
    QueueNewFrame();
});

Использование функции аппаратного масштабирования

Современные браузеры и Windows 8 поддерживают аппаратное ускорение холста. Это означает, что, например, вы можете использовать графический процессор для масштабирования содержимого холста.

В случае эффекта 2D-туннеля алгоритм требует обработки каждого пикселя холста. Так, например, для холста 1024×768 вы должны обработать 786432 пикселей. И чтобы быть плавным, вы должны делать это 60 раз в секунду, что соответствует 47185920 пикселям в секунду!

Очевидно, что каждое решение, которое поможет вам уменьшить количество пикселей, значительно улучшит общую производительность.

И снова у холста есть решение! В следующем коде показано, как использовать аппаратное ускорение для масштабирования внутреннего рабочего буфера холста до внешнего размера объекта DOM:

// Setting hardware scaling
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).

Когда есть разница между этими двумя размерами, аппаратное обеспечение используется для масштабирования рабочего буфера, и в нашем случае это отличная вещь: мы можем работать с меньшим разрешением и позволить графическому процессору масштабировать результат, чтобы соответствовать объекту DOM (с красивый и бесплатный фильтр для размытия результата).

В этом случае рендеринг выполняется в формате 300×200, а графический процессор масштабирует его до размера вашего окна.

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

Оптимизируйте цикл рендеринга

Когда вы пишете игру, у вас должен быть цикл рендеринга, где вы рисуете все компоненты вашей игры (фон, спрайты, счет и т. Д.). Этот цикл является основой вашего кода и должен быть чрезмерно оптимизирован, чтобы ваша игра была быстрой и плавной.

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 (объектная модель документа)

Чтобы оптимизировать цикл рендеринга, вы должны следовать по крайней мере одному золотому правилу: НЕ ДОСТУПАЙТЕСЬ К 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 немного медленная. Фактически, эта операция вряд ли закодирована внутри ЦП, поэтому среда выполнения JavaScript должна добавить некоторый код для вычисления результата.

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

// precompute arctangent
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:

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

JavaScript предоставляет Math.round, Math.floor или даже parseInt для преобразования числа в целое число. Но эта функция делает некоторые дополнительные работы (например, для проверки диапазонов или проверки, является ли значение фактически числом. ParseInt даже сначала преобразует свой параметр в строку!). И внутри моего цикла рендеринга мне нужен быстрый способ выполнить это преобразование.

Вспоминая мой старый код на ассемблере, я использовал небольшую хитрость: вместо использования parseInt, вам просто нужно сместить ваше число вправо со значением 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;

Конечно, это решение требует, чтобы вы были уверены, что значение является правильным числом

Конечный результат

Применение всех оптимизаций дает вам следующий отчет:

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

Начиная с оригинального рендера туннеля ( без какой-либо оптимизации ):

<вставьте пакет tunnel.zip сюда, как эта страница >

И после применения всех этих оптимизаций :

<вставьте пакет tunnel.zip сюда, как эта страница >

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

Идти дальше

Помня об этих ключевых моментах, вы готовы создавать быстрые и динамичные игры в режиме реального времени для современных браузеров, таких как IE10 или приложения для метро для Windows 8 !

Лицензия

Эта статья, наряду со всеми связанными исходным кодом и файлами, распространяется под лицензией The Code Project Open License (CPOL).