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