Работа с QML Canvas

в 5:51, , рубрики: canvas, Digia, game development, QML, qt, Qt 5, qt quick, Qt Software, qt5, декларативное программирование, метки: , , , , , , ,

Работа с QML Canvas В последнее время на хабре было много хороших постов, раскрывающих аспекты работы с QML: XMLHTTPRequest, Loader, GLSL, но до сих пор никто не упоминал, что Qt Quick 2.0 содержит также компонент Canvas, который даёт нам возможность (сюрприз!) рисовать. Синтаксис использования тот же, что и у HTML5 Canvas, но лично мне, как человеку, далекому от разработки для веба, это ни о чём не говорило.
Продемонстрировать работу с ним я хочу на примере создания каркаса для игры, который, при желании, легко можно будет переделать либо в старую добрую Snake, либо во что-то вроде Achtung, die Kurve!

В проекте у нас будет два компонента: сцена (Scene.qml, корневой элемент) и игрок (Player.qml).
Начнём с игрока. Игрок в любой момент времени будет представлять собой движущийся объект, за котором будет оставаться линия. Движение характеризуется тремя параметрами: углом (так как действие происходит на плоскости, то он задаёт направление движения), скоростью движения и скоростью поворота. Так и запишем:

import QtQuick 2.0

Item {
    width: 30;
    height: 30;
    property int angle: 0;
    property real linearSpeed: 1.5; // скорость движения
    property real angularSpeed: 2.0; // скорость поворота (не угловая)
    function step() {
        x += Math.cos(angle*Math.PI/180)*linearSpeed;
        y += Math.sin(angle*Math.PI/180)*linearSpeed;
    }
    Rectangle {
        anchors.fill: parent;
        color: "black";
        antialiasing: true;
        radius: width/2;
    }
}

Двигать игрока теперь можно простым вызовом функции step(). Хорошо, но как насчёт управления? Чтобы не зависеть от какого-то одного способа ввода, поступим следующим образом: добавим игроку ещё два свойства

    property bool turnLeft: false;
    property bool turnRight: false;

А функцию step() дополним этой строкой:

    if (turnLeft) angle -= angularSpeed; else if (turnRight) angle += angularSpeed;

Само же управление повесим на клавиши:

    Keys.onPressed: {
        switch (event.key) {
        case Qt.Key_Left: turnLeft = true; break;
        case Qt.Key_Right: turnRight = true; break;
        }
    }
    Keys.onReleased: {
        switch (event.key) {
        case Qt.Key_Left: turnLeft = false; break;
        case Qt.Key_Right: turnRight = false; break;
        }
    }
    focus: true;

Самое время проверить, работает ли оно так, как задумывалось.
Начнём разрабатывать сцену. Тут всё просто — возьмём компонент Canvas и разместим на нём нашего игрока. Пока без всякого рисования.

import QtQuick 2.0

Canvas {
    id: canvas;
    width: 640;
    height: 480;
    antialiasing: true;
    Player {
        id: player;
    }
    Timer {
        id: timer;
        interval: 16;
        running: true;
        repeat: true
        onTriggered: {
            player.step();
        }
    }
}

Работает? Здорово. Теперь нужно начать каким-то образом оставлять за собой следы. Пусть игрок занимается этим самостоятельно. Добавим в самый конец нашего «игрового цикла» (timer.onTriggered) запрос на рисование.

    canvas.requestPaint();

Рисование происходит на неком контексте. Контекст мы получаем в обработчике события рисования и передаём нашему игроку:

    onPaint: {
        var ctx = canvas.getContext("2d");
        player.draw(ctx);
    }

В документации Qt вы не найдёте описания функций работы с Canvas (что, в общем-то, логично), поэтому обратимся к сторонним источникам (первый результат гугла по запросу «canvas functions»).
Для минимизации вычислительных затрат на каждой итерации мы будем лишь «дорисовывать» линию за игроком. То есть, придётся сохранять ещё и прошлые координаты игрока. Конечно, визуальная гладкость линии, нарисованной подобным образом, будет зависеть от точности представления вещественных чисел и может быть разной на различных платформах (я предупредил!). Простой пример работы с контекстом есть в описании функции lineTo(). То, что нужно, теперь мы можем доработать игрока. Следите за руками.

    property color lineColor: "green";
    property real lastX: 0;
    property real lastY: 0;
    function step() {
        if (turnLeft) angle -= angularSpeed; else if (turnRight) angle += angularSpeed;
        lastX = x;
        lastY = y;
        x += Math.cos(angle*Math.PI/180)*linearSpeed;
        y += Math.sin(angle*Math.PI/180)*linearSpeed;
    }
    function draw(ctx) {
        ctx.beginPath();
        ctx.strokeStyle = lineColor;
        ctx.lineWidth = 5;
        var radius = width/2;
        ctx.moveTo(lastX+radius, lastY+radius);
        ctx.lineTo(x+radius, y+radius);
        ctx.stroke();
    }

