QML 고급 튜토리얼 4 - 마무리 작업

감각 추가하기

이제 게임에 활기를 불어넣기 위해 블록에 애니메이션을 적용하고 최고 점수 시스템을 추가하는 두 가지 작업을 해보겠습니다.

새로운 블록 애니메이션을 예상하여 Block.qml 파일의 이름을 BoomBlock.qml 으로 변경했습니다.

블록 움직임 애니메이션 적용

먼저 블록이 유동적으로 움직이도록 애니메이션을 적용하겠습니다. QML에는 유동적인 움직임을 추가하는 여러 가지 방법이 있으며, 이 경우 Behavior 유형을 사용하여 SpringAnimation 을 추가하겠습니다. BoomBlock.qml 에서 xy 속성에 SpringAnimation 동작을 적용하여 블록이 지정된 위치(값은 samegame.js 으로 설정됨)를 향해 스프링과 같은 방식으로 따라 움직이도록 애니메이션을 적용합니다.다음은 BoomBlock.qml 에 추가한 코드입니다:

property bool spawned: false

Behavior on x {
    enabled: block.spawned;
    SpringAnimation{ spring: 2; damping: 0.2 }
}
Behavior on y {
    SpringAnimation{ spring: 2; damping: 0.2 }
}

springdamping 값을 변경하여 애니메이션의 스프링과 같은 효과를 수정할 수 있습니다.

enabled: spawned 설정은 samegame.jscreateBlock() 에서 설정된 spawned 값을 참조합니다. 이렇게 하면 createBlock() 에서 블록을 올바른 위치로 설정한 후에만 xSpringAnimation 이 활성화됩니다. 그렇지 않으면 게임이 시작될 때 블록이 위에서부터 일렬로 떨어지는 대신 모서리(0,0)에서 미끄러져 떨어집니다. ( enabled: spawned 에 댓글을 달아 직접 확인해 보세요.)

블록 불투명도 변경 애니메이션

다음으로 부드러운 종료 애니메이션을 추가하겠습니다. 이를 위해 속성 변경이 발생할 때 기본 애니메이션을 지정할 수 있는 Behavior 유형을 사용하겠습니다. 이 경우 블록의 opacity 값이 변경되면 불투명도 값이 갑자기 완전히 보이거나 보이지 않게 바뀌는 대신 서서히 페이드 인 및 페이드 아웃되도록 애니메이션을 적용합니다. 이를 위해 BoomBlock.qml 에서 Image 유형의 opacity 속성에 Behavior 을 적용합니다:

Image {
    id: img

    anchors.fill: parent
    source: {
        if (block.type == 0)
            return "pics/redStone.png";
        else if (block.type == 1)
            return "pics/blueStone.png";
        else
            return "pics/greenStone.png";
    }
    opacity: 0

    Behavior on opacity {
        NumberAnimation { properties:"opacity"; duration: 200 }
    }
}

opacity: 0 는 블록이 처음 생성될 때 투명하다는 것을 의미합니다. 블록을 생성하고 소멸할 때 samegame.js 에서 불투명도를 설정할 수도 있지만, 다음에 추가할 애니메이션에 유용하므로 대신 상태를 사용하겠습니다. 처음에는 이러한 상태를 BoomBlock.qml 의 루트 유형에 추가합니다:

property bool dying: false
states: [
    State{ name: "AliveState"; when: spawned == true && dying == false
        PropertyChanges { target: img; opacity: 1 }
    },
    State{ name: "DeathState"; when: dying == true
        PropertyChanges { target: img; opacity: 0 }
    }
]

이제 블록 애니메이션을 구현할 때 spawned 을 참으로 설정했으므로 블록이 자동으로 페이드 인됩니다. 페이드 아웃하려면 블록이 소멸될 때 불투명도를 0으로 설정하는 대신 dying 을 true로 설정합니다( floodFill() 함수에서).

파티클 효과 추가하기

마지막으로 블록이 파괴될 때 멋지게 보이는 파티클 효과를 추가하겠습니다. 이를 위해 먼저 BoomBlock.qmlParticleSystem 를 다음과 같이 추가합니다:

ParticleSystem {
    id: sys
    anchors.centerIn: parent
    ImageParticle {
        // ![0]
        source: {
            if (block.type == 0)
                return "pics/redStar.png";
            else if (block.type == 1)
                return "pics/blueStar.png";
            else
                return "pics/greenStar.png";
        }
        rotationVelocityVariation: 360
        // ![0]
    }

    Emitter {
        id: particles
        anchors.centerIn: parent
        emitRate: 0
        lifeSpan: 700
        velocity: AngleDirection {angleVariation: 360; magnitude: 80; magnitudeVariation: 40}
        size: 16
    }
}

