Showing posts with label QML. Show all posts
Showing posts with label QML. Show all posts

Monday, August 19, 2013

Introducing: Merry Cook


After a long hiatus since November 2011 I have released another clone of classic Russian handheld game from the '80s - Merry Cook. I knew that the "Nu, Pogodi!" code wasn't my top achievement, and I had to force myself into diving into it, but I feel it was worth it. Few things I think I did right this time:
  • Do not keep *any* game logic in QML. Qt has an excellent state machine framework, which makes writing the game logic in C++ relatively easy.
  • Keep the QML/C++ interface as simple as possible. Send signals from QML to C++ when user takes some action and update the QML UI from the C++ side by changing QProperties on some context property object. I've actually used two objects for that, because it made testing a bit easier.
  • Unit tests. I've set up the testing harness using gmock/gtest and I've used it to unit test some things. I probably would have been fine without them, since Merry Cook is a very simple but a) it forced me to divide stuff into more manageable classes and b) it gave me a sense of accomplishing something early. It's funny, because even though I'm absolutely conscious of the latter fact, I think it gave me enough boost to get to the point where I had moved forward with implementation and polishing, because I really wanted to publish this game.
  • QProperty helper. I wrote an abominable macro for reducing the QProperty boilerplate:
Things still on my TODO list:
  • More tests. Besides unit tests I'd also like to write some integration tests for the state machine setup and connections, but I didn't have time to think how this should be done without making too much state public just for testing. Maybe next time.
  • Refactor "Nu, Pogodi!". I jumped straight into new project, but I should have started with refactoring the old crap. On the other hand, it might have sucked out all the motivation out of me, and had I done it, I wouldn't have been writing this post right now. So, maybe next time.
  • Passing enums to QML. I have no idea what I did wrong, but I couldn't get the QML to see my C++ enums. I've resorted to passing them as simple ints and using magic numbers on QML side, but it's definitely something I should fix. Obviously not now, but next time.
Anyways, I'm really happy with the final results, especially with the gameplay experience, which I think mimics the original game very well. Try it yourself!

Wednesday, October 3, 2012

Screensaver blocking on Symbian

In my latest game the players spend significant amount of time just watching the screen and trying to figure the puzzle out. The first few levels are obvious and most players will sole them in a few seconds,m but as the difficulty of the puzzles increases the players stare at the screen longer and longer. At some point the screensaver would kick in and piss the player off (fortunately my ragtag QA/betatester team found this issue before launch).

Google search results are (as usual) helpful, but the most promising lead, the QSystemScreenSaver class is not a solution. There are three problems with it:

  1. The API of the class itself is terrible.
  2. The API of the related QML element is even worse.
  3. Last, but not least, it doesn't work (at least not in the Qt Mobility version shipped with the Qt SDK).

(BTW: these three points sums up pretty much every experience with Qt Mobility package I had. Qt devs should either kill this festering boil with fire or fix it and rename it, because I learned to dread everything remotely related to Qt Mobility, and I suspect I'm not the only one).

Anyways, let's get back to the core of the problem, i.e. "how to block the screensaver". Qt Mobility failed, but the task doesn't seems like a rocket science to me. Slightly different Google search suggested using native Symbian's User::ResetInactivityTime() method. Few minutes and one QTimer later, everything worked:
#ifndef SCREENSAVERBLOCKER_H
#define SCREENSAVERBLOCKER_H

#include <QObject>
#include <QApplication>
#include <QTimer>

class ScreenSaverBlocker : public QObject
{
    Q_OBJECT

public:
    explicit ScreenSaverBlocker(QObject *parent = 0) : QObject(parent) {
        mTimer.setInterval(1000);
        connect(&mTimer, SIGNAL(timeout()), this, SLOT(blockScreenSaver()));
        changeScreenSaverState(QApplication::activeWindow() != 0);
        if (qApp) {
            qApp->installEventFilter(this);
        }
    }

    void changeScreenSaverState(bool blockScreenSaver) {
        if (blockScreenSaver && !mTimer.isActive()) {
            mTimer.start();
        } else {
            mTimer.stop();
        }
    }

protected:
    bool eventFilter(QObject *obj, QEvent *event) {
        Q_UNUSED(obj)
        if (event->type() == QEvent::ApplicationActivate
         || event->type() == QEvent::ApplicationDeactivate) {
            changeScreenSaverState(event->type() == QEvent::ApplicationActivate);
        }
        return false;
    }

private slots:
    void blockScreenSaver() {
#ifdef Q_OS_SYMBIAN
        User::ResetInactivityTime();
#endif
    }

private:
    QTimer mTimer;
};

#endif // SCREENSAVERBLOCKER_H
The important thing in the code above is watching the ApplicationActivate and ApplicationDeactivate events - after all, when your app is in background, you shouldn't affect the phone behavior. I'm not sure if the app would fail the Nokia's QA process without this feature, but it seemed prudent to write the code this way.

If you want to use this object in your QML UI just register it with qmlRegisterType and add the registered import and QML element to your root element.

Wednesday, September 12, 2012

Screen orientation and QT applications on Symbian Belle

Let's take a break from Android gotchas and do some mobile necrophilia, i.e. let's talk about Symbian.

Recently I received an email from Nokians saying that they are testing Nokia Store content with new firmware update and my app won't work after update. After few quick email exchanges we narrowed down the problem to screen orientation locking code I wrote about some time ago. It turns out that things can be done much simpler:

Window {
    Rectangle {
        id: root
        anchors.fill: parent

        Component.onCompleted: {
            screen.allowedOrientations = Screen.Landscape
        }

        // more stuff
    }
}
int main(int argc, char *argv[])
{
    QScopedPointer<QApplication> app(createApplication(argc, argv));

    QmlApplicationViewer viewer;
    viewer.setMainQmlFile(QLatin1String("qml/nupagadi/GameArea.qml"));
    viewer.setOrientation(QmlApplicationViewer::ScreenOrientationLockLandscape);
    viewer.setResizeMode(QDeclarativeView::SizeRootObjectToView);
    viewer.showExpanded();

    return app->exec();
}
Less code, no need to comment it as a gotcha/workaround, and it's supposedly futureproof.

I'm very positively surprised with Nokians' approach, responsiveness and this whole experience. Of course I wouldn't be me if I didn't bitch a little bit, namely: why did I have this problem in the first place? I mean, locking the screen orientation is not a rocket science and should be well documented. It should, but unfortunately it's not, like so many things about QML.

Wednesday, May 30, 2012

QML applications on Nokia Belle

After the latest update of "Nu, Pogodi!", I received few negative reviews saying that the game doesn't work. I've tested the game thoroughly on all devices I was able to get my hands on, but I wasn't able to reproduce the error, so I decided to wait until I get more info. Few days ago with the help of one customer I was able to pin down the problem - the game failed to display any UI on new Belle firmware with Qt 4.8.0. I don't have such device myself, but fortunately the great Remote Device Access service allows testing on Nokia 808 PureView with latest Belle firmware. I've reproduced the error, wrote the Nokia Developers Support, and they sent me a very helpful link: Changes in Nokia Belle FP1. One issue listed there caught my eye:

If application does not specify a size for the root QML object and doesn’t use the Qt components Window as root element (Window component should not be used as a child item), it might cause the root window not to be shown.

Solution / Workaround:
Always declare a size for your custom QML root element.

I've checked my main QML file and indeed, I did not set the root element size, instead I've set the resize mode to SizeRootObjectToView and maximized the QDeclarativeView. I think it's the better solution than setting the root element size explicitly, because the display resolution is not the same on all Nokia phones (I'm looking at you, E6). Instead of doing that, I wrapped my entire UI into Window element from Qt Components and lo and behold, my game displayed something, although it wasn't exactly what I expected:


My code locked the screen orientation after loading main QML file, and it looked like the only thing that might cause this problem, so I changed the calls order. On Belle FP1 devices everything worked fine, but this change broke the display on devices with Anna and older Belle firmware:


