Qt Quick 3D - Beispiel „Quick Ball“

Zeigt, wie man mit Quick3D ein einfaches Spiel erstellt.

Spielszene mit Ball und Zielfeldern mit dem Qt-Logo

Dieses Beispiel zeigt, wie man „ Qt Quick “ und „ Qt Quick 3D “ kombinieren kann, um ein einfaches 3D-Spiel zu erstellen. Das Ziel des Spiels ist es, Zielfelder zu treffen, indem man einen Ball wirft. Die Punkte werden danach vergeben, wie schnell und mit wie wenigen Bällen alle Ziele getroffen werden. Ziel gut, aber sei schnell!

Der Quellcode befindet sich in einer einzigen QML-Datei, um zu verdeutlichen, wie kompakt dieses Beispiel ist – wenn man bedenkt, dass es sich um ein voll spielbares Spiel handelt. Schauen wir uns zunächst die wichtigsten Eigenschaften an. Diese sind ziemlich selbsterklärend, und du kannst sie leicht anpassen, um zu sehen, wie sie sich auf das Spiel auswirken.

// 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 Spielelogik ist in JavaScript implementiert. „ View3D “ enthält eine Funktion zum Starten des Spiels, die alle erforderlichen Variablen (neu) initialisiert und die Level-Ziele erstellt. Außerdem enthält sie eine Funktion zur Berechnung der Endpunktzahl, 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();
    mainWindow.score = mainWindow.timeBonus = mainWindow.ballsBonus = 0;
    mainWindow.currentBalls = mainWindow.gameBalls;
    mainWindow.gameOn = true;
    mainWindow.playingStarted = true;
}

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

Die Ansicht enthält zudem einen „ PointLight “-Knoten, um die Szene zu beleuchten. Er ist über den Objekten positioniert und so eingestellt, dass er Schatten wirft. Beachten Sie, wie die Helligkeit genutzt wird, um den Spielbereich nach Spielende abzudunkeln. Die Eigenschaft „ ambientColor “ dient dazu, den Lichtkontrast abzuschwächen, da die unteren Teile der Objekte ohne sie 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
        }
    }
}

Das Werfen des Balls erfolgt über das Element „ Qt Quick “ unter „ MouseArea “, das nur aktiviert ist, wenn das Spiel läuft 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();
    }
}

Nun kommen wir zu den eigentlichen 3D-Modellen. Das Ballmodell ist das umfangreichste, da es die Logik für das Verhalten des Balls, seine Animationen und die Treffererkennung enthält. Schauen wir uns zunächst die Eigenschaften des Balls an. Der Ball verwendet ein integriertes Kugelmodell, das anhand von „ ballSize “ skaliert wird. Wir nutzen „ PrincipledMaterial “ mit einer „ baseColorMap “ und einer „ normalMap “, um das Aussehen eines Tennisballs zu erzeugen.

Kombination aus Grundfarbkarte und Normalkarte zur Erstellung einer Textur für einen Tennisball

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: PrincipledMaterial {
        baseColorMap: Texture {
            source: "images/ball.jpg"
        }
        normalMap: Texture {
            source: "images/ball_n.jpg"
        }
        normalStrength: 1.0
    }

Wenn die Maus bewegt oder auf dem Touchscreen gewischt wird, werden die letzten Positionen von „ maxMoves “ vor dem Loslassen des Balls im Array „ moves “ gespeichert. Wenn der Benutzer den Ball loslässt, wird „ throwBall() “ aufgerufen, das anhand dieser letzten Positionen die Richtung des Balls 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 die verschiedenen Achsen separat animiert. Diese Animationen verwenden die zuvor zugewiesenen Werte aus ` directionX ` und ` directionY `, um zu definieren, wohin sich der Ball bewegt, sowie ` speed ` für die Flugzeit des Balls. Die vertikale Position verfügt über zwei aufeinanderfolgende Animationen, sodass wir für das Abprallen des Balls eine Easing-Funktion verwenden können. Wenn die Positionsanimationen beendet sind, prüfen wir, ob noch Bälle übrig sind oder ob das Spiel beendet werden soll. Schließlich animieren wir auch die Drehung des Balls, damit der Benutzer Kurvenwürfe ausführen 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 Spielablaufs ist die Erkennung, wann der Ball die Ziele trifft. Immer wenn sich die z-Position des Balls ändert, durchlaufen wir das Array „ targets “ und prüfen mithilfe von „ fuzzyEquals() “, ob der Ball eines der Ziele berührt. Sobald wir einen Treffer erkennen, rufen wir die Funktion „ hit() “ des Ziels auf und prüfen, ob alle Ziele ausgeschaltet sind.

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

