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

Quick3D を使って簡単なゲームを作成する方法を示します。

この例では、Qt Quick と Qt 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 QuickMouseArea アイテムを使います。これは、ゲームがオンになっていて、ボールがまだ動いていないときだけ有効になります。

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 モジュールを使って星も追加します。他の2D Qt 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部分は終わりです。時間、スコア、スタートボタンなどを表示するための2D Qt Quickエレメントが残っていますが、これはゲームには重要ですが、このQuick 3Dのドキュメントには関係ありません。

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

サンプルプロジェクト @ code.qt.io

©2024 The Qt Company Ltd. ここに含まれるドキュメントの著作権は、それぞれの所有者に帰属します。 ここで提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。