Wat? The only solution I came up with was creating the utility method for detecting version of Qt during runtime and locking screen orientation after and before loading main QML file, depending on the Qt version. Relevant piece of code:

bool Utils::runtimeQtAtLeast(int major, int minor, int bugfix)
{
    const QStringList v = QString::fromAscii(qVersion()).split(QLatin1Char('.'));
    if (v.count() == 3) {
        int runtimeVersion = v.at(0).toInt() << 16 | v.at(1).toInt() << 8 | v.at(2).toInt();
        int version = major << 16 | minor << 8 | bugfix;
        return version <= runtimeVersion;
    }
    return false;
}

// ...

const bool qt48 = Utils::runtimeQtAtLeast(4,8,0);
QmlApplicationViewer viewer;
if (qt48) {
    viewer.setOrientation(QmlApplicationViewer::ScreenOrientationLockLandscape);
    viewer.setResizeMode(QDeclarativeView::SizeRootObjectToView);
}

viewer.setMainQmlFile(QLatin1String("qml/nupagadi/GameArea.qml"));

if (!qt48) {
    viewer.setOrientation(QmlApplicationViewer::ScreenOrientationLockLandscape);
    viewer.setResizeMode(QDeclarativeView::SizeRootObjectToView);
}
This kind of incompatibility between minor versions of the Qt framework is mind boggling. It makes me think what else did Nokia screw up in Qt 4.8.0 for Symbian and what will they screw up in the next firmware updates. One thing is sure: I'll have a lot of blogging material.

Wednesday, March 28, 2012

QML error handling revisited

After releasing Nu, Pogodi! I learned the hard way that checking the QML runtime errors might be a good idea. For that particular application, simply checking the errors from QDeclarativeView after setting the main qml file was enough, because everything in qml file was statically declared. But what if you use QML Loader element, either explicitly or through some other qml element like PageStack from Qt Components, and something goes wrong?

Well, if you don't improve the error handling code, your application will silently fail in some places, which probably won't make the users happy. I didn't wanted to repeat the Nu, Pogodi! screw up when releasing Word Judge, so I've created a better error handling solution. First part is an error handler class:
// ----------------------------------------
//  qmlerrorhandler.h
// ----------------------------------------

class QmlErrorHandler : public QObject
{
    Q_OBJECT
public:
    explicit QmlErrorHandler(QDeclarativeView &view, QObject *parent = 0);
    bool errorOccured() const;

private slots:
    void handleQmlStatusChange(QDeclarativeView::Status status);
    void handleQmlErrors(const QList<QDeclarativeError>& qmlErrors);

private:
    QDeclarativeView &mView;
    bool mErrorOccured;

};

// ----------------------------------------
//  qmlerrorhandler.cpp
// ----------------------------------------

QmlErrorHandler::QmlErrorHandler(QDeclarativeView &view, QObject *parent) :
    QObject(parent),
    mView(view),
    mErrorOccured(false)
{
    connect(&view, SIGNAL(statusChanged(QDeclarativeView::Status)), SLOT(handleQmlStatusChange(QDeclarativeView::Status)));
    connect(view.engine(), SIGNAL(warnings(QList<QDeclarativeError>)), SLOT(handleQmlErrors(QList<QDeclarativeError>)));
}

void QmlErrorHandler::handleQmlStatusChange(QDeclarativeView::Status status)
{
    if (status == QDeclarativeView::Error) {
        handleQmlErrors(mView.errors());
    }
}

void QmlErrorHandler::handleQmlErrors(const QList<QDeclarativeError>& qmlErrors)
{
    QStringList errors;
    foreach (const QDeclarativeError& error, qmlErrors) {
        // Special case for bug in QtComponents 1.1
        // https://bugreports.qt-project.org/browse/QTCOMPONENTS-1217
        if (error.url().toString().endsWith("PageStackWindow.qml") && error.line() == 70)
            continue;

        errors.append(error.toString());
    }

    if (errors.isEmpty())
        return;

    mErrorOccured = true;

    QMessageBox msgBox;
    msgBox.setText("Uh oh, something went terribly wrong!");
    msgBox.setInformativeText("We're sorry, but it seems there are some problems "
                              "with running our application on your phone. Please "
                              "send us the following information to help us resolve "
                              "this issue:\n\n") +
                              errors.join("\n"));
    msgBox.exec();
    qApp->exit(-1);
}

