Qt Quick 3D - Beispiel „Quick Ball“
Zeigt, wie man mit Quick3D ein einfaches Spiel erstellt.

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: 0Die 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.

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:

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