Qt-based BB10 API Examples Documentation

Contents

Accel Game Example

Files:

Description

The Accel Game example demonstrates how to use sensors from the QtSensors module to move a player through a maze.

Overview

In this example we'll learn how to use the QAccelerometerSensor, QAccelerometerFilter and QAccelerometerReading classes to retrieve the current x/y/z values from the accelerometer sensor of the device. The values are used to trigger an direction change or move of the player inside the maze.

The UI

The UI of this sample application consists of a custom component (Maze.qml) that represents the maze board and a Button to start a new game.

The business logic of the application is encapsulated in the GameController class which is made available to the UI under the name '_gameController'.

    // The maze board
    Maze {
        horizontalAlignment: HorizontalAlignment.Center
        verticalAlignment: VerticalAlignment.Center
    }

The custom Maze component is placed at the left hand side of the screen and simply contains a background image and a Container with the object name 'board', which will be accessed from the business logic to place the stones and player items on it.

    // The board where the player object can be moved
    Container {
        objectName: "board"

        layoutProperties: AbsoluteLayoutProperties {
            positionX: 50
            positionY: 50
        }

        layout: AbsoluteLayout {}

        preferredWidth: 450
        preferredHeight: 450
    }

The button simply invokes the newGame() method of the GameController object when the user clicks on it.

    Button {
        horizontalAlignment: HorizontalAlignment.Center
        text: qsTr ("New Game")
        onClicked: _gameController.newGame()
    }

Player

The Player class encapsulates the business logic of a player on the maze board. It provides methods to change direction and position of the player.

    class Player : public QObject, public QScriptable
    {
        Q_OBJECT

    public:
        /**
         * Constructor takes the Board object, where the player should play on
         */
        Player(Board *board, QObject *parent = 0);
        ~Player();

    public Q_SLOTS:
        // Reset the player to its initial state
        void reset();

        // These methods turn and move the player in a given direction
        void goUp();
        void goRight();
        void goDown();
        void goLeft();

    Q_SIGNALS:
        // Emitted whenever the user has finished its move animation
        void moved();

    private:
        // Describes the possible directions the player can move to
        enum Direction {
            Up, Right, Down, Left
        };

        // Move the player in a given direction
        void go(Direction direction);

        // The Board object the player is playing on
        Board *m_board;

        // The tile that represents the player in the UI
        bb::cascades::ImageView *m_playerTile;

        // The direction the player is currently moving
        Direction m_currentDirection;

        // The current position of the player on the board
        QPoint m_currentPosition;

        // The move animation that is current running
        bb::cascades::AbstractAnimation *m_currentAnimation;
    };

The Player class also stores a reference to the UI object (ImageView) that represents the player on the screen.

Inside the constructor we create the ImageView, load the player image and add it to the Container that acts as board.

    Player::Player(Board *board, QObject *parent)
        : QObject(parent)
        , m_board(board)
        , m_playerTile(new ImageView)
        , m_currentDirection(Up)
        , m_currentPosition(0, 0)
        , m_currentAnimation(0)
    {

        // Initialise the player tile and add it to the board container
        m_playerTile->setPreferredWidth(s_tileSize);
        m_playerTile->setPreferredHeight(s_tileSize);
        m_playerTile->setImage(Image(QUrl("asset:///images/player.png")));
        m_board->board()->add(m_playerTile);

        // Ensure that the x/y position is really 0,0 otherwise using the
        // translationX/translationY properties does not work as expected.
        AbsoluteLayoutProperties *props
            = qobject_cast<AbsoluteLayoutProperties*>(m_playerTile->layoutProperties());

        if (props) {
            props->setPositionX(0);
            props->setPositionY(0);
        }

    }

If the goUp() method is invoked, we simply forward this to a parameterized call of the go() method, in which all the movement handling of the player is calculated.

    void Player::goUp()
    {
        go(Up);
    }