bool QmlErrorHandler::errorOccured() const
{
    return mErrorOccured;
}
And here's how I use it in my applications:
int main(int argc, char *argv[])
{
    QScopedPointer<QApplication> app(createApplication(argc, argv));

    QScopedPointer<QmlApplicationViewer> viewer(QmlApplicationViewer::create());
    QmlErrorHandler errorHandler(*viewer);
    viewer->setMainQmlFile(QLatin1String("main.qml"));
    viewer->showExpanded();

    if (!errorHandler.errorOccured()) {
        return app->exec();
    } else {
        return -1;
    }
}
Basically we need to catch the runtime errors, which are emitted from QDeclarativeEngine in signal named for some unfathomable reason "warnings". Checking the errorOccured() in main() is ugly, but the qApp->exit() doesn't work until the event loop in main is started and that's the first thing which came to my mind. Please leave a comment if you know a simpler solution.

Note the lines 46-49 in QmlErrorHandler: we're catching all warnings and the qt components are not completely free of them. I had to add a special case to prevent triggering the handler on every orientation change. If you stumble upon some other errors that should be ignored, please let me know.

Wednesday, February 1, 2012

Handling QML errors 101

Qt SDK comes with great documentation and examples, but there is hardly any information about handling QML errors. All examples assume that nothing can go wrong during runtime, which is a bit naive approach. Even if your app is completely bug free, the users will find a way to shoot themselves in the foot. They will install your app and then uninstall key plugins ignoring all warnings from system. They will install your app on SD card and then delete some files for some unfathomable reason. And then they will blame you.

Let's consider what happens if you have an error in your main QML file? User sees the black screen, gets pissed off, uninstalls your app and gives it a negative review in Nokia store. Not good. Let's try something different:

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);   
    QDeclarativeView canvas;

    canvas.setSource(QLatin1String("main.qml"));
    if (!canvas.errors().empty()) {
        // handle errors
        QMessageBox msgBox;
        msgBox.setText("Uh oh, something went terribly wrong!");
        msgBox.setInformativeText(
            "We're sorry, but it seems there "
            "are some problems with running "
            "our application on your phone.");
        msgBox.exec();

        return -1;
    }

    // optionally set the screen orientation
    // call show/showFullscreen/showMaximized

    return app.exec();
}

Few notes about the snippet above:

  • If you're using the QML Application template from the recent version of Qt Creator you probably have a generated QmlApplicationViewer class instead of raw QDeclarativeView, but the general idea stays the same.
  • Error handling section above is only a stub. You may ask user to reinstall the application. You may display more information about the error along with some contact information. If your application uses network connection, you should probably ask user if he wants to send an error report.
  • If you use the QMessageBox or other modal dialog and lock the screen orientation, you mustn't lock the orientation before showing the dialog. In current version of Qt (4.7.4) there is a bug and in case of forced orientation change only dialog buttons are displayed.


Sunday, January 22, 2012

How (not to) test your QML application for Symbian

First of all by QML I do not mean this, I mean this: a UI module of Qt, the cross-platform framework. The gals and guys at Nokia figured out that modern user interface cannot be fully described by static layout in a XML file. Microsoft figured out that too, but they chickened out and only extended XML a bit and added an 'a' to file extension to make it look like something new. Nokians took a step further and created new language for declarative UI based on JavaScript called QML.

