Этот пост участвует в конкурсе „Умные телефоны за умные посты“.
Ни для кого не секрет, что сегодня мобильные игры очень популярны. Возможность написать одну из таких игр есть у каждого разработчика, даже начинающего. Часто возникает вопрос с выбором платформы. Конечно, хочется, чтобы игра была сразу везде: на 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, и вы увидите, насколько это легко и просто.
Реализация логики не сложная, поэтому я не буду писать о ней (желающие могут взглянуть на код). Вместо этого я сконцентрируюсь на том, как заставить игру работать на всех операционных системах.
Как я уже говорил, основная часть кода будет общей для всех платформ. Назовем ее «движок». Мне нужно будет решить две задачи. Первая — вызов методов движка на каждой платформе:

Для этого движок предоставит платформам следующий интерфейс:

Вызовы обработчиков рисования и ввода на различных платформах будут вызывать методы из класса Application, например, при использовании Qt это будет выглядеть так:

На Android выйдет немного сложнее, потому что из Java нужно попасть в C++:

После этого в C++ вызываются соответствующие методы:

При использовании Native Client в браузере из javascript нельзя напрямую обращаться к С++, вместо этого надо отправлять сообщения модулю, например, строки:

В С++ сообщения анализируются, и в зависимости от содержания вы��ывается тот или иной метод:

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

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

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

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

Сложно ли все это сделать? Вы убедитесь в том, что нет. Время, конечно, придется потратить, но в большинстве случаев им можно пренебречь в сравнении со временем, потраченным на программирование логики приложения. Я приведу код для платформ Android, Qt и Native Client для каждой необходимой операции:
Рисование изображения, Android (libgdx):

Рисование изображения, Qt:

Рисование изображения, javascript (HTML5 Canvas):

Рисование текста, Android (libgdx):

Рисование текста, Qt:

Рисование текста, javascript (HTML5 Canvas):

Проигрывание звука, Android (libgdx):

Проигрывание звука, Qt:

Проигрывание звука, javascript (HTML5 Audio):

Вибрация, Android(libgdx):

При реализации для Android придется немного повозиться с вызовом java кода из C++ — один раз получить ID нужных java методов:

и потом вызывать их:

Нетривиальная ситуация и с Native Client — нужно отправлять сообщения из С++ кода в javascript:

И в javascript эти сообщения парсить:

Эта простая игра называется «Поймай яблочко». Предлагаю запустить и попробовать продержаться пару минут, у меня вначале не получалось:
— 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, будет еще лучше.
Ни для кого не секрет, что сегодня мобильные игры очень популярны. Возможность написать одну из таких игр есть у каждого разработчика, даже начинающего. Часто возникает вопрос с выбором платформы. Конечно, хочется, чтобы игра была сразу везде: на 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, будет еще лучше.
