Evgenii Legotckoi
Evgenii Legotckoi14 июля 2016 г. 5:57

Игра на QML - Урок 1. Игровая арена и динамические объекты

После первого опыта по написанию игры на Qt под Android, хочу поделиться этим опытом и предлагаю вместе написать простенькую игру в стиле "Убей крота". Это игра, в которой нужно успевать попадать по кротам, которые вылазят из нор, но учитывая, что это будет упрощённая игра, то вместо кротов используем круглые мишени, которые будут появляться на игровом поле. В качестве игрового поля будет использовать сетку 6 на 6 ячеек. Но для создания поля не будет использоваться какой-то специальный объект, типа GridLayout . В игровом поле сетка будет формироваться из количества строк, колонок и длины стороны квадратной ячейки. Данные о заполненности ячейки будут храниться в двумерном массиве, который будет сформирован в javascript составляющей (на забываем, что QML - это декларативный JSON-подобный язык с поддержкой javascript).

Структура проекта

Проект состоит из следующих файлов:

  • TargetGame.pro - профайл проекта;
  • deployment.pri - файл сборки и деплоя проекта;
  • main.cpp - файл с main функцией проекта;
  • main.qml - основной файл QML-слоя с объектом главного окна приложения;
  • Target.qml - файл QML с описанием объекта мишени;
  • Target.png - внешний вид мишени является png рисунком;
  • GameArea.qml - игровая арена с сеткой для размещения мишеней;
  • logic.js - файл с javascript логикой игры.

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


main.qml

В главном окне приложения располагается объект типа GameArea, который описан в GameArea.qml. Поскольку ширина и высота игровой арены рассчитывается автоматически в зависимости от размеров сетки и ячеек сетки, то мы просто расположим игровую арену в середине окна приложения.

Отличительным моментом этого файла является подключение файла logic.js в качестве типа Logic , который будет отвечать с логику игры и в данном файле, по окончании создания объекта главного окна приложения с игровой ареной, мы передадим указатель на игровую арену в ядро логики игры, чтобы запомнить текущее состояние игрового поля и провести первичную инициализацию игрового поля, а также массива игрового поля, который будет отвечать содержание ячеек сетки. Делается это вызовом функции newGameState(), и передачей в неё в качестве аргумента id игровой арены. В данном уроке мы не отслеживаем состояние игры, то есть запущена ли игра или остановлена, поэтому игровой таймер арены запуститься сразу же, после запуска приложения.

import QtQuick 2.7
import QtQuick.Controls 1.5
import "logic.js" as Logic

ApplicationWindow {
    visible: true
    width: 420
    height: 480
    title: qsTr("Target")

    GameArea {
        id: gameArea
        anchors.centerIn: parent
    }

    // По окончании создания окна инициализируем состояние игрового поля
    Component.onCompleted: {
        Logic.newGameState(gameArea)
    }
}

GameArea.qml

Поскольку логическое ядро на javascript фигурирует у нас во всех аспектах игры, то подключаем данный файл и здесь.

Игровая арена представляет собой прямоугольник со свойствами предзаданных строк и колонок, а также размеров одной стороны ячейки. В зависимости от этих свойств и рассчитываются ширина и высота игровой арены. Также в игровой арене присутствует таймер, по срабатыванию которого будут создаваться мишени.

Создание мишеней сопровождается проверкой доступности случайно выбранной ячейки. Выбор ячейки осуществляется с помощью выбора случайных строки и колонки, посредством получения случайного целого числа в выбранном диапазоне от минимального до максимального включительно с помощью функции getRandomRound(min, max) , которая также описана в logic.js.

Функция checkEmptyField(row, column) проверяет, существует ли какой-нибудь объект в заданной ячейке, и если ячейка пустая, то создается мишень с помощью функции createTarget(parent, row, column).

import QtQuick 2.7
import "logic.js" as Logic

