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