Cocos2d-x под Android: ускорение чтения файлов

в 10:19, , рубрики: android, c++, cocos2d, game development, производительность, Разработка под android, метки: , , ,

Один из моих последних проектов — портирование игры с iOS на Android. Игра написана с использованием Cocos2d-x, довольно популярного кроссплатформенного игрового движка.

Подробнее о Cocos2d-x для Android

Cocos2d-x существует для Android уже фактически два года, достаточно солидный возраст. Открытый исходный код, лицензия MIT (не требует возврата изменений).
Последний стабильный релиз для OpenGL ES 1.x — 0.13.0, вышел в марте этого года.
Первый релиз для OpenGL ES 2.0 — 2.0.2, появился в конце августа.

Хочу рассказать, как можно легко многократно повысить скорость загрузки большой игры, а также улучшить общую плавность реакции на действия пользователя.

Спокойная работа

Игра на iOS использует версию 0.13, так что для Android взяли ту же самую (включая последние исправления из официального репозитория). Всё шло в общем-то нормально, без больших разочарований и найденных значительных багов именно в движке, версия игрушки для слабой графики (старый iPhone 480x320) нормально работала. Пришло время добавить «HD» режим графики (960x640).

На iOS в первой ветке cocos2d поддержка других режимов осуществляется автоматическим использованием файла с суффиксом текущего режима, если такой файл существует, иначе же — файлом без такого суффикса. Суффиксы — обычно это '-hd' для iPhone HD и iPad SD режимов, а также '-ipadhd' для iPad HD. Так, при попытке загрузить файл 'image.png', на самом деле может загрузиться файл 'image-hd.png'. Для версии под Android такого не было — добавлено самостоятельно. Графика стала лучше.

Проблема

Игрушка стала дольше грузиться, и значительно хуже реагировать на действия пользователя (переходы в меню, например).

Вначале решил, что так как каждый раз файл с суффиксом проверяется на существавание, то достаточно простого кеша в виде std::unordered_map<std::string requestedFile, std::string receivedFile> — чтобы проверять файл на существование всего 1 раз — частично помогло, но ситуация оставалась очень тяжелой. Удивляло, что для порта той же самой игры на Win8 такой проблемы не наблюдалось (а версия для Win8 поддерживала всё, включая iPad HD режим — с текстурами под экран 2048x1536).

Стал разбираться дальше.
CCFileUtils::getFileData — метод, отвечающий за чтение файлов в Cocos2d-x (на github — и в дальнейшем ссылки туда же) — для загрузки из APK вызывает CCFileUtils::getFileDataFromZip:

Исходник
unsigned char* CCFileUtils::getFileDataFromZip(const char* pszZipFilePath, const char* pszFileName, unsigned long * pSize)
{
    unsigned char * pBuffer = NULL;
    unzFile pFile = NULL;
    *pSize = 0;

    do
    {
        CC_BREAK_IF(!pszZipFilePath || !pszFileName);
        CC_BREAK_IF(strlen(pszZipFilePath) == 0);

        pFile = unzOpen(pszZipFilePath);
        CC_BREAK_IF(!pFile);

        int nRet = unzLocateFile(pFile, pszFileName, 1);
        CC_BREAK_IF(UNZ_OK != nRet);

        char szFilePathA[260];
        unz_file_info FileInfo;
        nRet = unzGetCurrentFileInfo(pFile, &FileInfo, szFilePathA, sizeof(szFilePathA), NULL, 0, NULL, 0);
        CC_BREAK_IF(UNZ_OK != nRet);

        nRet = unzOpenCurrentFile(pFile);
        CC_BREAK_IF(UNZ_OK != nRet);

        pBuffer = new unsigned char[FileInfo.uncompressed_size];
        int nSize = 0;
        nSize = unzReadCurrentFile(pFile, pBuffer, FileInfo.uncompressed_size);
        CCAssert(nSize == 0 || nSize == (int)FileInfo.uncompressed_size, "the file size is wrong");

        *pSize = FileInfo.uncompressed_size;
        unzCloseCurrentFile(pFile);
    } while (0);

    if (pFile)
    {
        unzClose(pFile);
    }

    return pBuffer;
}

