QML-Tutorial für Fortgeschrittene 3 - Implementieren der Spiellogik

Ein spielbares Spiel erstellen

Nun, da wir alle Spielkomponenten haben, können wir die Spiellogik hinzufügen, die bestimmt, wie ein Spieler mit den Blöcken interagiert und das Spiel spielt, bis es gewonnen oder verloren ist.

Zu diesem Zweck haben wir die folgenden Funktionen zu samegame.js hinzugefügt:

  • handleClick(x,y)
  • floodFill(xIdx,yIdx,type)
  • shuffleDown()
  • victoryCheck()
  • floodMoveCheck(xIdx, yIdx, type)

Da dies ein Tutorium über QML und nicht über Spieldesign ist, werden wir im Folgenden nur handleClick() und victoryCheck() besprechen, da sie direkt mit den QML-Typen verbunden sind. Beachten Sie, dass die Spiellogik hier zwar in JavaScript geschrieben ist, aber auch in C++ hätte geschrieben und dann in QML dargestellt werden können.

Aktivieren der Mausklick-Interaktion

Um dem JavaScript-Code die Schnittstelle zu den QML-Typen zu erleichtern, haben wir ein Element namens gameCanvas zu samegame.qml hinzugefügt. Es nimmt auch Mauseingaben des Benutzers entgegen. Hier ist der Code des Elements:

        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)
            }
        }

Das Element gameCanvas hat die exakte Größe der Tafel und verfügt über die Eigenschaften score und MouseArea, um Mausklicks zu verarbeiten. Die Blöcke werden nun als seine Kinder erstellt, und seine Abmessungen werden verwendet, um die Größe der Tafel zu bestimmen, damit die Anwendung auf die verfügbare Bildschirmgröße skaliert. Da seine Größe an ein Vielfaches von blockSize gebunden ist, wurde blockSize als QML-Eigenschaft aus samegame.js in samegame.qml verschoben. Beachten Sie, dass der Zugriff über das Skript weiterhin möglich ist.

Beim Anklicken ruft MouseArea handleClick() in samegame.js auf, das feststellt, ob der Klick des Spielers dazu führen soll, dass Blöcke entfernt werden, und aktualisiert gameCanvas.score gegebenenfalls mit dem aktuellen Punktestand. Hier ist die Funktion 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();
}

Wenn score eine globale Variable in der Datei samegame.js wäre, könnten Sie keine Verbindung zu ihr herstellen. Sie können nur an QML-Eigenschaften binden.

Aktualisieren des Spielstands

Wenn der Spieler auf einen Block klickt und handleClick() auslöst, ruft handleClick() auch victoryCheck() auf, um den Punktestand zu aktualisieren und zu prüfen, ob der Spieler das Spiel beendet hat. Hier ist der Code von 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);
}

Damit wird der Wert gameCanvas.score aktualisiert und ein "Game Over"-Dialog angezeigt, wenn das Spiel beendet ist.

Das Dialogfeld "Game Over" wird mit dem Typ Dialog erstellt, der in Dialog.qml definiert ist. Hier ist der Code Dialog.qml. Beachten Sie, dass er so konzipiert ist, dass er über die Funktionen und Signale in der Skriptdatei verwendet werden kann:

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();
    }
}

Und so wird er in der Hauptdatei samegame.qml verwendet:

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

Wir geben dem Dialog einen z Wert von 100, um sicherzustellen, dass er über unseren anderen Komponenten angezeigt wird. Der Standardwert von z für ein Element ist 0.

Ein Hauch von Farbe

Es macht keinen Spaß, Same Game zu spielen, wenn alle Blöcke dieselbe Farbe haben. Deshalb haben wir die Funktion createBlock() in samegame.js so geändert, dass sie bei jedem Aufruf zufällig einen anderen Blocktyp (entweder rot, grün oder blau) erzeugt. Block.qml wurde auch so geändert, dass jeder Block je nach Typ ein anderes Bild enthält:

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";
        }
    }
}
Ein funktionierendes Spiel

Jetzt haben wir ein funktionierendes Spiel! Die Blöcke können angeklickt werden, der Spieler kann punkten, und das Spiel kann beendet werden (und dann kann man ein neues beginnen). Hier ist ein Bildschirmfoto von dem, was bisher erreicht worden ist:

So sieht samegame.qml jetzt aus:

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?"
        }
    }
}

Das Spiel funktioniert, aber es ist im Moment ein bisschen langweilig. Wo sind die flüssigen animierten Übergänge? Wo sind die hohen Punktzahlen? Wenn du ein QML-Experte wärst, hättest du diese Dinge schon in der ersten Iteration schreiben können, aber in diesem Tutorial wurden sie bis zum nächsten Kapitel aufgespart - wo deine Anwendung lebendig wird!

Beispielprojekt @ code.qt.io

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.