Qt Quick 3D - クイックボールの例

Quick3Dを使用して簡単なゲームを作成する方法を説明します。

この例では、Qt QuickQt Quick 3D を組み合わせて簡単な 3D ゲームを作成する方法を示します。ゲームの目的は、ボールを投げてターゲットボックスに当てることです。どれだけ速く、どれだけ少ないボールで、すべてのターゲットを倒したかによってポイントが与えられます。よく狙いを定めて、素早く!

ソースコードは1つのQMLファイルになっており、このサンプルがいかにコンパクトであるかを強調しています。主なプロパティから見ていきましょう。これらは非常に自明で、ゲームにどのような影響を与えるか簡単に調整できます。

// 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

ゲームロジックはJavaScriptで実装されている。View3D 、ゲームを開始する関数があり、必要な変数を(再)初期化し、レベルターゲットを作成する。また、ゲーム終了時に最終得点を計算する関数も含まれています。

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

ビューには、シーンをライトアップするためのPointLight ノードも含まれています。これはオブジェクトの上に配置され、影を落とすように設定されます。ゲームが終了したときに、プレーエリアを暗くするために明るさがどのように使用されているかに注意してください。ambientColor プロパティは、光のコントラストを和らげるために使用されます。このプロパティがないと、オブジェクトの下部が非常に暗くなるからです。

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

ボールを投げるには、Qt Quick MouseArea 。これは、ゲームがオンで、ボールがまだ動いていないときだけ有効になります。

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

次に、実際の3Dモデルに入ります。ボールの挙動やアニメーション、当たり判定などのロジックが含まれています。まずはボールのプロパティを調べてみよう。ボールは組み込みの球体モデルを使用し、ballSize に基づいてスケーリングされます。テニスボールの外観を作成するために、diffuseMapnormalMap で DefaultMaterial を使用しています。

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
    }

マウスを動かすかタッチスクリーンをスワイプすると、ボールを離す前の最後のmaxMoves 位置がmoves 配列に格納されます。ユーザーがボールを離すと、throwBall() が呼び出され、この最新の位置からボールの方向を計算し、アニメーションを開始します。

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

ボールの位置は、異なる軸の間で別々にアニメーション化されます。これらのアニメーションは、ボールがどこに移動するかを定義するために、以前に割り当てられたdirectionXdirectionY を使用し、ボールが飛ぶ時間にはspeed を使用します。縦位置には2つの連続したアニメーションがあるので、ボールのバウンスにイージングを使うことができます。位置のアニメーションが終了したら、ボールがまだ残っているか、ゲームを終了すべきかをチェックします。最後に、ユーザーがカーブボールを投げられるように、ボールの回転もアニメーションさせます。

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

ゲームプレイで重要なのは、ボールがターゲットに当たったことを検出することです。ボールのzの位置が変わるたびに、targets の配列をループして、ボールがどれかに触れているかどうかをfuzzyEquals() を使って検出します。ヒットを検出するたびに、ターゲットhit() 関数を呼び出し、すべてのターゲットがダウンしているかどうかをチェックします。

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

それからターゲットを切り替える。これらのターゲットは、ヘルパー関数を含むグループ化ノードに動的に生成され、たとえば、すべてのターゲットをグループとしてアニメーションさせることができます。currentTargets プロパティが必要なのは、QML の配列の変更ではバインディングがトリガーされないためです。

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

ターゲットは、立方体モデルと、点を示すためのテキスト要素を持つノードです。ボールモデルと同様に、diffuseMapnormalMap テクスチャを使用して、Qt ロゴのあるキューブを作成します。ヒットが検出されると、立方体を順次アニメーションさせ、このターゲットから得たポイントを表示します。アニメーションが終了したら、ターゲットノードを動的に削除します。

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

競技エリアのモデルもいくつか必要です。グラウンド・モデルは、草のテクスチャを拡大縮小した矩形で、より大きな面積を埋めます。

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

空のモデルはさらに奥にあり、空に影が落ちるのは避けたいので、receivesShadows を false に設定します。空には Qt Quick Particlesモジュールを使って星も追加します。他の2DQt Quick 要素と同様に、パーティクルも3Dノード内に直接追加できます。

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

地上と空のモデルを組み合わせると、このような3Dワールドになります:

最後に、QtQuick3D.Particles3D モジュールを使って、ターゲットにキラキラを追加します。ParticleSystem3D にはSpriteParticle3D が含まれており、200100 のパーティクル・バーストを同時に2つ使用するのに十分な数を割り当てます。ParticleEmitter3D では、パーティクルのスケール、回転、速度、ライフスパンなどの放出プロパティを定義します。また、パーティクルを適切な大きさで下にドラッグするためのGravity3D アフェクタも追加します。

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

これでゲームの3D部分は終わりです。時間、スコア、スタートボタンなどを表示するための2DQt Quick 要素がまだいくつか残っています。これらはゲームにとって重要ですが、このQuick 3D ドキュメントには関係ありません。

さて、ボールはあなたの側にあります(ダジャレです)。自由にゲームを拡張し、新しい奇抜なレベルを生成してください!

プロジェクト例 @ 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.