В сумме у вас должно было получиться следующее:

Player.qml

import QtQuick 2.0

Item {
    width: 30;
    height: 30;
    property real angle: 0;
    property real linearSpeed: 2.0;
    property real angularSpeed: 2.0;
    property bool turnLeft: false;
    property bool turnRight: false;
    property color lineColor: "green";
    property real lastX: 0;
    property real lastY: 0;
    function step() {
        if (turnLeft) angle -= angularSpeed; else if (turnRight) angle += angularSpeed;
        lastX = x;
        lastY = y;
        x += Math.cos(angle*Math.PI/180)*linearSpeed;
        y += Math.sin(angle*Math.PI/180)*linearSpeed;
    }
    function draw(ctx) {
        ctx.beginPath();
        ctx.strokeStyle = lineColor;
        ctx.lineWidth = 5;
        var radius = width/2;
        ctx.moveTo(lastX+radius, lastY+radius);
        ctx.lineTo(x+radius, y+radius);
        ctx.stroke();
    }
    Keys.onPressed: {
        switch (event.key) {
        case Qt.Key_Left: turnLeft = true; break;
        case Qt.Key_Right: turnRight = true; break;
        }
    }
    Keys.onReleased: {
        switch (event.key) {
        case Qt.Key_Left: turnLeft = false; break;
        case Qt.Key_Right: turnRight = false; break;
        }
    }
    focus: true;
    Rectangle {
        anchors.fill: parent;
        color: "black";
        antialiasing: true;
        radius: width/2;
    }
}
Scene.qml

import QtQuick 2.0

Canvas {
    id: canvas;
    width: 640;
    height: 480;
    antialiasing: true;
    onPaint: {
        var ctx = canvas.getContext("2d");
        player.draw(ctx);
    }
    Player {
        id: player;
    }
    Timer {
        id: timer;
        interval: 16;
        running: true;
        repeat: true
        onTriggered: {
            player.step();
            canvas.requestPaint();
        }
    }
}

По умолчанию рисование происходит в кадровый буфер OpenGL, поэтому, если вы хотите более-менее одинакового отображения на различных устройствах (например, сглаживание не будет работать, если видеокарта не поддерживает Sample Buffers), рекомендую установить свойство Canvas renderTarget в значение Canvas.Image. Кроме этого, немаловажную роль играет свойство renderStrategy.

Работа с QML Canvas

Если вы захотите написать клона игры Zatacka (Achtung, die Kurve!), то вам нужно будет определять пересечения игроков с линией. Делается это достаточно легко: функция getImageData возвращает объект типа CanvasImageData, содержащий свойство data. Этот одномерный массив содержит информацию о цвете заданных пикселей (подробнее). Для определения пересечения остаётся лишь проверить пиксели в текущих координатах игрока на прозрачность (учтите, что вызовы getImageData довольно затратны, особенно, если рендеринг осуществляется в кадровый буфер). Функция могла бы выглядеть так:

    function check(ctx) {
        var c = ctx.getImageData(x+xSpeed*2-1, y+ySpeed*2-1, 2, 2).data;
        if (c[3]||c[7]||c[11]||c[15]) lose();
    }

Очистка игрового поля может производиться следующим образом:

    function clear() {
        var ctx = getContext("2d");
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    }

Если только одно ограничение — производительность. Прямо к холсту мы можем применить какой-нибудь шейдерный эффект:

import QtQuick 2.0
import QtGraphicalEffects 1.0

Item {
    width: 640;
    height: 480;
    Canvas {
        id: canvas;
        ...
    }
    DirectionalBlur {
        anchors.fill: canvas;
        source: canvas;
        angle: 90;
        length: 32;
        samples: 24;
    }
}

Работа с QML Canvas

Спасибо за внимание.

Автор: epicfailguy93

Источник

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


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