Кроссплатформенность — это круто

    Этот пост участвует в конкурсе „Умные телефоны за умные посты“.

    Ни для кого не секрет, что сегодня мобильные игры очень популярны. Возможность написать одну из таких игр есть у каждого разработчика, даже начинающего. Часто возникает вопрос с выбором платформы. Конечно, хочется, чтобы игра была сразу везде: на iOS и Android, на WP7 и MeeGo, на десктопе и в браузере. И чтобы все это можно было лекго реализовать с помощью бесплатных инструментов.



    В этой статье я расскажу вам, как сделать основную часть кода платформонезависимой, а для остального использовать удобные средства разработки для каждой конкретной платформы.

    Цель игры, изображенной на рисунке выше — успеть попасть по яблоку, пока оно летит вниз. Со временем количество яблок увеличивается, и не пропускать их становится все сложнее. Яблоки падают под произвольным углом, вращаясь и реалистично отскакивая от границ благодаря физическому движку Box2D. Игра будет запускаться на Android, платформах с поддержкой Qt (Symbian, Maemo, MeeGo, Windows, Linux, Mac OS X) и в браузере Google Chrome.

    Выбор удобных инструментов



    Так как основную часть кода я буду писать на чистом С++ (почему, читайте в конце статьи), IDE для этого подойдет любая. Я выберу Qt Creator, хотя ничего не мешает мне использовать Microsoft Visual Studio или Eclipse, например.


    Для платформы Android я остановлюсь на библиотеке libgdx. С ее помощью легко можно рисовать текстуры, проигрывать звуки и делать другие необходимые вещи.


    В качестве инструмента для разработки игры на десктопе я возьму Qt. Я давно знаком с этой библиотекой, и она не перестает меня радовать. При использовании Qt я также получу приятный бонус в виде поддержки мобильных операционных систем Symbian, Maemo и MeeGo.


    Также специально для этой статьи я с помощью HTML5, javascript и Google Native Client сделаю так, чтобы игра запускалась в браузере Google Chrome. Я буду использовать HTML5 Canvas и Audio, и вы увидите, насколько это легко и просто.


    Реализация логики не сложная, поэтому я не буду писать о ней (желающие могут взглянуть на код). Вместо этого я сконцентрируюсь на том, как заставить игру работать на всех операционных системах.

    Абстрагируемся от конечной платформы


    Как я уже говорил, основная часть кода будет общей для всех платформ. Назовем ее «движок». Мне нужно будет решить две задачи. Первая — вызов методов движка на каждой платформе:

    Для этого движок предоставит платформам следующий интерфейс:
    class Application
    {
    public:
        Application();
        ~Application();
    
        void render();
        void touch(int x, int y);
        //...
    };
    

    Вызовы обработчиков рисования и ввода на различных платформах будут вызывать методы из класса Application, например, при использовании Qt это будет выглядеть так:
    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());
    }
    

    На Android выйдет немного сложнее, потому что из Java нужно попасть в 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;
    }
    

    После этого в 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);
    }
    

    При использовании Native Client в браузере из javascript нельзя напрямую обращаться к С++, вместо этого надо отправлять сообщения модулю, например, строки:
    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);
    }
    

    В С++ сообщения анализируются, и в зависимости от содержания вызывается тот или иной метод:
    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);
        }
    }
    

    В итоге движку не важно, из какой платформы был вызов, он абстрагировался от этого. Но он знает, что произошло касание экрана в точке (x, y) или пришло время для обработки физики и вывода изображений на экран.

    Обратное взаимодействие


    Вторая задача — обратное взаимодействие движка с платформой:

    Это нужно для того, чтобы движок командовал, когда выводить изображения и текст на экран, проигрывать звук, вибрировать. Для этого все платформы должны реализовать общий интерфейс. Назовем этот интерфейс Platform:
    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();
        //...
    };
    

    На уровне движка я не привязываюсь ни к какой конкретной платформе, я не загружаю картинки или аудио файлы, вместо этого я использую числовые идентификаторы. Когда я хочу вывести изображение на экран, или проиграть звук, я делаю следующее:

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

    Таким образом движок абстрагируется от деталей реализации различных операций на каждой платформе. Привожу для наглядности диаграмму классов:

    Сложно ли все это сделать? Вы убедитесь в том, что нет. Время, конечно, придется потратить, но в большинстве случаев им можно пренебречь в сравнении со временем, потраченным на программирование логики приложения. Я приведу код для платформ Android, Qt и Native Client для каждой необходимой операции:
    Рисование изображения, 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);
    }
    

    Рисование изображения, 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);
    }
    

    Рисование изображения, 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);
    }
    

    Рисование текста, Android (libgdx):
    
    public void drawText(String text, float x, float y) {
        font.draw(batch, text, x, y);
    }
    
    

    Рисование текста, Qt:
    void QtPlatrom::drawTextImpl(const char *text, float x, float y)
    {
        y = height() - y;
        m_painter->drawText(x, y, text);
    }
    

    Рисование текста, javascript (HTML5 Canvas):
    function drawText(text, x, y) {
        y = canvas.height - y;
        context.fillText(text, x, y);
    }
    

    Проигрывание звука, Android (libgdx):
    public void playSound(int id) {
        switch (id) {
        case CRUNCH:
            crunch.play();
            break;
        case CRASH:
            crash.play();
            break;
        }
    }
    

    Проигрывание звука, 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;
        }
    }
    

    Проигрывание звука, 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();
    }
    

    Вибрация, Android(libgdx):
    
    void vibrate() {
        Gdx.input.vibrate(100);
    }
    
    

    При реализации для Android придется немного повозиться с вызовом java кода из C++ — один раз получить ID нужных java методов:
    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");
    }
    

    и потом вызывать их:
    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);
    }
    

    Нетривиальная ситуация и с Native Client — нужно отправлять сообщения из С++ кода в 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()));
    }
    

    И в javascript эти сообщения парсить:
    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]));
        }
    }
    


    Результат


    Эта простая игра называется «Поймай яблочко». Предлагаю запустить и попробовать продержаться пару минут, у меня вначале не получалось:
    Native Client версия (убедитесь, что у вас последняя версия браузера Google Chrome, и Native Client включен в about:plugins и about:flags). Размер исполняемого файла nexe — 4.2Мб для 32-битных систем и 4.9Мб для 64-битных, при медленном соединении придется немного подождать;
    Windows версия — для тех, кто не любит Google Chrome.

    Видео:

    Игра прекрасно запускается на Android эмуляторе и моем LG Optimus. Та же ситуация с Qt Simulator (скриншот с Nokia N9 в самом начале темы).

    Код

    Код можно взять тут, я думаю, он может пригодиться кому-нибудь, особенно участки, которые отвечают за связку Java и C++, javascript и C++ (если по этому поводу у вас возникнут вопросы — задавайте, не стесняйтесь, с удовольствием отвечу).

    Зачем все это?


    Многие из вас подумают, зачем писать велосипед? Если есть Marmalade или Unity, например. Есть, но они стоят денег, да и зачем такие тяжеловесы для простой 2D игрушки? Некоторые говорят также, что Qt заводится на Android и iOS, но на самом деле на Android не очень так заводится, без звука и OpenGL, а на iOS так вообще, только ролики на YouTube. Мне очень нравится Qt, и я надеюсь, что в недалеком будущем приложения для iOS и Android можно будет писать так же просто, как сейчас для MeeGo, но пока лучше пользоваться другими инструментами для этих платформ.

    Преимущества

    Используя подход, описанный в этой статье, вы не привязаны к платформе, вы можете использовать те инструменты, которые хотите, а в последующем легко их менять. На десктопе — Qt или GTK, на Android — libgdx или AndEngine, на iOS — cocos2d, выбор за вами. Можете вовсе отказаться от движков, используя API, предоставляемое платформой. Большую часть времени вы можете писать и отлаживать код в вашей любимой IDE на великом и могучем C++.

    Недостатки

    Недостатки, конечно, тоже есть, например, вы не сможете пользоваться готовыми UI компонентами — вам нужно будет реализовать их на C++. Либо выносить UI часть приложения в каждую платформу. Также вам обязательно придется тесно познакомиться с каждой платформой, но как показывает практика, полностью уйти от этого знакомства никогда не удается.

    Продолжение следует?


    Вы все еще думаете, что игра для мобильных платформ на C++ — это плохая идея? Посмотрите на Angry Birds. Послушайте замечательное выступление Герба Саттера. Подумайте о том, что поддержка C++ есть почти везде, и что после того, как новый стандарт C++11 реализуют во всех NDK, будет еще лучше.
    Поделиться публикацией

    Комментарии 69

      +7
      Кроссплатформенность была бы на много круче, если максимум что нужно было сделать это выбрать среду, для которой компилировать приложение. Но до таких технологий пока вроде как не дошли. Спасибо за статью было интересно.
        +6
        В Qt дошли. Причём давно. Можно просто из среды разработки выбирать для какой платформы собирать и тут же запускать в эмуляторе или на устройстве. Гляньте например сюда: habrahabr.ru/blogs/qt_software/120966/
          +2
          Marmalade тоже до этого дошел, но он платный, это да.
            0
            По моему для разработчика 300-400$ в год это не такая и большая сумма.
            Хотя если вы преследуете целью не результат, а процесс, то вылепливание велосипеда — то что доктор прописал.
            +1
            Никто не упомянул, восполню.
            Отчего б и не www.haxenme.org/?
              0
              Adobe AIR Mobile
            • НЛО прилетело и опубликовало эту надпись здесь
                0
                При использовании самого популярного и удобного решения, Necessitas — не работает. Вот issue, будем надеяться, что в недалеком будущем пофиксят. Жаль, что у главного разработчика нет мотивации реализовать это самому, возможно найдется кто-то другой.
                  0
                  На самом деле там есть экспериментальные версии. И Томас — не главный. Главного как такового нет, но негласно главным считается все же Богдан (который начал порт). Томас присоединился чуть позже и в основном ОГЛем и занимается.
                    –1
                    Там вроде есть egl full screen версия, видел вполне рабочую и быструю демку на ведроидных планшетах. Но всё равно я не слишком верю, что возможно с кривой архитектурой Андроида получить порт production качества.
                      0
                      Да, я про нее и говорю.
                      Начиная с 2.3 стало попроще с архитектурой вроде для реализации целей, необходимых нецесситасу. Но полностью переход на 2.3+ сейчас невозможен из за большого количества аппаратов на 2.2
                        0
                        Мне так кажется, что когда проект созреет уже по настоящему, в основном будет мейнстримом 4ая версия.
                          0
                          да, скорее всего
                0
                Всё круто, но получается, что искусственно ограничиваем себя только Chrome'ом. Ежели хотим обрадовать нашим творением всех пользователей и-нета, то натыкаемся на проблемы с другими браузерами.
                  +5
                  К сожалению, пока только Google Chrome имеет такую прогрессивную технологию, как Native Client.
                    0
                    Сильверлайт последний вроде бы через какие-то трюки позволяет работать с нативным кодом.
                      –1
                      Мелкомягкие уже пообещали закрыть сильвер, поэтому не вариант.
                        +1
                        Саппорт 5-ой версии продлён до 2021 года. Мало?
                          +1
                          Саппорт одно, развитие другое. Мне казалось что его не будут развивать, не?
                    0
                    А где версии под все заявленные платформы? Почему версия для Хрома такого большого размера и брожит?
                      +2
                      Версии под все заявленные платформы можете собрать сами, вот исходники. Игра имеет размер 320х480, разве это много? Возможно частота перерисовки HTML5 Canvas примерно в 60 fps на вашем оборудовании создает дрожание, я у себя не замечал.
                        0
                        > Игра имеет размер 320х480, разве это много?

                        4.2Мб для 32-битных систем и 4.9Мб для 64-битных.
                          0
                          Я думаю, что это из-за того, что в проект включены все исходники библиотеки Box2D и статически прилинкована стандартная библиотека.
                            0
                            в NaCl всё линкуется статически.
                        0
                        При попытке получения кроссплатформенности на мобильных устройствах как ни крути всегда получаешь один минус: приходится забивать на всяческие интерфейс-гайды платформ, а они у тех же iOS/Android/WinPhone очень сильно разнятся
                          +2
                          Я с вами согласен, но подумайте вот о чем: Angry birds следуют этим гайдам? Это им сильно мешает?
                          В играх интерфейс-гайды не столь важны, это больше касается других программ.
                          +1
                          отличная статья, очень понравилась. А вот вопрос, можно вместо Qt за основу взять Objective-C, все равно GUI у всех свой будет?
                            +1
                            Основа — это чистый С++, без Qt. Я использовал Qt для того, чтобы игра работала на десктопе и телефонах Nokia. Objective-C можно использовать для того, чтобы портировать игру на iOS. Причем весь существующий С++-код не нужно будет переписывать на Objective-C, в этом большой плюс.
                            +9
                            Как-то один человек, сведущий в HTML5 рассказывал, что по его опыту попытка сделать приложение под все мобильные платформы на HTML5 для заказчика заканчивается разработкой отдельных приложений под каждую платформу на радном языке/платформе.
                              –1
                              Если под iOs приложение ещё кое-как работает, то под Андроид — хочется плакать…
                            • НЛО прилетело и опубликовало эту надпись здесь
                                +2
                                Цены у MonoTouch и MonoDroid были бы еще поадекватнее…
                                  0
                                  Вполне адекватные деньги. Это рабочий инструмент, с которым вы собираетесь заработывать гораздо большие деньги.
                                    0
                                    Применительно к gamedev? Не думаю, что C# + сопутствующий набор проблем(биндинги, «движок» и gc) стоят таких денег :)
                                  +1
                                  a) Закрытое
                                  б) Кривое, как моя жизнь.
                                  • НЛО прилетело и опубликовало эту надпись здесь
                                  0
                                  Ещё вариант — Titanium
                                    +2
                                    А про Adobe AIR как всегда забыли?
                                      +1
                                      а он еще жив?
                                        +3
                                        Еще как жив. А с учетом того, что в последней версии добавили возможность подключать нативные библиотеки, будет жить еще очень долго.

                                        Просто со всей этой суматохой вокруг смерти флеша, люди которые находятся вне флеш-технологии не правильно воспринимают информацию.
                                          0
                                          я просто с эйром мало знаком, но мне казалось, что его основное применение сейчас это блекберри. Которая, в свою очередь перешла на qt
                                            +1
                                            Сейчас вполне себе живет на iOS, Android.
                                              0
                                              Linux порт закрыли. И в целом, она не пользуется особым спросом.
                                                0
                                                Насчет линукса, да. Хотя тут вопрос в требованиях, мы сейчас под QNX разрабатываем приложение, и AIR 2.6 вполне хватает.
                                                  +1
                                                  На qnx тоже балом будет править кьют скоро. В поддержке он появился
                                        0
                                        Даешь такую же статью для AIR!
                                          0
                                          Совсем забыл, есть же такая вещь, как Adobe Alchemy. Значит будет еще два кандидата для продолжения, Adobe Flash Player и Adobe AIR!
                                        –1
                                        Я чего то не понял при чем тут конкурс
                                          +3
                                          Хотя данный пост и обогнал меня на конкурсе и мне это очень обидно, я не могу не отметить профессионализм автора и тот факт, что пост полезный и в теме конкурса — получившийся софт работает с использованием Qt и может запускаться на нокиевских телефонах. Так что всё ок, мне кажется.
                                            –2
                                            С формальной точки зрения все ок — Qt упомянут, но в основном идет разбор Android и Nacl особенностей…
                                              +2
                                              Во-первых, это тоже вполне себе подходит под «мобильную разработку», во-вторых, никто в здравом умен не будет писать на Qt приложение «только под нокие», если его столь небольшими усилиями можно выпустить везде.
                                                +3
                                                теми же небольшими силами можно написать приложение на Qt, которое будет работать на симбе, миго, дроиде, будущем блекберри и (в будущем) иосе. Но при этом это будет одно цельное приложение с использованием ОДНОГО инструмента.
                                                  +1
                                                  И код будет куда более прозрачным для редактирования и расширения. Причем на BB и Вебось Qt вполне официально портировали. А QtComponents'ы позволяют делать само приложения по гайдлайнам целевой платформы.
                                                    +1
                                                    Я очень надеюсь, что это будущее наступит скоро. Когда это случится, я смогу отказаться от поддержки кода, специфичного для Android и iOS. Пока, увы, я не могу этого сделать.
                                                      +1
                                                      Так а зачем? В Qt специфичный код для Android и iOS вполне себе может жить внутри соответствующих реализаций QtComponents'ов, а все элементы управления смогут рисоваться нативными методами.
                                                        0
                                                        о да, был бы вот пост про такой подход, было бы клево. Автор, как насчет?
                                                          0
                                                          Не уверен, что полностью понимаю вас. Разработка для Android и iOS с использованием библиотеки Qt — пока что скорее сказка, чем реальность, а QtComponents — часть библиотеки Qt.
                                                            0
                                                            речь идет о создании прослойки из нативных реализаций и разработке верхнего уровня логики приложения на qt
                                                              0
                                                              Можно перефразировать ваш вопрос так: «Автор, как насчет портировать Qt Lighthouse на Android, iOS, Native Client, или изучить и допилить имеющиеся порты в сети и потом со спокойной душой пользоваться QtComponents»?
                                                              Или существует способ использовать QtComponents без портирования Qt на эти платформы?
                                                                0
                                                                э, при чем здесь полноценное портирование? пишете, то чего недостает вам например в андроиде (при желании реквестите мерж в нецесситас) на уровне далвика или ndk (как вам больше нравится) и используете это у себя в проекте на qt
                                                                  0
                                                                  Я вас понял. К сожалению, опыт работы с платформой Android у меня небольшой и ограничивается использованием библиотеки libgdx. То же самое можно сказать про iOS и Native Client. Я не могу назвать себя специалистом по OpenGL, я предпочитаю избегать прямого взаимодействия с ней. Поэтому я не могу позволить себе пойти на такой отвественный шаг, к тому же мной отчасти двигает лень, я свято верю, что есть люди, которые сделают это вместо меня, и надеюсь, что это будет скоро.
                                              +2
                                              еще есть cocos2d-x: iOS, Android, BlackBerry, Bada, Win, Linux.
                                                +4
                                                За статью спасибо очень интересно, а оформление так вообще шикарное!
                                                  +2
                                                  Когда проиграешь, можно кликать по оставшимся яблокам на экране, и очки считаются дальше =)
                                                    +1
                                                    Спасибо за первый зарепорченный баг! И да, теперь вы знаете, как набрать в этой игре больше всех очков :)
                                                    –2
                                                    Написать одинаковое решение под множество платформ, значит сделать на всех одинаково плохо, как правило. И примеров тому множество.
                                                      +1
                                                      Расскажите это энгрибёрдсам, то-то они посмеются.
                                                        0
                                                        Примеры в студию во первых, во вторых многоплатформенный код как правило более чистый в плане API.
                                                          0
                                                          Вот это да. Особенно весело когда схожее апи растягивают на Qt и Java. В одном сигнал-слоты, в другом листенеры. Вроде такие похожие, но такие разные…

                                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                      Самое читаемое