The QML UI components can be defined in two ways: it can be a QML file composed of other components (for instance that's the usual way to define the main UI file) or it can be a C++ extension. Both ways can be used together to create a plugin, which can be imported to your project.

And at last we reach the intended topic of this post: testing. What happens if some QML file defining a component is missing? What happens if the whole plugin is missing or the version of this plugin is lower than the one required by application? QML files are interpreted during runtime, so of course you get the runtime error. In the best scenario it limits the functionality of your app, in the worst case it renders it completely unusable.

But hey, you can catch most of those errors simply by clicking through your application, right? Not exactly, doing so only tells you that in works on one particular device. You might have some plugins already installed, but not included in application's package and your app will work only on the devices which happen to have those plugins, which is not very likely.


That's exactly the error I made when I published the first version of "Nu, Pogodi!". I submitted for Q&A process an application with dependencies to Qt Components 1.1, build with the latest Qt SDK. I've tested it thoroughly on some devices I had access to and via Remote Device Access service (which BTW rocks; I wish there was a similar service for Android) and everything worked fine. The application was rejected by Q&A, because at the end of 2011 there was some technical issues with Nokia Store and latest Qt and I was told to rebuild my application with old SDK, which included only Qt Components 1.0. I've tested my game again and everything worked so I published it to Nokia Store. Few days later I received first reviews - all negative, along the lines "doesn't work, beware".

Qt Smart Installer partially prevents those errors, but you still might shoot yourself in the foot in some cases. My game had dependencies to Qt Components 1.1, but the pkg file declared dependency to version 1.0, because it was created with old SDK. When my customers installed the game, the smart installer ensured only that version 1.0 is installed, but my game needed newer version and failed during runtime. I didn't caught this during testing, because all of my devices had latest Qt Components installed.

That was the "How not to test your QML application" part, now let's get to solution. It's really simple: downgrade all the stuff needed by your application to versions defined in pkg file. To check the current versions of Qt libraries and plugins I recommend using an excellent QtInfo tool. To downgrade Qt you need the sis files distributed with old Qt SDK versions.

This simple steps should ensure that your application will work properly on all supported devices. Nevertheless, you should prepare for failure and handle all runtime errors in a user friendly way. But that's the topic for another post...

Friday, January 20, 2012

Introducing: Nu, Pogodi!



"Nu, pogodi!" is the first application I've published in Nokia Store (or any other app store, in fact). It's a remake of a classic handheld console game I used to play in my childhood: you had to place a wolf with a basket under one of four roosts to catch the eggs rolling from them. Despite the extreme simplicity and the obvious flaw in the game plot (namely: what the hell does the wolf need the eggs for?) the game was quite addictive and I spent many hours listening to the hypnotizing ticking of the falling eggs (if you played this game you know what I'm talking about).

The handheld console I based my game on is actually a Russian clone of "Egg" game from Nintendo's "Game & Watch" game series. The main difference is the graphics - instead of fox and hen the Russian clone featured the Wolf and the Hare from a "Nu, pogodi!" cartoon - hence the name of the game.


At the beginning of 2011 I wanted to check out the Qt Quick, which was advertised by Nokia as the best thing since sliced bread. I never liked the go-through-a-boring-tutorials way of learning new things, so I started writing UI for a simple game instead. Few weeks later Nokia announced Qt Quick Competition - an event promoting Qt Quick introduced in Qt 4.7. I've entered the competition with the early version of my game under the name "Nu, Pagadi!" (which is, as I learned later, incorrect - apparently in Russian sometimes you write an 'o', but pronounce it as 'a'), which didn't won me anything, but at least I had a motivation to work on the game. In accordance with the competition rules I've published my code under OSS license and forgot about the whole deal.

In November 2011 I've stumbled upon a "Soviet Eggs" game in Nokia Store. It seems that some company forked my competition entry, added a splash screen and the menu and charged 2 euros for it. I've watched the gameplay videos on YouTube, and thought I can do much better version than them. I polished my game, added sound effects, better looking menu and new game mode which resembles the gameplay of original game much more than the one included in the competition entry. All those changes took me about one week worth of evenings and one weekend. Subsequentially I've screwed up testing, published to Nokia Store a game which silently crashed on 90% of the phones, fixed the problem, and then I screwed up again, this time when publishing the update.

Despite the initial issues the game reached top 10 bestseller list in two weeks and stayed there ever since. Try it yourself!