Продемонстрировать работу с ним я хочу на примере создания каркаса для игры, который, при желании, легко можно будет переделать либо в старую добрую 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; } }
Спасибо за внимание.
ссылка на оригинал статьи http://habrahabr.ru/post/178151/
Добавить комментарий