이를 완전히 이해하려면 Qt Quick 파티클 시스템 사용하기를 읽어야 하지만, 파티클이 정상적으로 방출되지 않도록 emitRate 이 0으로 설정되어 있다는 점에 유의해야 합니다. 또한 파티클 유형에 대해 burst() 메서드를 호출하여 파티클 버스트를 생성하는 dying 스테이트도 확장합니다. 이제 스테이트에 대한 코드는 다음과 같습니다:

states: [
    State {
        name: "AliveState"
        when: block.spawned == true && block.dying == false
        PropertyChanges { img.opacity: 1 }
    },

    State {
        name: "DeathState"
        when: block.dying == true
        StateChangeScript { script: particles.burst(50); }
        PropertyChanges { img.opacity: 0 }
        StateChangeScript { script: block.destroy(1000); }
    }
]

이제 플레이어의 모든 동작에 미묘한(또는 미묘하지 않은) 애니메이션이 추가되어 게임이 아름답게 애니메이션화되었습니다. 최종 결과는 아래와 같으며, 기본 테마를 보여주기 위해 다른 이미지 세트가 포함되어 있습니다:

여기서 테마 변경은 블록 이미지를 교체하기만 하면 됩니다. 이는 런타임에 Image source 프로퍼티를 변경하여 수행할 수 있으므로 추가 과제를 위해 다른 이미지로 테마를 전환하는 버튼을 추가할 수 있습니다.

고득점 표 유지

게임에 추가하고 싶은 또 다른 기능은 고득점을 저장하고 검색하는 방법입니다.

이를 위해 게임이 종료되면 플레이어의 이름을 요청하는 대화 상자를 표시하고 이를 고득점 표에 추가할 것입니다. 이를 위해서는 Dialog.qml 에 몇 가지 변경이 필요합니다. Text 유형 외에도 키보드 텍스트 입력을 받기 위한 TextInput 하위 항목이 추가됩니다:

Rectangle {
    id: container
    ...
    TextInput {
        id: textInput
        anchors { verticalCenter: parent.verticalCenter; left: dialogText.right }
        width: 80
        text: ""

        onAccepted: container.hide()    // close dialog when Enter is pressed
    }
    ...
}

showWithInput() 기능도 추가할 예정입니다. 텍스트 입력은 show() 대신 이 함수가 호출된 경우에만 표시됩니다. 대화 상자가 닫히면 closed() 신호를 보내고 다른 유형은 inputText 속성을 통해 사용자가 입력한 텍스트를 검색할 수 있습니다:

Rectangle {
    id: container
    property string inputText: textInput.text
    signal closed

    function show(text) {
        dialogText.text = text;
        container.opacity = 1;
        textInput.opacity = 0;
    }

    function showWithInput(text) {
        show(text);
        textInput.opacity = 1;
        textInput.focus = true;
        textInput.text = ""
    }

    function hide() {
        textInput.focus = false;
        container.opacity = 0;
        container.closed();
    }
    ...
}

이제 이 대화 상자는 samegame.qml 에서 사용할 수 있습니다:

Dialog {
    id: nameInputDialog
    anchors.centerIn: parent
    z: 100

    onClosed: {
        if (nameInputDialog.inputText != "")
            SameGame.saveHighScore(nameInputDialog.inputText);
    }
}

대화 상자가 closed 신호를 보내면 samegame.js 에서 새로운 saveHighScore() 함수를 호출하여 높은 점수를 SQL 데이터베이스에 로컬로 저장하고 가능한 경우 온라인 데이터베이스로도 점수를 보냅니다.

nameInputDialogsamegame.jsvictoryCheck() 함수에서 활성화됩니다:

function victoryCheck() {
    ...
    //Check whether game has finished
    if (deservesBonus || !(floodMoveCheck(0, maxRow - 1, -1))) {
        gameDuration = new Date() - gameDuration;
        nameInputDialog.showWithInput("You won! Please enter your name: ");
    }
}
고득점 오프라인 저장

이제 고득점 표를 실제로 저장하는 기능을 구현해야 합니다.

다음은 samegame.jssaveHighScore() 함수입니다:

function saveHighScore(name) {
    if (gameCanvas.score == 0)
        return;

    if (scoresURL != "")
        sendHighScore(name);

    var db = Sql.LocalStorage.openDatabaseSync("SameGameScores", "1.0", "Local SameGame High Scores", 100);
    var dataStr = "INSERT INTO Scores VALUES(?, ?, ?, ?)";
    var data = [name, gameCanvas.score, maxColumn + "x" + maxRow, Math.floor(gameDuration / 1000)];
    db.transaction(function(tx) {
        tx.executeSql('CREATE TABLE IF NOT EXISTS Scores(name TEXT, score NUMBER, gridSize TEXT, time NUMBER)');
        tx.executeSql(dataStr, data);

        var rs = tx.executeSql('SELECT * FROM Scores WHERE gridSize = "12x17" ORDER BY score desc LIMIT 10');
        var r = "\nHIGH SCORES for a standard sized grid\n\n"
        for (var i = 0; i < rs.rows.length; i++) {
            r += (i + 1) + ". " + rs.rows.item(i).name + ' got ' + rs.rows.item(i).score + ' points in ' + rs.rows.item(i).time + ' seconds.\n';
        }
        dialog.show(r);
    });
}