In the go() method we update the current direction and then calculate the new position depending on our current position and the current direction. If the canMoveTo() method of the Board object returns true (that means there is no wall) for the new position, we update our position. Afterwards we update the UI by starting an animation that will move the player item (ImageView) to the new position and applies a rotation if necessary.

    void Player::go(Direction direction)
    {

        // Update the current direction
        m_currentDirection = direction;

        // A falg to store whether the player can actually move
        bool move = false;

        // Depending on the new direction rotate the plater and move it one step
        switch (direction) {
        case Up:
            if (m_board->canMoveTo(m_currentPosition.x(), m_currentPosition.y() - 1)) {
                move = true;
                m_currentPosition.setY(m_currentPosition.y() - 1);
            }
            break;
        case Right:
            if (m_board->canMoveTo(m_currentPosition.x() + 1, m_currentPosition.y())) {
                move = true;
                m_currentPosition.setX(m_currentPosition.x() + 1);
            }
            break;
        case Down:
            if (m_board->canMoveTo(m_currentPosition.x(), m_currentPosition.y() + 1)) {
                move = true;
                m_currentPosition.setY(m_currentPosition.y() + 1);
            }
            break;
        case Left:
            if (m_board->canMoveTo(m_currentPosition.x() - 1, m_currentPosition.y())) {
                move = true;
                m_currentPosition.setX(m_currentPosition.x() - 1);
            }
            break;
        default:
            break;
        }

        // Update the position of the player tile on screen

        // Stop any previously running animation
        if (m_currentAnimation) {

            m_currentAnimation->stop();
            m_currentAnimation->deleteLater();

        }

        // Calculate the rotation of the player image depending on the direction
        const int rotationOffset
            = (move ? ((m_currentDirection == Up || m_currentDirection == Left)
                ? -115 : 115) : 0);

        // The animation should take 80 milliseconds
        const int duration = 80;

        // Create a new move animation
        // It's a parallel animation that consists of two translate transition for x & y
        // directions and the rotate transition.
        m_currentAnimation = ParallelAnimation::create(m_playerTile)
            .add(TranslateTransition::create()
                .toX(m_currentPosition.x() * s_tileSize)
                .duration(duration).easingCurve(StockCurve::Linear))
            .add(TranslateTransition::create()
                .toY(m_currentPosition.y() * s_tileSize)
                .duration(duration).easingCurve(StockCurve::Linear))
            .add(RotateTransition::create()
                .toAngleZ(m_playerTile->rotationZ() + rotationOffset)
                .duration(duration).easingCurve(StockCurve::Linear));

        // Emit the moved() signal when the animation has finished,
        // so that the GameController can evaluate the next input
        bool ok = connect(m_currentAnimation, SIGNAL(ended()), SIGNAL(moved()));
        Q_ASSERT(ok);
        Q_UNUSED(ok);
        // Start the animation
        m_currentAnimation->play();

    }

Board

The Board class contains all the business logic for handling the maze board. It generates a distribution of blocks on the maze and the tiles (ImageViews) that are displayed in the UI.

    class Board : public QObject
    {
        Q_OBJECT

    public:
        // We take the Container that represents the maze board as a parameter
        Board(bb::cascades::Container *boardContainer, QObject *parent = 0);

        /**
         * Returns whether a player can move to the given position.
         * The position is given in logical coordinates (0-9).
         */
        bool canMoveTo(int x, int y) const;

        /**
         * The Container that represents the maze board.
         */
        bb::cascades::Container *board() const;

    public Q_SLOTS:
        /**
         * Reset the maze board and regenerate all blocks.
         */
        void reset();

    private:
        // The Container object that represents the maze board in the UI
        bb::cascades::Container *m_board;

        // The list of block tiles that we created
        QVector<bb::cascades::Control *> m_blocks;

        // The map where we store the locations of the blocks inside the maze
        QVector<QVector<bool> > m_blockMap;

    };

Inside the constructor we fill the board representation (a QVector<QVector<bool>>) with false to mark the complete board as empty. Later on we'll change the values to true at the positions where a wall is located.

    Board::Board(bb::cascades::Container *boardContainer, QObject *parent)
        : QObject(parent)
        , m_board(boardContainer)
    {

        // Initialize the random number generator so that we can use it for dynamic
        // block distribution
        qsrand(QDateTime::currentDateTime().toMSecsSinceEpoch());

        // Initialise the block map with 'false' for all cells -> no blocks available
        for (int x = 0; x < s_boardDimension; x++) {
            m_blockMap << QVector<bool>();
            for (int y = 0; y < s_boardDimension; y++)
                m_blockMap[x] << false;
        }

    }

