Портирование на Android
В конце данной статьи мною был озвучен план сделать порт под Android. Тут я попытаюсь описать проблемы, с которыми я столкнулся и методы их решения. Сразу хочу оговорится, что опыта работы с Android на данный момент ровно 2 месяца и возможно некоторые решения опасны или даже не приемлемы на данной платформе.
Движок
Движок (хобби) находится в разработке уже 10 лет.
Движок полностью написан на C/C++, до начала портации на Android поддерживал iOS и Windows.
Логика, рендеринг, звук — все на C/C++.
Файловая система
Важная особенность которая сделала портацию на Android очень простой — это файловая система движка.
Псевдо код:
class IStream
{
void setName(string name ) = 0;
open() = 0;
write() = 0;
… = 0;
}
class FileStream : public IStream
{
имплементация через fopen,fread... POSIX API.
}
class DataPackStream : public IStream
{
имплементация для работы с .pak файлами
Core::Meta* m_packFileStreamMeta;; /// m_packFileStreamMeta->Create() IStream* этого типа будет использован для работы непосредственно с .pak файлом
}
Итак: вся работа с файлами в движке основана на интерфейсе IStream, архитектура движка поддерживает фабрику объектов, и её кастомизацию.
Пример кода:
Core::FileStream::Type()->setFactoryMeta(DataPack::DataPackStream::Type());
FileStream* filestream1 = FileStream::Create();
// тоже самое будет и при new FileStream()
FileStream* filestream2 = FileStream::CreateExact();
filestream1 будет типа DataPackStream
filestream2 будет типа FileStream
Ничего нового и все довольно стандартно.
Так как все ресурсы запрепроцесенны в .pak файл, то для Android системы пришлось написать свою реализацию IStream специально для работы с Java.
Инициализация данного ресурса на Java
AssetFileDescriptor RawAssetsDescriptor = this.getApplicationContext()
.getResources().openRawResourceFd(R.raw.data000);
if (RawAssetsDescriptor != null)
{
FileInputStream fis = RawAssetsDescriptor.createInputStream();
NativeMethods.dataPackChannel = fis.getChannel();
NativeMethods.dataPackOffset = RawAssetsDescriptor.getStartOffset();
NativeMethods.dataPackSize = RawAssetsDescriptor.getLength();
}
Нейтив класс все вызовы open,read,seek транслирует опять в Java
class AndroidPackStream : public IStream
{
//....
// пример работы read
size_t read(void *buffer, size_t size, size_t count)
{
if( JavaHelpers::m_pClass )
{
jmethodID mid = JavaHelpers::GetEnv()->GetStaticMethodID(JavaHelpers::m_pClass, "freadDataPack", "(I)I");
int res = JavaHelpers::GetEnv()->CallStaticIntMethod(JavaHelpers::m_pClass, mid, (int)size*count);
jfieldID field = JavaHelpers::GetEnv()->GetStaticFieldID(JavaHelpers::m_pClass,"byteBuffer","Ljava/nio/MappedByteBuffer;");
jobject obj = JavaHelpers::GetEnv()->GetStaticObjectField(JavaHelpers::m_pClass,field);
uint8_t* pData=(uint8_t*)JavaHelpers::GetEnv()->GetDirectBufferAddress(obj);
memcpy(buffer,pData,size*count);
JavaHelpers::GetEnv()->DeleteLocalRef(obj);
return res/size;
}
}
Java реализации чтения данного стрима с возвратом данных назад в нейтив
public static int freadDataPack(int count)
{
long curPos = dataPackChannel.position();
int countReaded = count;
byteBuffer = dataPackChannel.map(MapMode.READ_ONLY,dataPackChannel.position(), count);
byteBuffer.load();
dataPackChannel.position(curPos+countReaded);
return countReaded;
}
Важный момент — сборщик apk сжимает все ресурсы, а мы хотим читать файл как будто он просто лежит в файловой системе. Для того чтобы наш .pak файл не сжимался внутри apk — вы должны сменить ему расширение на одно из тех которые будут говорить упаковщику не сжимать данные файлы при упаковке (детали ponystyle.com/blog/2010/03/26/dealing-with-asset-compression-in-android-apps). Я выбрал .imy.
Загрузка ресуров таким способом очень быстра, например на Kindle Fire работает быстрее чем на iPad 1
Оговорю стразу что такой финт можно провернуть если у вас не очень много данных.
Для большого объема данных — вы можете сами распаковать данные на внутрений носитель(например при первом запуске) и напрямую их использовать посредством функций fopen fread (в моем случае, через не подмененный FileStream) и.т.д.
Звук
Для Windows и iOS был использован OpenAL. OpenAL для Android был собран благодаря pielot.org/2010/12/14/openal-on-android. Заработал не сразу, а только после исправлений которые описаны в комментариях на вебсайте.
Сборка под Android собрана только для Arm v7 — потому что сборка порта под Arm v6 OpenAL иногда приводила к лагам, в iOS OpenAL микшер работает быстрее и без лагов (даже на ArmV6, например на iPod Touch 2G).
Взаимодействие с нативным кодом из С/C++
За обработку вызовов платформо зависимых реализаций отвечает очень простая связка классов
Псевдо код:
class IPlatfomCommandFeedback
{
void onResponse(string&);
};
class IPlatfomCommand
{
string execute(string& command, IPlatfomCommandFeedback* feebback);
};
Для iOS своя реализация на Objective-C а для Android своя реализация на JNI->Java.
Рендеринг
Нейтив часть работает с использованием OpenGL, для Android были сделаны минимальные изменения. Как в последствии оказалось этого было недостаточно. Дело в том что на Windows и на iOS текстуры не теряются когда приложение уходит в бакграунд, а вот для Android это происходит всегда. В движке уже был менеджер текстур (в основном для отладки) и добавить перезагрузку ресурсов оказалось не сложно.
Сборка
В самой первой статье я писал о том, что изначально компилировал iOS с использованием toolchain и у меня были настроенные makefile. Вот тут то мне это и пригодилось. Были дописаны таргеты для сборки с использованием Android NDK, добавлен степ в билдеры Eclipse и все взлетело. Да, можно использовать билд систему из Android NDK samples. Её я использовал только для выяснения параметров которые используются для вызовов gcc.
OpenFenit AdMob
Интеграция обоих библиотек прошла без проблем — четко по инструкции разработчиков
Proguard
Тут все более или менее понятно — нужно только позаботится о том чтобы ваши JNI Native бинды не обускейтились
Средства разработки
Железо: Kindle Fire, и пару планшетов и мобильных телефонов друзей(для тестирования).
Софт: Eclipse с плагинами
Шок номер 1
Android Market это не AppStore. Там все по другому. Там нет категории New.
Для сравнения на AppStore в первый день релиза было 800+ скачек, во второй 2000+, на Andoid Market официальная первая сотня скачек была получена только на третий день. Активность по раскрутке для обоих платформ была одинакова
Шок номер 2
Если ваше приложение бесплатное вы можете свободно распространять apk, любой человек может проинсталировать себе данное приложение с любого источника. Если это сделаете не вы, то за вас это сделают другие.
Шок номер 3
Бестиарий устройств велик. Количество проблем соответственно.
Amazon
Процесс ревью на Amazon AppStore for Android занимает неделю, плюс еще несколько дней пока приложение появится в списке AppStore на Kindle Fire. Количество скачек и динамика соответствует Android Market
Реклама
Лучи ненависти Корпорации Добра — уже неделя как рекламный баннер был отправлен на апрув. До сих пор тишина.
Деньги
На данный момент количество денег (напомню в игре только реклама) которые приносит iOS версия в 40 (сорок) раз больше чем версия для Android. Понятно, что сравнение не корректно и надо подождать как минимум еще пару месяцев пока не стабилизируется количество постоянных игроков в день.
UPDATE: В секции звука я описал причину сборки только для ArmV7 Android устройств.