Rectangle {
    id: grid
    property int squareSize: 64 // Размер ячейки игровой сетки
    property int rows: 6        // Количество строк сетки
    property int cols: 6        // Количество колонок сетки

    width: cols * squareSize    // Ширина и высота игровой арены
    height: rows * squareSize   // Будет зависеть от количества колонок, строк и размера одной ячейки

    // Таймер для создания мишеней
    Timer {
        id: createTargetsTimer
        interval: 350; running: true; repeat: true;
        // Раз в секунду выбираем случайную ячейку,
        // проверяем наличие мишени в ней, и если ячейка пустая,
        // то создаём мишень
        onTriggered: {
            var targetRow = Logic.getRandomRound(0, rows - 1);
            var targetColumns = Logic.getRandomRound(0, cols - 1);
            if (Logic.checkEmptyField(targetRow, targetColumns)) {
                Logic.createTarget(grid ,targetRow, targetColumns)
            }
        }
    }
}

Target.qml

Прежде, чем изучать содержимое логического javascript ядра игры, изучим описание объекта мишени.

Логика поведения мишени следующая. После создания, мишень должна исчезнуть через случайный промежуток времени. Причем не просто исчезнуть, то есть поменяв уровень прозрачности, а самоуничтожиться. Для этого служит метод destroy(), который будет применён  на корневом объекте, его id: root. Также Вы можете увидеть функцию destroyTarget(row, column) в javascript ядре игры. Это функция служит для очистки ячейки двумерного информационного массива сетки.

Поскольку, размер ячейки составляет 64 пикселя, а размер мишени 56 пикселей, то используя нехитрые вычисления, мы определяем положение мишени на игровом поле в зависимости от колонки и строки, которые она должна занимать.

import QtQuick 2.7
import "logic.js" as Logic

Image {
    id: root
    property int row: 0
    property int col: 0

    width: 56
    height: 56
    x: (col * (width + 8)) + 4
    y: (row * (height + 8)) + 4

    source: "Target.png"  // Загружаем изображение мишени

    Timer {
        interval: Logic.getRandomRound(350, 1500); running: true; repeat: false
        onTriggered: root.opacity = 0.0
    }

    // Когда изображение станет прозрачным. мы уничтожим сам объект
    onOpacityChanged: {
        if (opacity == 0.0) {
            Logic.destroyTarget(row, col)
            root.destroy()
        }
    }

    // Задаём поведение анимации свойства прозрачности объекта
    Behavior on opacity { PropertyAnimation { duration: 250 } }
}

logic.js

В данном файле имеется объявление .pragma library *. *

.pragma library

Это сделано в связи с необходимостью, чтобы можно было обращаться к состоянию игры ( var gameState ) из любого места, где подключен данный файл в QML слое. Фактически переменные становятся разделяемыми ресурсами для различных объектов в игре. И самым главным ресурсом является gameState (он же игровая арена GameArea ). Дело в том, что если мы можем обращаться к игровой арене непосредственно по id из различных объектов, которые статически объявлены в игровой арене, или в файле, где объявлена сама арена, то в динамических ресурсах, как например в Target.qml , у нас такой способ не сработает. Для этого и используется gameState из logic.js, который позволяет проверять состояние игровой арены независимо от того, объявлен ли объект статически (под состоянием я понимаю, запущена ли игра, или поставлена на паузу, что будет рассмотрено в следующих статьях) .

А теперь обратимся к содержимому файла logic.js. Здесь имеется два наиболее интересных момента:

  1. Это создание двумерного массива на javascript, который служит информационной моделью о содержащихся в сетке объектах, и проверка на предмет существования объекта в ячейке.
  2. Это создание динамических мишеней.

Если с первым всё достаточно понятно из кода, то про второе скажу несколько слов. Для динамического создания мишени в данном случае используется компонент заготовка, которая создаётся из файла Target.qml. Причем такая заготовка требуется всего лишь одна даже для множества таких однотипных элементов в игре.

var targetComponent = Qt.createComponent("Target.qml");

А создание непосредственно самого объекта на игровой арене осуществляется в ниже следующей функции.

function createTarget(parent, row, column)
{
    var target = targetComponent.createObject(parent, {"row": row, "col": column})
    gameField[row][column] = target;
}

В нашем случае достаточно создать объект, применив метод createObject на заготовке компонента мишени с указанием родительского объекта, то есть объекта в котором будет располагаться мишень (в данном случае это будет игровая арена) и перечислив параметры, которые должны быть установлены при создании объекта, в данном случае это строка и колонка, в которых будет располагаться мишень.

Полный листинг файла

// Объявляем, как библиотеку, чтобы иметь доступ
// к разделяемым ресурсам, например состоянию игры
.pragma library

var gameState       // Локальное объявление состояния игры
                    // в нашем случае это будет игровая область gameArea
function getGameState() { return gameState; }

var gameField;      // Игровое поле, игровая сетка
// Создаём заготовку для мишеней
var targetComponent = Qt.createComponent("Target.qml");

// Инициализируем новое состояние игры
function newGameState(gameArea)
{
    gameState = gameArea;
    // Игровой сеткой будет служить двумерный массив,
    // в котором будем сохранять информацию о наличии в ячейках объектов
    gameField = create2DArray(gameState.rows, gameState.cols);
    return gameState;
}
// Функция получения случайного целого числа
// в диапазоне чисел включительно
function getRandomRound(min, max)
{
    return Math.round(Math.random() * (max - min) + min);
}

// Создаём мишень из компонента заготовки
function createTarget(parent, row, column)
{
    var target = targetComponent.createObject(parent, {"row": row, "col": column})
    gameField[row][column] = target;
}

// Мишень удаляется из массива сетки
function destroyTarget(row, column)
{
    gameField[row][column] = null;
}

// Функция создания двумерного массива сетки
function create2DArray(rows, columns)
{
  var arr = [];

  for (var i=0;i<rows;i++) {
     arr[i] = [];
  }

  return arr;
}

// Проверка на предмет наличия какого либо объекта в выбранной ячейке
function checkEmptyField(row, column)
{
    if (gameField[row][column] == null) {
        return true;
    } else {
        return false;
    }
}

Итог

В результате мы получим программу с игровым полем 6 на 6, в котором при запуске будут генерироваться исчезающие мишени.

Скачать исходники с Примером игры на QML с GitHub

Вся серия уроков

Видеоурок

Рекомендуем хостинг TIMEWEB
Рекомендуем хостинг TIMEWEB
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.

Вам это нравится? Поделитесь в социальных сетях!

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь
e
  • ehot
  • 31 марта 2024 г. 14:29

C++ - Тест 003. Условия и циклы

  • Результат:78баллов,
  • Очки рейтинга2
B

C++ - Тест 002. Константы

  • Результат:16баллов,
  • Очки рейтинга-10
B

C++ - Тест 001. Первая программа и типы данных

  • Результат:46баллов,
  • Очки рейтинга-6
Последние комментарии
k
kmssr8 февраля 2024 г. 18:43
Qt Linux - Урок 001. Автозапуск Qt приложения под Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий Кононенко5 февраля 2024 г. 1:50
Qt WinAPI - Урок 007. Работаем с ICMP Ping в Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25 декабря 2023 г. 10:30
Boost - статическая линковка в CMake проекте под Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJo25 декабря 2023 г. 8:38
Boost - статическая линковка в CMake проекте под Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
Gvozdik18 декабря 2023 г. 21:01
Qt/C++ - Урок 056. Подключение библиотеки Boost в Qt для компиляторов MinGW и MSVC Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Сейчас обсуждают на форуме
a
a_vlasov14 апреля 2024 г. 6:41
Мобильное приложение на C++Qt и бэкенд к нему на Django Rest Framework Евгений, добрый день! Такой вопрос. Верно ли следующее утверждение: Любое Android-приложение, написанное на Java/Kotlin чисто теоретически (пусть и с большими трудностями) можно написать и на C+…
Павел Дорофеев
Павел Дорофеев14 апреля 2024 г. 2:35
QTableWidget с 2 заголовками Вот тут есть кастомный QTableView с многорядностью проект поддерживается, обращайтесь
f
fastrex4 апреля 2024 г. 4:47
Вернуть старое поведение QComboBox, не менять индекс при resetModel Добрый день! У нас много проектов в которых используется QComboBox, в версии 5.5.1, когда модель испускает сигнал resetModel, currentIndex не менялся. В версии 5.15 при resetModel происходит try…
P
Pisych27 февраля 2023 г. 4:04
Как получить в массив значения из связанной модели? Спасибо, разобрался:))
AC
Alexandru Codreanu19 января 2024 г. 11:57
QML Обнулить значения SpinBox Доброго времени суток, не могу разобраться с обнулением значение SpinBox находящего в делегате. import QtQuickimport QtQuick.ControlsWindow { width: 640 height: 480 visible: tr…

Следите за нами в социальных сетях