В последнее время на хабре было много хороших постов, раскрывающих аспекты работы с 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();
}
В сумме у вас должно было получиться следующее:
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;
}
}
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;
}
}
Спасибо за внимание.
Автор: epicfailguy93