The canMoveTo() method returns whether there is no wall at the requested position and therefor we can move the player there.

    bool Board::canMoveTo(int x, int y) const
    {

        // We can't move beyond the borders of the board
        if (x < 0 || x >= s_boardDimension || y < 0 || y >= s_boardDimension)
            return false;

        // We can't move to coordinate where a block is located
        return m_blockMap[x][y] == false;

    }

The reset() method rebuilds the layout of the maze by placing a couple of walls at random positions.

    void Board::reset()
    {

        // Remove all block controls from the board...
        Q_FOREACH (Control *block, m_blocks) {
            m_board->remove(block);
        }

        // ... and delete them
        qDeleteAll(m_blocks);
        m_blocks.clear();

        // Clear out internal block map
        for (int x = 0; x < s_boardDimension; x++)
            for (int y = 0; y < s_boardDimension; y++)
                m_blockMap[x][y] = false;

        QVector<QPoint> blockCoordinates;

        // For dynamic block distribution we use random coordinates
        for (int i = 0; i < 20; i++) {

            const QPoint newPoint(qrand() % s_boardDimension, qrand() % s_boardDimension);

            // Contains a block already
            if (blockCoordinates.contains(newPoint))
                continue;

            // That's the starting place for the player
            if (newPoint == QPoint(0, 0))
                continue;

            blockCoordinates << newPoint;

        }

        // Generate the new blocks
        Q_FOREACH (const QPoint position, blockCoordinates) {

            // Mark as occupied in blockMap
            m_blockMap[position.x()][position.y()] = true;

            // Create block tile
            ImageView *block = new ImageView();
            block->setPreferredWidth(50);
            block->setPreferredHeight(50);
            block->setImage(Image(QUrl("asset:///images/block.png")));
            block->setTranslationX(position.x() * s_tileSize);
            block->setTranslationY(position.y() * s_tileSize);

            // Add the block tile to the board container...
            m_board->add(block);

            // ... and store the object in our internal list, so that we can
            // clean it up later
            m_blocks << block;

        }
    }

GameController

The GameController is the central class of this application. It contains the objects that encapsulate the business logic (Board and Player) and the AccelerationSensor, which triggers the movement of the player.

    class GameController : public QObject
    {
        Q_OBJECT

    public:
        GameController(QObject *parent = 0);

        /**
         * Set which Container object should be the board for the game.
         */
        void setBoard(bb::cascades::Container *board);

    public Q_SLOTS:
        // Called when 'New Game' action selected in UI
        void newGame();

    private Q_SLOTS:
        /**
         * Evalue the sensor data and move the player depending on sensor values.
         */
        void evaluateInput();

    private:
        // The Container Object the game is run on
        bb::cascades::Container *m_boardContainer;

        // The Board object that contains the business logic for the maze board
        Board *m_board;

        // The Player object that contains the business logic for the main player
        Player *m_player;

        AccelerationSensor m_sensor;

    };

Inside the constructor we start the acceleration sensor, so that we can access the current data from the hardware later on.

    GameController::GameController(QObject *parent)
        : QObject(parent)
        , m_boardContainer(0)
        , m_board(0)
        , m_player(0)
    {
        // Start the sensor to gather data
        m_sensor.start();
    }

The setBoard() method is called after the UI has been initialized. We store the pointer to the Container that represents the maze here and create the Board and Player business logic objects. Since we want the animation of a player move to finish before we start the next animation, the evaluation of the acceleration sensor is not time driven but depends on the moved() signal of the Player object, which is emitted after a move animation has been finished. However for the initial case we have to call evaluateInput() explicitly.

    void GameController::setBoard(bb::cascades::Container *board)
    {
        /*
         * Now that we know on which Container we are supposed to work,
         * create our Board and Player objects that handle logic.
         */
        m_boardContainer = board;
        m_board = new Board(m_boardContainer, this);
        m_board->reset();

        m_player = new Player(m_board, this);
        m_player->reset();

        // Whenever the player has finished its move animation we check for new input
        bool ok = connect(m_player, SIGNAL(moved()), SLOT(evaluateInput()), Qt::QueuedConnection);
        Q_ASSERT(ok);
        Q_UNUSED(ok);

        // and to kick things off...
        evaluateInput();
    }

