En esta página

QML Tutorial Avanzado 4 - Toques Finales

Añadir un poco de estilo

Ahora vamos a hacer dos cosas para animar el juego: animar los bloques y añadir un sistema de puntuación máxima.

En previsión de las nuevas animaciones de bloques, el archivo Block.qml pasa a llamarse BoomBlock.qml.

Animar el movimiento de los bloques

Primero animaremos los bloques para que se muevan con fluidez. QML dispone de varios métodos para añadir movimiento fluido, y en este caso vamos a utilizar el tipo Behavior para añadir un comportamiento SpringAnimation. En BoomBlock.qml, aplicamos un comportamiento SpringAnimation a las propiedades x y y para que el bloque siga y anime su movimiento de forma elástica hacia la posición especificada (cuyos valores se establecerán en samegame.js). He aquí el código añadido a 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 }
}

Los valores spring y damping pueden cambiarse para modificar el efecto de muelle de la animación.

El ajuste enabled: spawned se refiere al valor spawned que se establece desde createBlock() en samegame.js. Esto asegura que el SpringAnimation en el x sólo se active después de que createBlock() haya colocado el bloque en la posición correcta. De lo contrario, los bloques se deslizarán fuera de la esquina (0,0) cuando comience una partida, en lugar de caer desde arriba en filas. (Prueba a comentar enabled: spawned y compruébalo por ti mismo).

Animación de los cambios de opacidad de los bloques

A continuación, añadiremos una animación de salida suave. Para ello, utilizaremos un tipo Behavior, que nos permite especificar una animación por defecto cuando se produce un cambio de propiedad. En este caso, cuando cambie la opacity de un Bloque, animaremos el valor de opacidad para que se vaya desvaneciendo gradualmente, en lugar de cambiar bruscamente entre totalmente visible e invisible. Para ello, aplicaremos un Behavior en la propiedad opacity del tipo Image en BoomBlock.qml:

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

Observe que opacity: 0 significa que el bloque es transparente cuando se crea por primera vez. Podríamos establecer la opacidad en samegame.js cuando creamos y destruimos los bloques, pero en su lugar utilizaremos estados, ya que esto es útil para la siguiente animación que vamos a añadir. Inicialmente, añadimos estos Estados al tipo raíz de 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 }
    }
]

Ahora los bloques se desvanecerán automáticamente, puesto que ya establecimos spawned en true cuando implementamos las animaciones de los bloques. Para el fundido de salida, establecemos dying a true en lugar de establecer la opacidad a 0 cuando se destruye un bloque (en la función floodFill() ).

Añadir efectos de partículas

Por último, añadiremos un bonito efecto de partículas a los bloques cuando se destruyan. Para ello, primero añadiremos un ParticleSystem en BoomBlock.qml, de esta forma:

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

Para entenderlo completamente deberías leer Usando el Sistema de Partículas Qt Quick , pero es importante tener en cuenta que emitRate se pone a cero para que las partículas no se emitan normalmente. Además, extendemos el Estado dying, que crea una ráfaga de partículas llamando al método burst() sobre el tipo de partículas. El código para los estados ahora se ven así:

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

Ahora el juego está bellamente animado, con animaciones sutiles (o no tan sutiles) añadidas para todas las acciones del jugador. El resultado final se muestra a continuación, con un conjunto diferente de imágenes para demostrar la tematización básica:

El cambio de tema aquí se produce simplemente sustituyendo las imágenes de bloque. Esto se puede hacer en tiempo de ejecución cambiando la propiedad Image source , por lo que para un mayor desafío, usted podría añadir un botón que cambia entre temas con diferentes imágenes.

Mantener una tabla de puntuaciones altas

Otra característica que podríamos querer añadir al juego es un método para almacenar y recuperar las puntuaciones más altas.

Para ello, mostraremos un diálogo cuando termine la partida para solicitar el nombre del jugador y añadirlo a una tabla de Puntuaciones Altas. Esto requiere algunos cambios en Dialog.qml. Además de un tipo Text, ahora tiene un elemento hijo TextInput para recibir la entrada de texto del teclado:

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

