Qt Quick 3D - Quick Ball Beispiel

Zeigt, wie man ein einfaches Spiel mit Quick3D erstellt.

Dieses Beispiel zeigt, wie man Qt Quick und Qt Quick 3D kombiniert, um ein einfaches 3D-Spiel zu erstellen. Das Ziel des Spiels ist es, mit einem Ball Zielfelder zu treffen. Es gibt Punkte dafür, wie schnell und mit wie wenigen Bällen alle Ziele getroffen werden. Gut zielen, aber schnell sein!

Der Quellcode befindet sich in einer einzigen QML-Datei, um zu verdeutlichen, wie kompakt dieses Beispiel ist, wenn man bedenkt, dass es ein voll spielbares Spiel ist. Schauen wir uns zunächst die Haupteigenschaften an. Diese sind recht selbsterklärend, und Sie können sie leicht anpassen, um zu sehen, wie sie das Spiel beeinflussen.

// Scaling helpper
readonly property real px: 0.2 + Math.min(width, height) / 800
// This is false until the first game has started
property bool playingStarted: false
// This is true whenever game is on
property bool gameOn: false
// Sizes of our 3D models
readonly property real ballSize: 40
readonly property real targetSize: 120
// Playing time in seconds
readonly property real gameTime: 60
property real currentTime: 0
// Amount of balls per game
readonly property int gameBalls: 20
property int currentBalls: 0
// Scores
property int score: 0
property int timeBonus: 0
property int ballsBonus: 0

Die Spiellogik ist mit JavaScript implementiert. View3D enthält eine Funktion zum Starten des Spiels, die alle erforderlichen Variablen (neu) initialisiert und die Levelziele erstellt. Sie enthält auch eine Funktion zur Berechnung der Endpunkte, wenn das Spiel endet.

function createLevel1() {
    // Simple level of target items
    var level1 = [{ "x": 0, "y": 100, "z": -100, "points": 10 },
                  { "x": -300, "y": 100, "z": -400, "points": 10 },
                  { "x": 300, "y": 100, "z": -400, "points": 10 },
                  { "x": -200, "y": 400, "z": -600, "points": 20 },
                  { "x": 0, "y": 400, "z": -600, "points": 20 },
                  { "x": 200, "y": 400, "z": -600, "points": 20 },
                  { "x": 0, "y": 700, "z": -600, "points": 30 }];
    targetsNode.addTargets(level1);
}

function startGame() {
    ballModel.resetBall();
    targetsNode.resetTargets();
    createLevel1();
    score = timeBonus = ballsBonus = 0;
    currentBalls = gameBalls;
    gameOn = true;
    playingStarted = true;
}

function endGame() {
    if (targetsNode.currentTargets == 0) {
        // If we managed to get all targets down -> bonus points!
        timeBonus = mainWindow.currentTime;
        ballsBonus = currentBalls * 10;
    }
    gameOn = false;
    ballModel.resetBall();
}

Die Ansicht enthält auch einen PointLight Knoten, um die Szene zu beleuchten. Er wird über den Objekten positioniert und so eingestellt, dass er Schatten wirft. Beachten Sie, wie die Helligkeit verwendet wird, um den Spielbereich zu verdunkeln, wenn das Spiel beendet ist. Die Eigenschaft ambientColor wird verwendet, um den Lichtkontrast abzuschwächen, da ohne sie die unteren Teile der Objekte sehr dunkel wären.

PointLight {
    x: 400
    y: 1200
    castsShadow: true
    shadowMapQuality: Light.ShadowMapQualityHigh
    shadowFactor: 50
    quadraticFade: 2
    ambientColor: "#202020"
    brightness: mainWindow.gameOn ? 200 : 40
    Behavior on brightness {
        NumberAnimation {
            duration: 1000
            easing.type: Easing.InOutQuad
        }
    }
}

Zum Werfen des Balls wird das Element Qt Quick MouseArea verwendet, das nur aktiviert ist, wenn das Spiel eingeschaltet ist und sich der Ball nicht bereits bewegt.

MouseArea {
    anchors.fill: parent
    enabled: mainWindow.gameOn && !ballModel.ballMoving
    onPressed: {
        ballModel.moveBall(mouseX, mouseY);
    }
    onPositionChanged: {
        ballModel.moveBall(mouseX, mouseY);
    }
    onReleased: {
        ballModel.throwBall();
    }
}

Dann kommen wir zu den eigentlichen 3D-Modellen. Das Ballmodell ist das umfangreichste, denn es enthält die Logik, wie sich der Ball verhält, seine Animationen und die Treffererkennung. Schauen wir uns zuerst die Eigenschaften des Balls an. Der Ball verwendet ein eingebautes Kugelmodell, das auf der Grundlage von ballSize skaliert wird. Wir verwenden DefaultMaterial mit einem diffuseMap und einem normalMap, um das Aussehen eines Tennisballs zu erzeugen.