То есть каждый раз для любого файла из APK — идёт отдельное открытие/закрытие архива. Самое простое действие — хотя бы открывать всего раз, решаю проверить, исправлено ли это в 2.0.x версиях Cocos2d-x — обнаруживаю, что не только не исправлено, но и ухудшено — в связи с измененной логикой файлов для разной графики (уже добавленной для Android версии) — возможна двойная попытка чтения из APK файла.

Ладно, смотрю дальше — вызов unzLocateFile. Что делает эта функция?

    err = unzGoToFirstFile(file);

    while (err == UNZ_OK)
    {
        char szCurrentFileName[UNZ_MAXFILENAMEINZIP+1];
        err = unzGetCurrentFileInfo64(file,NULL, szCurrentFileName,sizeof(szCurrentFileName)-1,NULL,0,NULL,0);
        if (err == UNZ_OK)
        {
            if (unzStringFileNameCompare(szCurrentFileName, szFileName,iCaseSensitivity)==0)
                return UNZ_OK;
            err = unzGoToNextFile(file);
        }
    }

То есть еще и каждый раз — линейный поиск по всем файлам в архиве. Каждый раз. Там глубже по коду — с экономией памяти, так что то и дело — перемещение по файлу с архивом и чтением. В APK с игрушкой — более 1300 файлов с ресурсами.
А для gles20 версии cocos2d-x — вообще худший случай, когда файл не прочитался, и надо искать второй заново.

Решение

Хорошо — решил как-то закешировать хотя бы список файлов, да и желательно его позицию в архиве, чтобы unzLocateFile быстрее работал. Начинаю копать дальше — и c удивлением обнаруживаю, что всё уже есть, достаточно начать использовать:

/* unz_file_info contain information about a file in the zipfile */
typedef struct unz_file_pos_s
{
    uLong pos_in_zip_directory; /* offset in zip file directory */
    uLong num_of_file; /* # of file */
} unz_file_pos;

int ZEXPORT unzGetFilePos(unzFile file, unz_file_pos* file_pos);

int ZEXPORT unzGoToFilePos(unzFile file, unz_file_pos* file_pos);

Так что в общем-то элементарно генерировать список файлов с позициями 1 раз, а уж дальше — спокойно напрямую читать, что требуется.

Проверка идеи показала ускорение загрузки в 5 раз (причем версии с тяжелой графикой, по сравнению со старой версией с упрощенной графикой), и общее улучшение игры (так как графики много, то все хранить в памяти невозможно, и приходится постоянно что-то подгружать).

В дальнейшем — выяснилось, что отдельно вызывать unzGetCurrentFileInfo после unzGoToFirstFile/unzGoToNextFile не требуется, они всё равно считывают ту же самую информацию.

Так что в итоге — создан вспомогательный класс, читающий весь список файлов из ZIP примерно за то же время, что и поиск одного существующего файла оригинальной unzLocateFile.

Pull request для последней версии Cocos2d-x, для Android — точно работает. Версия для 0.13 получается незначительной правкой.
Пользуйтесь на здоровье.

Выводы

Почему такое возможно? Всё в исходниках фактически готово, поиск решения занял буквально пару часов. Тем не менее, проблема существовала и решалась разнообразными методами, в основном — «меньше файлов в APK».
Потом уже нашел предложение 3 месяца назад использовать Asset Manager — надо будет глянуть, что там и как, и будет ли вообще рост производительности (или уже падение).

Думаю, то же самое неоднократно решалось в различных компаниях, просто никто не возвращал исправления обратно.

Используемые же части MiniZip в Cocos2d-x — фактически, просто демонстрируют как надо делать, и никогда не предназначались для постоянного чтения случайных файлов.

Так что не смотря на два года, в публичной версии всё еще существует много мест, где элементарно что-то улучшить. Производительность — да те же самые классы из CCProfiling.h/.cpp, предназначеные для профилирования движка, где нечто среднее вычисляется сложением старого среднего и нового значения, и дальнейшим делением пополам. Так что результаты для, например, одних и тех же значений 10 4 1 и 1 4 10 получаются разные (4 и 6.25 соответственно). Точно та же проблема и в исходниках cocos2d-iphone, так что общее исправление буду делать чуть позже — если никого другого это не заинтересует.

Надеюсь, будет время, и другие исправления тоже получится добавить в официальную версию, с переводом из 0.13 в текущую 2.x.

Автор: dei

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js