También añadiremos una función showWithInput(). La entrada de texto sólo será visible si se llama a esta función en lugar de a show(). Cuando el diálogo se cierra, emite una señal closed(), y otros tipos pueden recuperar el texto introducido por el usuario a través de una propiedad 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();
    }
    ...
}

Ahora el diálogo puede utilizarse en samegame.qml:

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

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

Cuando el diálogo emite la señal closed, llamamos a la nueva función saveHighScore() en samegame.js, que almacena la puntuación más alta localmente en una base de datos SQL y también envía la puntuación a una base de datos online si es posible.

El nameInputDialog se activa en la función victoryCheck() en samegame.js:

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: ");
    }
}
Almacenamiento de puntuaciones altas fuera de línea

Ahora necesitamos implementar la funcionalidad para guardar realmente la tabla de Puntuaciones Altas.

Aquí está la función saveHighScore() en samegame.js:

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

Primero llamamos a sendHighScore() (explicado en la sección de abajo) si es posible enviar los puntajes altos a una base de datos en línea.

Luego, usamos Local Storage API para mantener una base de datos SQL persistente única para esta aplicación. Creamos una base de datos de almacenamiento fuera de línea para las puntuaciones más altas utilizando openDatabaseSync() y preparamos los datos y la consulta SQL que queremos utilizar para guardarlos. La API de almacenamiento fuera de línea utiliza consultas SQL para la manipulación y recuperación de datos, y en la llamada a db.transaction() utilizamos tres consultas SQL para inicializar la base de datos (si es necesario) y, a continuación, añadir y recuperar las puntuaciones más altas. Para utilizar los datos devueltos, los convertimos en una cadena con una línea por fila devuelta, y mostramos un diálogo que contiene esa cadena.

Esta es una forma de almacenar y mostrar las puntuaciones más altas localmente, pero ciertamente no es la única. Una alternativa más compleja sería crear un componente de diálogo de puntuaciones altas, y pasarle los resultados para procesarlos y mostrarlos (en lugar de reutilizar Dialog). Esto permitiría un diálogo más tematizable que podría presentar mejor las puntuaciones altas. Si su QML es la interfaz de usuario para una aplicación C++, también podría haber pasado la puntuación a una función C++ para almacenarla localmente de varias maneras, incluyendo un formato simple sin SQL o en otra base de datos SQL.

Almacenamiento de puntuaciones altas en línea

Ya has visto cómo puedes almacenar puntuaciones altas localmente, pero también es fácil integrar un almacenamiento de puntuaciones altas habilitado para la web en tu aplicación QML. La implementación que hemos hecho aquí es muy sencilla: los datos de las puntuaciones máximas se envían a un script php que se ejecuta en algún servidor, y ese servidor los almacena y los muestra a los visitantes. También se podría solicitar a ese mismo servidor un archivo XML o QML que contenga y muestre las puntuaciones, pero eso está fuera del alcance de este tutorial. El script php que usamos aquí está disponible en el directorio examples.

Si el jugador introduce su nombre podemos enviar los datos al servicio web nosotros

Si el jugador introduce un nombre, enviamos los datos al servicio usando este código en 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);
}

El XMLHttpRequest en este código es el mismo que el XMLHttpRequest() que encontrarás en el JavaScript estándar del navegador, y puede ser usado de la misma manera para obtener dinámicamente XML o QML del servicio web para mostrar las puntuaciones más altas. En este caso no nos preocupamos de la respuesta, sólo enviamos los datos de las puntuaciones más altas al servidor web. Si hubiera devuelto un archivo QML (o una URL a un archivo QML) podría instanciarlo de forma muy similar a como lo hizo con los bloques.

Una forma alternativa de acceder y enviar datos basados en la web sería utilizar tipos QML diseñados para este fin. XmlListModel facilita mucho la obtención y visualización de datos basados en XML, como RSS, en una aplicación QML.

Ya está.

Siguiendo este tutorial has visto cómo puedes escribir una aplicación completamente funcional en QML:

Hay mucho más que aprender sobre QML que no hemos podido cubrir en este tutorial. Consulta todos los ejemplos y la documentación para ver todo lo que puedes hacer con QML.

Proyecto de ejemplo @ code.qt.io

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