Inside evaluateInput() we simply call the different move methods on the Player object depending on the current values reported by the AccelerationSensor.

    void GameController::evaluateInput()
    {
        if (!m_player)
            return;

        // Move the player depending on the current x/y/z values of the sensor
        if (m_sensor.x() > 0.8)
            m_player->goLeft();
        else if (m_sensor.x() < -0.8)
            m_player->goRight();
        else if (m_sensor.y() > 0.8)
            m_player->goDown();
        else if (m_sensor.y() < -0.8)
            m_player->goUp();
        else // Try again in 80 milliseconds
            QTimer::singleShot(80, this, SLOT(evaluateInput()));
    }

If the user clicks the 'New Game' button, the newGame() method is invoked, where we reset the board to regenerate a random maze and set the player back at its start position.

    void GameController::newGame()
    {
        // For a new game we let the Board generate a new random maze...
        m_board->reset();

        // ... and move the player back to its initial position
        m_player->reset();

        m_sensor.start();
        evaluateInput();
    }

AccelerationSensor

The AccelerationSensor class encapsulates the data gathering of the application. It contains a QAccelerometerSensor object, which does the low-level communication with the accelerometer sensor of the device, and provides the three properties 'x', 'y' and 'z' to make the current acceleration value available to the UI. It inherits from QAccelerometerFilter and reimplements the 'bool filter(QAccelerometerReading*)' method to retrieve the sensor data from the QAccelerometerSensor object.

    class AccelerationSensor : public QObject, public QAccelerometerFilter
    {
        Q_OBJECT

        // The properties to access the x/yz/ values of the acceleration sensor
        Q_PROPERTY(qreal x READ x NOTIFY xChanged)
        Q_PROPERTY(qreal y READ y NOTIFY yChanged)
        Q_PROPERTY(qreal z READ z NOTIFY zChanged)

    public:
        AccelerationSensor(QObject *parent = 0);

        // The accessor methods for the value properties
        qreal x() const;
        qreal y() const;
        qreal z() const;

    public Q_SLOTS:
        // Start gathering sensor values
        void start();

        // Stop gathering sensor values
        void stop();

    Q_SIGNALS:
        // The change notification signals of the value properties
        void xChanged();
        void yChanged();
        void zChanged();

    protected:
        /**
         * Called by the QAccelerometer whenever new values are available.
         */
        bool filter(QAccelerometerReading *reading);

    private:
        // The acceleration sensor
        QAccelerometer m_accelerationSensor;

        // The value properties
        qreal m_x;
        qreal m_y;
        qreal m_z;
    };

Inside the constructor we try to connect the QAccelerometerSensor object to the hardware backend. If that's successful, we register the AccelerationSensor class as filter for the QAccelerometerSensor object and start the sensor to gather data.

    AccelerationSensor::AccelerationSensor(QObject *parent)
        : QObject(parent)
        , m_x(0)
        , m_y(0)
        , m_z(0)
    {
        // At first we have to connect to the sensor backend...
        if (!m_accelerationSensor.connectToBackend())
            qWarning() << "Cannot connect to acceleration sensor backend!";

        // ... and then add a filter that will process the read data
        m_accelerationSensor.addFilter(this);

        // Use only the gravity information
        m_accelerationSensor.setAccelerationMode(QAccelerometer::Gravity);

        // Automatically map the sensor information according to current device orientation
        m_accelerationSensor.setAxesOrientationMode(QAccelerometer::AutomaticOrientation);
    }

The 'bool filter(QAccelerometerReading*)' method is called whenever the QAccelerometerSensor object retrieved new data from the hardware sensor. Inside this method we update the properties with the data from the sensor and inform the UI about possible value changes.

    bool AccelerationSensor::filter(QAccelerometerReading *reading)
    {
        // Store the previous values...
        const qreal oldX = m_x;
        const qreal oldY = m_y;
        const qreal oldZ = m_z;

        // ... update the property values with current sensor values
        m_x = reading->x();
        m_y = reading->y();
        m_z = reading->z();

        // ... and emit changed signals
        if (oldX != m_x)
            emit xChanged();
        if (oldY != m_y)
            emit yChanged();
        if (oldZ != m_z)
            emit zChanged();

        // Do no further processing of the sensor data
        return false;
    }