Cross-platform is cool

    This post is participating in the competition “ Smart Phones for Smart Posts ”.

    It's no secret that mobile games are very popular today. Every developer, even a beginner, has the opportunity to write one of these games. Often the question arises with the choice of platform. Of course, I want the game to be everywhere immediately: on iOS and Android, on WP7 and MeeGo, on the desktop and in the browser. And so that all this can be easily implemented using free tools.



    In this article I will tell you how to make the main part of the code platform independent, and for the rest use convenient development tools for each specific platform.

    The goal of the game shown in the figure above is to catch the apple while it flies down. Over time, the number of apples increases, and not to miss them becomes more difficult. Apples fall at an arbitrary angle, rotating and realistically bouncing off the borders thanks to the Box2D physics engine . The game will run on Android, Qt-enabled platforms (Symbian, Maemo, MeeGo, Windows, Linux, Mac OS X) and in the Google Chrome browser.

    Selection of convenient tools



    Since the main part of the code I will write in pure C ++ (why, read at the end of the article), any IDE will do for this. I will choose Qt Creator, although nothing prevents me from using Microsoft Visual Studio or Eclipse, for example. For the Android platform, I will focus on the libgdx library. With its help, you can easily draw textures, play sounds and do other necessary things. As a tool for developing a game on the desktop, I will take Qt. I have been familiar with this library for a long time, and it does not cease to please me. When using Qt, I will also get a nice bonus in the form of support for the mobile operating systems Symbian, Maemo and MeeGo. Also specifically for this article I am using HTML5, javascript and Google Native Client








    I will make the game run in the Google Chrome browser. I will use HTML5 Canvas and Audio, and you will see how easy and simple it is.


    The implementation of logic is not complicated, so I will not write about it (anyone can take a look at the code ). Instead, I will concentrate on how to make the game work on all operating systems.

    We abstract from the final platform


    As I said, the bulk of the code will be common to all platforms. Let's call it the "engine." I will need to solve two problems. The first is to call engine methods on each platform:

    For this, the engine will provide the following interface to platforms:
    class Application
    {
    public:
        Application();
        ~Application();
        void render();
        void touch(int x, int y);
        //...
    };
    

    Calls to drawing and input handlers on various platforms will call methods from the Application class, for example, when using Qt, it will look like this:
    void QtPlatrom::paintEvent(QPaintEvent *)
    {
        QPainter painter(this);
        m_painter = &painter;
        m_app->render();
    }
    void QtPlatrom::mousePressEvent(QMouseEvent *e)
    {
        QWidget::mousePressEvent(e);
        m_app->touch(e->x(), height() - e->y());
    }
    

    It will be a bit more complicated on Android, because you need to get into Java from C ++:
    private native void renderNative();
    private native void touchNative(int x, int y);
    static {
        System.loadLibrary("fruitclick");
    }
    public void render() {
        renderNative();
    }
    public boolean touchDown(int x, int y, int pointer, int button) {
        touchNative(x, Gdx.graphics.getHeight() - y);
        return false;
    }
    

    After that, the corresponding methods are called in C ++:
    void Java_com_fruitclick_Application_renderNative(JNIEnv* env, jobject thiz)
    {
        g_app->render();
    }
    void Java_com_fruitclick_Application_touchNative(JNIEnv* env, jobject thiz, jint x, jint y)
    {
        g_app->touch(x, y);
    }
    

    When using the Native Client in a browser from javascript, you cannot directly access C ++, instead you need to send messages to the module, for example, the lines:
    function onTouch(e) {
        var coords = getCursorPosition(e);
        var x = coords[0];
        var y = canvas.height - coords[1];
        var message = "touch " + x.toString() + " " + y.toString();
        FruitclickModule.postMessage(message);
    }
    function simulate() {
        FruitclickModule.postMessage('render');
        setTimeout("simulate()", 16);
    }
    

    In C ++, messages are parsed, and one method or another is called depending on the content:
    void NaclPlatform::HandleMessage(const pp::Var& var)
    {
        if (!var.is_string())
            return;
        std::stringstream stream(var.AsString());
        std::string type;
        stream >> type;
        if (type == "render")
        {
            m_app.render();
        }
        else if (type == "touch")
        {
            int x;
            int y;
            stream >> x >> y;
            m_app.touch(x, y);
        }
    }
    

    As a result, the engine doesn’t matter which platform the call was from, it abstracted from this. But he knows that there was a touch of the screen at the point (x, y) or the time has come for processing physics and displaying images on the screen.

    Reverse interaction


    The second task is the reverse interaction of the engine with the platform:

    This is necessary for the engine to command when to display images and text on the screen, play sound, vibrate. To do this, all platforms must implement a common interface. Call this Platform interface:
    class Platform
    {
    public:
        enum Texture
        {
            APPLE = 0,
            BACKGROUND
        };
        static void draw(Texture id, float x, float y, float angle = 0);
        static void drawText(const char* text, float x, float y);
        enum Sound
        {
            CRUNCH = 0,
            CRASH
        };
        static void playSound(Sound id);
        static void vibrate();
        //...
    };
    

    At the engine level, I don’t get attached to any particular platform, I don’t upload pictures or audio files, instead I use numerical identifiers. When I want to display an image on the screen, or play a sound, I do the following:

    
    Platform::draw(Platform::BACKGROUND, screenWidth/2, screenHeight/2);
    Platform::playSound(Platform::CRASH);
    

    Thus, the engine is abstracted from the details of the implementation of various operations on each platform. I

    give a class diagram for clarity: Is it difficult to do all this? You will make sure not. Time, of course, will have to be spent, but in most cases it can be neglected in comparison with the time spent on programming the application logic. I will give the code for the Android, Qt and Native Client platforms for each necessary operation:
    Drawing an image, Android (libgdx):
    public void draw(int id, float x, float y, float angle) {
        TextureRegion region = null;
        switch (id) {
            case BACKGROUND:
                region = background;
                break;
            case APPLE:
                region = apple;
                break;
            default:
                break;
            }
            float w = region.getRegionWidth();
            float h = region.getRegionHeight();
            batch.draw(region, x - w/2, y - h/2, w/2, h/2, w, h, 1, 1, angle);
    }
    

    Drawing image, Qt:
    void QtPlatrom::drawImpl(Texture id, float x, float y, float angle)
    {
        QPixmap* pixmap = NULL;
        switch(id)
        {
        case FruitClick::Platform::APPLE:
            pixmap = &m_apple;
            break;
        case FruitClick::Platform::BACKGROUND:
            pixmap = &m_background;
            break;
        default:
            break;
        }
        y = height() - y;
        m_painter->translate(x, y);
        m_painter->rotate(-angle);
        int w = pixmap->width();
        int h = pixmap->height();
        m_painter->drawPixmap(-w/2, -h/2, w, h, *pixmap);
        m_painter->rotate(angle);
        m_painter->translate(-x, -y);
    }
    

    Drawing an image, javascript (HTML5 Canvas):
    function draw(id, x, y, angle) {
        y = canvas.height - y;
        var image = null;
        switch(id) {
        case 0:
            image = apple;
            break;
        case 1:
            image = background;
            break;
        }
        context.translate(x, y);
        context.rotate(-angle);
        context.drawImage(image, -image.width/2, -image.height/2);
        context.rotate(angle);
        context.translate(-x, -y);
    }
    

    Drawing text, Android (libgdx):
    
    public void drawText(String text, float x, float y) {
        font.draw(batch, text, x, y);
    }
    

    Drawing text, Qt:
    void QtPlatrom::drawTextImpl(const char *text, float x, float y)
    {
        y = height() - y;
        m_painter->drawText(x, y, text);
    }
    

    Drawing text, javascript (HTML5 Canvas):
    function drawText(text, x, y) {
        y = canvas.height - y;
        context.fillText(text, x, y);
    }
    

    Sound Play, Android (libgdx):
    public void playSound(int id) {
        switch (id) {
        case CRUNCH:
            crunch.play();
            break;
        case CRASH:
            crash.play();
            break;
        }
    }
    

    Sound playback, Qt:
    void QtPlatrom::playSoundImpl(Sound id) {
        switch (id)
        {
        case FruitClick::Platform::CRUNCH:
            m_crunch.play();
            break;
        case FruitClick::Platform::CRASH:
            m_crash.play();
            break;
        default:
            break;
        }
    }
    

    Sound playback, javascript (HTML5 Audio):
    function playSound(id) {
        var sound = null;
        switch(id) {
        case 0:
            sound = crunch;
            break;
        case 1:
            sound = crash;
            break;
        }
        sound.currentTime = 0;
        sound.play();
    }
    

    Vibration, Android (libgdx):
    
    void vibrate() {
        Gdx.input.vibrate(100);
    }
    

    When implementing for Android, you will have to tinker a bit with calling java code from C ++ - once get the ID of the necessary java methods:
    void setupEnv(JNIEnv* env, jobject thiz)
    {
        g_env = env;
        g_activity = thiz;
        g_activityClass = env->GetObjectClass(thiz);
        drawID = env->GetMethodID(g_activityClass, "draw", "(IFFF)V");
        drawTextID = env->GetMethodID(g_activityClass, "drawText", "(Ljava/lang/String;FF)V");
        playSoundID = env->GetMethodID(g_activityClass, "playSound", "(I)V");
    }
    

    and then call them:
    void AndroidPlatform::drawImpl(FruitClick::Platform::Texture id, float x, float y, float angle)
    {
        g_env->CallVoidMethod(g_activity, drawID, id, x, y, angle);
    }
    void AndroidPlatform::drawTextImpl(const char* text, float x, float y)
    {
        jstring javaString = g_env->NewStringUTF(text);
        g_env->CallVoidMethod(g_activity, drawTextID, javaString, x, y);
    }
    void AndroidPlatform::playSoundImpl(FruitClick::Platform::Sound id)
    {
        g_env->CallVoidMethod(g_activity, playSoundID, id);
    }
    

    There is a nontrivial situation with the Native Client - you need to send messages from C ++ code to javascript:
    const char* sep = "|";
    void NaclPlatform::drawImpl(FruitClick::Platform::Texture id, float x, float y, float angle)
    {
        std::stringstream stream;
        stream << "draw" << sep << id << sep << x << sep << y << sep << angle;
        PostMessage(pp::Var(stream.str()));
    }
    void NaclPlatform::drawTextImpl(const char* text, float x, float y)
    {
        std::stringstream stream;
        stream << "drawText" << sep << text << sep << x << sep << y;
        PostMessage(pp::Var(stream.str()));
    }
    void NaclPlatform::playSoundImpl(FruitClick::Platform::Sound id)
    {
        std::stringstream stream;
        stream << "playSound" << sep << id;
        PostMessage(pp::Var(stream.str()));
    }
    

    And in javascript, these messages are parsed:
    function handleMessage(message_event) {
        params = message_event.data.split("|");
        if (params[0] == "draw") {
            draw(parseInt(params[1]),
                 parseInt(params[2]),
                 parseInt(params[3]),
                 parseFloat(params[4]));
        }
        else if (params[0] == "drawText") {
            drawText(params[1], parseInt(params[2]), parseInt(params[3]));
        }
        else if (params[0] == "playSound") {
            playSound(parseInt(params[1]));
        }
    }
    


    Result


    This simple game is called Catch the Bullseye. I suggest launching and trying to hold on for a couple of minutes, at first I didn’t succeed:
    - Native Client version (make sure you have the latest version of Google Chrome browser, and Native Client is included in about: plugins and about: flags). The size of the nexe executable file is 4.2 MB for 32-bit systems and 4.9 MB for 64-bit ones; you will have to wait a little while connecting slowly;
    - Windows version - for those who do not like Google Chrome.

    Video:

    The game runs great on an Android emulator and my LG Optimus. The same situation with Qt Simulator (screenshot from Nokia N9 at the very beginning of the topic).

    The code

    The code can be taken here , I think it can be useful to someone, especially the sections that are responsible for the combination of Java and C ++, javascript and C ++ (if you have any questions about this - ask, do not be shy, I will answer with pleasure).

    Why all this?


    Many of you will think, why write a bike? If there is Marmalade or Unity , for example. Yes, but they cost money, and why such heavyweights for a simple 2D toy? Some also say that Qt starts up on Android and iOS, but in fact on Android it doesn’t start up very well, without sound and OpenGL, and on iOS in general, only videos on YouTube. I really like Qt, and I hope that in the near future applications for iOS and Android can be written as easy as they are now for MeeGo, but for now it’s better to use other tools for these platforms.

    Benefits

    Using the approach described in this article, you are not tied to the platform, you can use the tools you want, and in the future it is easy to change them. On the desktop - Qt or GTK, on ​​Android - libgdx or AndEngine, on iOS - cocos2d, the choice is yours. You can completely abandon the engines using the API provided by the platform. Most of the time you can write and debug code in your favorite IDE in the great and powerful C ++.

    disadvantages

    Of course, there are also disadvantages, for example, you cannot use ready-made UI components - you will need to implement them in C ++. Or to transfer the UI part of the application to each platform. You will also have to get to know each platform closely, but as practice shows, you can never completely get away from this acquaintance.

    To be continued?


    Do you still think a game for mobile platforms in C ++ is a bad idea? Look at the Angry Birds . Listen to the wonderful performance of Emblem of Sutter. Think about the fact that C ++ support is almost everywhere, and that after the new standard C ++ 11 is implemented in all NDKs, it will be even better.

    Also popular now: