QML 高级教程 3 - 实现游戏逻辑#

制作可玩游戏#

既然我们已经有所有游戏组件,我们可以添加游戏逻辑,该逻辑指定玩家如何与方块交互以及如何在赢得或输掉游戏之前玩游戏。

为此,我们在 samegame.js

  • handleClick(x,y)

  • floodFill(xIdx,yIdx,type)

  • shuffleDown()

  • victoryCheck()

  • floodMoveCheck(xIdx, yIdx, type)

由于这是一个关于 QML 而不是游戏设计的教程,因此我们只讨论 handleClick()victoryCheck(),因为它们直接与 QML 类型进行交互。请注意,尽管这里的游戏逻辑是用 JavaScript 编写的,但它也可以用 C++ 编写然后再暴露给 QML。

启用鼠标点击交互#

为了使 JavaScript 代码更容易与 QML 类型进行接口,我们已在 samegame.qml 中添加一个名为 gameCanvas 的项。它作为包含方块的项替换了背景。它还接受用户鼠标输入。以下是项代码

Item {
    id: gameCanvas

    property int score: 0
    property int blockSize: 40

    width: parent.width - (parent.width % blockSize)
    height: parent.height - (parent.height % blockSize)
    anchors.centerIn: parent

    MouseArea {
        anchors.fill: parent
        onClicked: (mouse)=> SameGame.handleClick(mouse.x, mouse.y)
    }
}

gameCanvas 项的大小与棋盘相同,并且有一个 score 属性和一个 MouseArea 来处理鼠标点击。现在,方块被创建为其子项,并且其尺寸用于确定棋盘大小,以便应用程序适应可用屏幕大小。由于其大小绑定到 blockSize 的倍数,因此 blockSize 已从 samegame.js 移动到 samegame.qml 作为 QML 属性。请注意,它仍然可以从脚本中访问。

当点击时,MouseArea 将在 samegame.js 中调用 handleClick(),该函数确定玩家的点击是否会导致移除任何方块,并且在必要时使用当前分数更新 gameCanvas.score。以下是 handleClick() 函数

function handleClick(xPos, yPos) {
    var column = Math.floor(xPos / gameCanvas.blockSize);
    var row = Math.floor(yPos / gameCanvas.blockSize);
    if (column >= maxColumn || column < 0 || row >= maxRow || row < 0)
        return;
    if (board[index(column, row)] == null)
        return;
    //If it's a valid block, remove it and all connected (does nothing if it's not connected)
    floodFill(column, row, -1);
    if (fillFound <= 0)
        return;
    gameCanvas.score += (fillFound - 1) * (fillFound - 1);
    shuffleDown();
    victoryCheck();
}

请注意,如果 scoresamegame.js 文件中的一个全局变量,您将无法将其绑定。您只能绑定到 QML 属性。

更新分数#

当玩家点击一个方框并触发 handleClick() 时,handleClick() 也会调用 victoryCheck() 以更新分数并检查玩家是否完成了游戏。以下是 victoryCheck() 代码。

function victoryCheck() {
    //Award bonus points if no blocks left
    var deservesBonus = true;
    for (var column = maxColumn - 1; column >= 0; column--)
        if (board[index(column, maxRow - 1)] != null)
        deservesBonus = false;
    if (deservesBonus)
        gameCanvas.score += 500;

    //Check whether game has finished
    if (deservesBonus || !(floodMoveCheck(0, maxRow - 1, -1)))
        dialog.show("Game Over. Your score is " + gameCanvas.score);
}

这会更新 gameCanvas.score 的值,并在游戏结束时显示“游戏结束”对话框。

“游戏结束”对话框使用在 Dialog.qml 中定义的 Dialog 类型创建。以下是 Dialog.qml 代码。注意,它被设计成可以从脚本文件通过函数和信号强制使用。

import QtQuick

Rectangle {
    id: container

    function show(text) {
        dialogText.text = text;
        container.opacity = 1;
    }

    function hide() {
        container.opacity = 0;
    }

    width: dialogText.width + 20
    height: dialogText.height + 20
    opacity: 0

    Text {
        id: dialogText
        anchors.centerIn: parent
        text: ""
    }

    MouseArea {
        anchors.fill: parent
        onClicked: hide();
    }
}

这是在主 samegame.qml 文件中使用它的方式。

Dialog {
    id: dialog
    anchors.centerIn: parent
    z: 100
}

我们给对话框分配了一个 z 值为 100,以确保它在我们的其他组件之上显示。项的默认 z 值为 0。

一点色彩#

如果所有方块都是同一颜色,那么玩 Same Game 就没有多少乐趣,所以我们修改了 samegame.js 中的 createBlock() 函数,每次调用时都随机创建不同类型的方块(红色、绿色或蓝色)。Block.qml 也已更改,以便每个方块根据其类型包含不同的图像。

import QtQuick

Item {
    id: block

    property int type: 0

    Image {
        id: img

        anchors.fill: parent
        source: {
            if (type == 0)
                return "pics/redStone.png";
            else if (type == 1)
                return "pics/blueStone.png";
            else
                return "pics/greenStone.png";
        }
    }
}

一个正在运行的游戏#

现在我们有一个可以运行的游戏了!方框可以被点击,玩家可以获得分数,游戏可以结束(然后你可以开始一个全新的游戏)。以下是目前已完成的截图。

../_images/declarative-adv-tutorial3.png

这是现在的 samegame.qml 的样子。

import QtQuick
import "samegame.js" as SameGame

Rectangle {
    id: screen

    width: 490; height: 720

    SystemPalette { id: activePalette }

    Item {
        width: parent.width
        anchors { top: parent.top; bottom: toolBar.top }

        Image {
            id: background
            anchors.fill: parent
            source: "pics/background.jpg"
            fillMode: Image.PreserveAspectCrop
        }

        Item {
            id: gameCanvas

            property int score: 0
            property int blockSize: 40

            width: parent.width - (parent.width % blockSize)
            height: parent.height - (parent.height % blockSize)
            anchors.centerIn: parent

            MouseArea {
                anchors.fill: parent
                onClicked: (mouse)=> SameGame.handleClick(mouse.x, mouse.y)
            }
        }
    }

    Dialog {
        id: dialog
        anchors.centerIn: parent
        z: 100
    }

    Rectangle {
        id: toolBar
        width: parent.width; height: 30
        color: activePalette.window
        anchors.bottom: screen.bottom

        Button {
            anchors { left: parent.left; verticalCenter: parent.verticalCenter }
            text: "New Game"
            onClicked: SameGame.startNewGame()
        }

        Text {
            id: score
            anchors { right: parent.right; verticalCenter: parent.verticalCenter }
            text: "Score: Who knows?"
        }
    }
}

游戏是可以工作的,但现在有点无聊。平滑的动画转换在哪里?高分排行榜在哪里?如果你是一个 QML 专家,你可以在第一次迭代中写下这些内容,但在本教程中,它们已保存到下一章 - 在那里你的应用程序将变得栩栩如生!

示例项目 @ code.qt.io