Anschließend können wir zu den Zielen wechseln. Diese werden dynamisch in einem Gruppierungsknoten generiert, der Hilfsfunktionen enthält und es beispielsweise ermöglicht, alle Ziele als Gruppe zu animieren. Beachten Sie, dass die Eigenschaft „ currentTargets “ erforderlich ist, da Änderungen an QML-Arrays keine Bindungen auslösen; daher aktualisieren wir die Anzahl der Ziele manuell.

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

Ziele sind Knoten mit einem Würfelmodell und einem Textelement zur Anzeige von Punkten. Ähnlich wie beim Ballmodell verwenden wir die Texturen „ baseColorMap “ und „ normalMap “, um Würfel mit einem Qt-Logo zu erstellen. Wenn ein Treffer erkannt wird, animieren wir den Würfel schrittweise weg und zeigen die von diesem Ziel erzielten Punkte an. Sobald die Animation beendet ist, entfernen wir den Zielknoten dynamisch.

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: PrincipledMaterial {
                baseColorMap: Texture {
                    source: "images/qt_logo.jpg"
                }
                normalMap: Texture {
                    source: "images/qt_logo_n.jpg"
                }
                normalStrength: 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"
        }
    }
}

Außerdem benötigen wir einige Modelle für den Spielbereich. Das Bodenmodell ist ein Rechteck mit Gras-Texturen, die so skaliert sind, dass sie eine größere Fläche ausfüllen.

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

Das Himmelsmodell befindet sich weiter hinten, und da wir keine Schatten im Himmel haben wollen, setzen wir „ receivesShadows “ auf „false“. Für den Himmel fügen wir außerdem mithilfe des Qt Quick Particles Modul. Ähnlich wie bei anderen 2D- Qt Quick -Elementen können auch Partikel direkt in 3D-Knoten eingefü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: PrincipledMaterial {
        baseColorMap: 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 die oben genannten Boden- und Himmelsmodelle kombinieren, erhalten wir eine 3D-Welt wie diese:

Himmel, Boden und Partikel bilden gemeinsam die Spielumgebung

Zum Schluss fügen wir noch einige Funken für die Ziele hinzu, diesmal mithilfe des Moduls „ QtQuick3D.Particles3D “. „ ParticleSystem3D “ enthält ein „ SpriteParticle3D “, und wir weisen ihnen „ 200 “ zu, was für zwei gleichzeitige Partikelsalven „ 100 “ ausreicht. „ ParticleEmitter3D “ definiert die Emissionseigenschaften der Partikel wie Skalierung, Rotation, Geschwindigkeit und Lebensdauer. Außerdem fügen wir einen „ Gravity3D “-Affektor hinzu, um die Partikel mit einer angemessenen Stärke 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-Elemente unter Qt Quick zur Anzeige von Zeit, Punktestand, Startknopf usw., die für das Spiel wichtig sind, für diese „Quick 3D“-Dokumentation jedoch nicht relevant sind.

Jetzt liegt der Ball bei dir (Wortspiel beabsichtigt). Du kannst das Spiel gerne auf verschiedene Weise erweitern und neue verrückte Levels erstellen!

Beispielprojekt @ code.qt.io

© 2026 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.