Model {
    id: ballModel
    property real directionX: 0
    property real directionY: 0
    // How many ms the ball flies
    readonly property real speed: 2000
    readonly property real ballScale: mainWindow.ballSize / 100
    property var moves: []
    readonly property int maxMoves: 5
    readonly property bool ballMoving: ballAnimation.running

    source: "#Sphere"
    scale: Qt.vector3d(ballScale, ballScale, ballScale)

    materials: DefaultMaterial {
        diffuseMap: Texture {
            source: "images/ball.jpg"
        }
        normalMap: Texture {
            source: "images/ball_n.jpg"
        }
        bumpAmount: 1.0
    }

Wenn die Maus bewegt oder über den Touchscreen gestrichen wird, werden die letzten maxMoves Positionen vor dem Loslassen des Balls im moves Array gespeichert. Wenn der Benutzer den Ball loslässt, wird throwBall() aufgerufen, das die Ballrichtung aus diesen letzten Positionen berechnet und mit der Animation beginnt.

function resetBall() {
    moves = [];
    x = 0;
    y = mainWindow.ballSize/2;
    z = 400;
}

function moveBall(posX, posY) {
    if (!mainWindow.gameOn) return;
    var pos = view3D.mapTo3DScene(Qt.vector3d(posX, posY, ballModel.z + mainWindow.ballSize));
    pos.y = Math.max(mainWindow.ballSize / 2, pos.y);
    var point = {"x": pos.x, "y": pos.y };
    moves.push(point);
    if (moves.length > maxMoves) moves.shift();
    // Apply position into ball model
    ballModel.x = pos.x;
    ballModel.y = pos.y;
}

function throwBall() {
    if (!mainWindow.gameOn) return;
    mainWindow.currentBalls--;
    var moveX = 0;
    var moveY = 0;
    if (moves.length >= 2) {
        var first = moves.shift();
        var last = moves.pop();
        moveX = last.x - first.x;
        moveY = last.y - first.y;
        if (moveY < 0) moveY = 0;
    }
    directionX = moveX * 20;
    directionY = moveY * 4;
    ballAnimation.start();
}

Die Ballposition wird für verschiedene Achsen getrennt animiert. Diese Animationen verwenden die zuvor zugewiesenen directionX und directionY, um zu definieren, wohin sich der Ball bewegt, sowie speed für die Flugzeit des Balls. Für die vertikale Position gibt es zwei aufeinander folgende Animationen, so dass wir für das Abprallen des Balls das Easing verwenden können. Wenn die Positionsanimationen beendet sind, prüfen wir, ob noch Bälle übrig sind oder das Spiel beendet werden soll. Schließlich animieren wir auch die Drehung des Balls, damit der Benutzer Kurvenbälle werfen kann.

ParallelAnimation {
    id: ballAnimation
    running: false
    // Move forward
    NumberAnimation {
        target: ballModel
        property: "z"
        duration: ballModel.speed
        to: -ballModel.directionY * 5
        easing.type: Easing.OutQuad
    }
    // Move up & down with a bounce
    SequentialAnimation {
        NumberAnimation {
            target: ballModel
            property: "y"
            duration: ballModel.speed * (1 / 3)
            to: ballModel.y + ballModel.directionY
            easing.type: Easing.OutQuad
        }
        NumberAnimation {
            target: ballModel
            property: "y"
            duration: ballModel.speed * (2 / 3)
            to: mainWindow.ballSize / 4
            easing.type: Easing.OutBounce
        }
    }
    // Move sideways
    NumberAnimation {
        target: ballModel
        property: "x"
        duration: ballModel.speed
        to: ballModel.x + ballModel.directionX
    }

    onFinished: {
        if (mainWindow.currentBalls <= 0)
            view3D.endGame();
        ballModel.resetBall();
    }
}

NumberAnimation on eulerRotation.z {
    running: ballModel.ballMoving
    loops: Animation.Infinite
    from: ballModel.directionX < 0 ? 0 : 720
    to: 360
    duration: 10000 / (2 + Math.abs(ballModel.directionX * 0.05))
}

Ein wichtiger Teil des Spiels ist die Erkennung, wann der Ball die Ziele trifft. Jedes Mal, wenn sich die z-Position des Balls ändert, durchlaufen wir das Array targets und erkennen mit fuzzyEquals(), ob der Ball eines der Ziele berührt. Sobald wir einen Treffer feststellen, rufen wir die Funktion hit() auf und prüfen, ob alle Ziele getroffen wurden.

onZChanged: {
    // Loop through target items and detect collisions
    var hitMargin = mainWindow.ballSize / 2 + mainWindow.targetSize / 2;
    for (var i = 0; i < targetsNode.targets.length; ++i) {
        var target = targetsNode.targets[i];
        var targetPos = target.scenePosition;
        var hit = ballModel.scenePosition.fuzzyEquals(targetPos, hitMargin);
        if (hit) {
            target.hit();
            if (targetsNode.currentTargets <= 0)
                view3D.endGame();
        }
    }
}

Dann können wir zu den Zielen wechseln. Diese werden dynamisch in einen Gruppierungsknoten generiert, der Hilfsfunktionen enthält und es z.B. ermöglicht, alle Ziele als Gruppe zu animieren. Beachten Sie, dass die Eigenschaft currentTargets benötigt wird, da in QML-Arrays Änderungen keine Bindungen auslösen, so dass wir die Anzahl der Ziele manuell aktualisieren müssen.

Node {
    id: targetsNode

    property var targets: []
    property int currentTargets: 0

    function addTargets(items) {
        items.forEach(function (item) {
            let instance = targetComponent.createObject(
                    targetsNode, { "x": item.x, "startPosY": item.y, "z": item.z, "points": item.points});
            targets.push(instance);
        });
        currentTargets = targets.length;
    }

    function removeTarget(item) {
        var index = targets.indexOf(item);
        targets.splice(index, 1);
        currentTargets = targets.length;
    }

    function resetTargets() {
        while (targets.length > 0)
            targets.pop().destroy();
        currentTargets = targets.length;
    }
}

Targets sind Knoten mit einem Würfelmodell und einem Textelement zur Anzeige von Punkten. Ähnlich wie beim Ballmodell verwenden wir diffuseMap und normalMap Texturen, um Würfel mit einem Qt-Logo zu erstellen. Wenn der Treffer erkannt wird, animieren wir den Würfel sequentiell weg und zeigen die von diesem Ziel erhaltenen Punkte an. Sobald die Animation beendet ist, wird der Zielknoten dynamisch entfernt.

Component {
    id: targetComponent
    Node {
        id: targetNode

        property int points: 0
        property real hide: 0
        property real startPosY: 0
        property real posY: 0
        property real pointsOpacity: 0

        function hit() {
            targetsNode.removeTarget(this);
            mainWindow.score += points;
            hitAnimation.start();
            var burstPos = targetNode.mapPositionToScene(Qt.vector3d(0, 0, 0));
            hitParticleEmitter.burst(100, 200, burstPos);
        }

        y: startPosY + posY
        SequentialAnimation {
            running: mainWindow.gameOn && !hitAnimation.running
            loops: Animation.Infinite
            NumberAnimation {
                target: targetNode
                property: "posY"
                from: 0
                to: 150
                duration: 3000
                easing.type: Easing.InOutQuad
            }
            NumberAnimation {
                target: targetNode
                property: "posY"
                to: 0
                duration: 1500
                easing.type: Easing.InOutQuad
            }
        }

        SequentialAnimation {
            id: hitAnimation
            NumberAnimation {
                target: targetNode
                property: "hide"
                to: 1
                duration: 800
                easing.type: Easing.InOutQuad
            }
            NumberAnimation {
                target: targetNode
                property: "pointsOpacity"
                to: 1
                duration: 1000
                easing.type: Easing.InOutQuad
            }
            NumberAnimation {
                target: targetNode
                property: "pointsOpacity"
                to: 0
                duration: 200
                easing.type: Easing.InOutQuad
            }
            ScriptAction {
                script: targetNode.destroy();
            }
        }

        Model {
            id: targetModel

            readonly property real targetScale: (1 + targetNode.hide) * (mainWindow.targetSize / 100)

            source: "#Cube"
            scale: Qt.vector3d(targetScale, targetScale, targetScale)
            opacity: 0.99 - targetNode.hide * 2
            materials: DefaultMaterial {
                diffuseMap: Texture {
                    source: "images/qt_logo.jpg"
                }
                normalMap: Texture {
                    source: "images/qt_logo_n.jpg"
                }
                bumpAmount: 1.0
            }
            Vector3dAnimation on eulerRotation {
                loops: Animation.Infinite
                duration: 5000
                from: Qt.vector3d(0, 0, 0)
                to: Qt.vector3d(360, 360, 360)
            }
        }
        Text {
            anchors.centerIn: parent
            scale: 1 + targetNode.pointsOpacity
            opacity: targetNode.pointsOpacity
            text: targetNode.points
            font.pixelSize: 60 * mainWindow.px
            color: "#808000"
            style: Text.Outline
            styleColor: "#f0f000"
        }
    }
}

Wir brauchen auch einige Modelle für den Spielbereich. Bodenmodell ist ein Rechteck mit Gras Texturen skaliert, um einen größeren Bereich zu füllen.

Model {
    source: "#Rectangle"
    scale: Qt.vector3d(50, 50, 1)
    eulerRotation.x: -90
    materials: DefaultMaterial {
        diffuseMap: Texture {
            source: "images/grass.jpg"
            tilingModeHorizontal: Texture.Repeat
            tilingModeVertical: Texture.Repeat
            scaleU: 25.0
            scaleV: 25.0
        }
        normalMap: Texture {
            source: "images/grass_n.jpg"
        }
        bumpAmount: 0.6
    }
}

Das Himmelsmodell liegt weiter hinten, und wir wollen keine Schatten in den Himmel werfen, also setzen wir receivesShadows auf false. Für den Himmel fügen wir auch einige Sterne mit Qt Quick Particles Modul. Ähnlich wie bei anderen 2D-Elementen Qt Quick können auch Partikel direkt in 3D-Knoten hinzugefügt werden.

Model {
    id: sky
    property real scaleX: 100
    property real scaleY: 20
    source: "#Rectangle"
    scale: Qt.vector3d(sky.scaleX, sky.scaleY, 1)
    position: Qt.vector3d(0, 960, -2000)
    // We don't want shadows casted into sky
    receivesShadows: false
    materials: DefaultMaterial {
        diffuseMap: Texture {
            source: "images/sky.jpg"
        }
    }
    // Star particles
    Node {
        z: 500
        y: 30
        // Stars are far away, scale up to half the resolution
        scale: Qt.vector3d(2 / sky.scaleX, 2 / sky.scaleY, 1)
        ParticleSystem {
            anchors.horizontalCenter: parent.horizontalCenter
            anchors.top: parent.top
            width: 3000
            height: 400
            ImageParticle {
                source: "qrc:///particleresources/star.png"
                rotationVariation: 360
                color: "#ffffa0"
                colorVariation: 0.1
            }
            Emitter {
                anchors.fill: parent
                emitRate: 4
                lifeSpan: 6000
                lifeSpanVariation: 4000
                size: 30
                sizeVariation: 20
            }
        }
    }
}

Wenn wir das Boden- und das Himmelsmodell kombinieren, erhalten wir eine 3D-Welt wie diese:

Zum Schluss fügen wir noch ein paar Funken für die Ziele hinzu, diesmal mit dem QtQuick3D.Particles3D Modul. ParticleSystem3D enthält ein SpriteParticle3D und wir weisen 200 oder ihnen zu, was für zwei gleichzeitige 100 Partikelausbrüche ausreicht. ParticleEmitter3D definiert die emittierenden Eigenschaften für die Partikel wie Skala, Rotation, Geschwindigkeit und lifeSpan. Wir fügen auch Gravity3D affector hinzu, um die Partikel mit einem geeigneten Ausmaß nach unten zu ziehen.

ParticleSystem3D {
    id: psystem
    SpriteParticle3D {
        id: sprite
        sprite: Texture {
            source: "images/particle.png"
        }
        color: Qt.rgba(1.0, 1.0, 0.0, 1.0)
        colorVariation: Qt.vector4d(0.4, 0.6, 0.0, 0.0)
        unifiedColorVariation: true
        maxAmount: 200
    }
    ParticleEmitter3D {
        id: hitParticleEmitter
        particle: sprite
        particleScale: 4.0
        particleScaleVariation: 2.0
        particleRotationVariation: Qt.vector3d(0, 0, 180)
        particleRotationVelocityVariation: Qt.vector3d(0, 0, 250)
        velocity: VectorDirection3D {
            direction: Qt.vector3d(0, 300, 0)
            directionVariation: Qt.vector3d(200, 150, 100)
        }
        lifeSpan: 800
        lifeSpanVariation: 200
        depthBias: 100
    }
    Gravity3D {
        magnitude: 600
    }
}

Damit sind die 3D-Teile unseres Spiels abgeschlossen. Es gibt noch einige 2D Qt Quick Elemente, um die Zeit, den Spielstand, den Startknopf usw. anzuzeigen, die für das Spiel wichtig sind, aber für diese Quick 3D Dokumentation nicht relevant sind.

Jetzt ist der Ball auf Ihrer Seite (Wortspiel beabsichtigt). Fühlen Sie sich frei, das Spiel auf verschiedene Arten zu erweitern und neue verrückte Levels zu erstellen!

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.