Раскрываем потенциал HTML 5 Canvas в играх

в 14:06, , рубрики: canvas, html 5, javascript, игры, оптимизация, метки: , , ,

image

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):

image

Готовый код можно взять тут: 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), результат налицо:

image

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

Начальный исходный кода был таким:

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, чтобы ускорить выполнение нужно модифицировать код.

image

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

image

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

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;

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

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

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

image

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

image

И после применения всего вышеописанного:

Автор: akamajoris

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js