먼저 고득점을 온라인 데이터베이스로 전송할 수 있는 경우 sendHighScore() (아래 섹션에서 설명)를 호출합니다.

그런 다음 Local Storage API 을 사용하여 이 애플리케이션에 고유한 영구 SQL 데이터베이스를 유지합니다. openDatabaseSync() 을 사용하여 고득점자에 대한 오프라인 저장 데이터베이스를 만들고 이를 저장하는 데 사용할 데이터와 SQL 쿼리를 준비합니다. 오프라인 저장소 API는 데이터 조작 및 검색을 위해 SQL 쿼리를 사용하며, db.transaction() 호출에서는 세 개의 SQL 쿼리를 사용하여 데이터베이스를 초기화한 다음(필요한 경우) 고득점을 추가하고 검색합니다. 반환된 데이터를 사용하기 위해 반환된 행당 한 줄씩 문자열로 변환하고 해당 문자열이 포함된 대화 상자를 표시합니다.

이것은 높은 점수를 로컬에 저장하고 표시하는 한 가지 방법이지만 유일한 방법은 아닙니다. 더 복잡한 대안은 고득점 대화 상자 구성 요소를 만들고 처리 및 표시를 위해 결과를 전달하는 것입니다( Dialog)를 재사용하는 대신. 이렇게 하면 고득점자를 더 잘 표시할 수 있는 테마가 있는 대화 상자를 만들 수 있습니다. QML이 C++ 애플리케이션의 UI인 경우 점수를 C++ 함수에 전달하여 SQL이 없는 간단한 형식이나 다른 SQL 데이터베이스 등 다양한 방식으로 로컬에 저장할 수도 있습니다.

고득점 온라인 저장

로컬에 고득점을 저장하는 방법을 살펴보았지만, 웹 지원 고득점 저장소를 QML 애플리케이션에 통합하는 것도 쉽습니다. 고득점 데이터는 서버 어딘가에서 실행되는 PHP 스크립트에 게시되고, 해당 서버가 이를 저장하여 방문자에게 표시하는 매우 간단한 구현입니다. 동일한 서버에서 점수를 포함하고 표시하는 XML 또는 QML 파일을 요청할 수도 있지만 이 튜토리얼의 범위를 벗어납니다. 여기서 사용하는 PHP 스크립트는 examples 디렉터리에서 사용할 수 있습니다.

플레이어가 자신의 이름을 입력하면 데이터를 웹 서비스로 전송할 수 있습니다.

플레이어가 이름을 입력하면 samegame.js 에서 이 코드를 사용하여 데이터를 서비스로 전송합니다:

function sendHighScore(name) {
    var postman = new XMLHttpRequest()
        var postData = "name=" + name + "&score=" + gameCanvas.score + "&gridSize=" + maxColumn + "x" + maxRow + "&time=" + Math.floor(gameDuration / 1000);
    postman.open("POST", scoresURL, true);
    postman.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    postman.onreadystatechange = function() {
        if (postman.readyState == postman.DONE) {
            dialog.show("Your score has been uploaded.");
        }
    }
    postman.send(postData);
}

이 코드의 XMLHttpRequest 는 표준 브라우저 자바스크립트에서 볼 수 있는 XMLHttpRequest() 과 동일하며, 같은 방식으로 웹 서비스에서 XML 또는 QML을 동적으로 가져와서 높은 점수를 표시하는 데 사용할 수 있습니다. 이 경우 응답에 대해서는 걱정할 필요가 없습니다. 웹 서버에 고득점 데이터를 게시하기만 하면 됩니다. QML 파일(또는 QML 파일에 대한 URL)을 반환했다면 블록과 거의 동일한 방식으로 인스턴스화할 수 있습니다.

웹 기반 데이터에 액세스하고 제출하는 또 다른 방법은 이러한 용도로 설계된 QML 유형을 사용하는 것입니다. XmlListModel 을 사용하면 QML 애플리케이션에서 RSS와 같은 XML 기반 데이터를 매우 쉽게 가져와 표시할 수 있습니다.

여기까지입니다!

이 튜토리얼을 통해 QML로 완전한 기능을 갖춘 애플리케이션을 작성하는 방법을 살펴보았습니다:

이 튜토리얼에서 다루지 못한 QML에 대해 배울 수 있는 내용이 훨씬 더 많습니다. 모든 예제와 설명서를 확인하여 QML로 할 수 있는 모든 작업을 살펴보세요!

예제 프로젝트 @ 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.