Работа с 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.

Если вы захотите написать клона игры 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;     } }

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

ссылка на оригинал статьи http://habrahabr.ru/post/178151/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *