Cocos2d-JS: загрузить внешние аудио файлы и javascript код
Сегодня я хотел бы поведать о достаточно сложной задаче, которую мне недавно удалось решить. Краткую суть ее можно увидеть в заголовке поста. Может возникнуть вопрос: зачем такое вообще могло понадобиться? Такое ощущение, что никто до меня этим вопросом не задавался, ибо гугл был крайне скуден на ответы. Ведь это же очень логично и очевидно — так гораздо легче отлаживать. Для того, чтобы опробовать новый звук или изменения в уровневой логике, не нужно перекомпилировать каждый раз проект, можно даже не перезапускать приложение. Сейчас расскажу, как мне удалось этого добиться.
Следует сразу оговориться. Хоть проект и кроссплатформенный (целевые платформы — iOS и Android), приведенное решение будет преимущественно для iOS, поскольку рассматриваемая функциональность мне нужна только на период отладки, а отлаживать я предпочитаю на iOS устройствах. Тем не менее, для андроида это решение также применимо, нужно только немного дописать нативный код. И так, как это работает в двух словах: коллекция игровых звуков хранится на внешнем сервере (ими там можно манипулировать: добавлять, удалять, менять), затем в самой игре есть некий служебный экран, где собственно происходит загрузка ассетов. Файлы сохраняются локально внутри стандартной папки Documents. Также я предусмотрел некоторую версионность, основанную на контрольных суммах — чтобы не загружать каждый раз все файлы, а только изменившиеся. Для удобства я сконструировал веб-консоль. Всю механику обновления я здесь описывать не стану — это не есть суть статьи. Перейдем к тому, как эти ассеты попадают в JS окружение. Проигрыванием звуков занимается модуль AudioEngine, являющийся частью фреймворка Cocos2d. Проигрывание аудио-файла можно осуществить следующим образом
cc.audioEngine.playEffect(soundPath, repeat);
где soundPath — путь к аудио-ассету. Загвоздка была в том, что во всех мануалах и на форумах говорится, что это относительный путь, и что кокос ищет указанный файл в директориях, перечисленных в коллекции searchPath. Добавить элемент в эту коллекцию можно так:
jsb.fileUtils.addSearchPath(resPath);
но, как оказалось, resPath так же может быть только относительным, то есть данный путь будет внутри папки, где лежат все запчасти билда (Applicaition Bundle). Спасением стало то, что в качестве параметра soundPath функции playEffect можно указать абсолютный путь! Таким образом, все, что нам осталось сделать, это подсунуть функции playEffect путь к аудиофайлу, который может находится в любом месте файловой системы. Как сообщить javascript коду, где брать аудио файл? Для этого создатели cocos2d очень разумно предусмотрели механизм javascript binding (JSB), за счет которого можно вызывать из javascript кода нативные методы Objective-C Один из вариантов данного взаимодействия заключается в создании Objective-C класса с набором статических методов, которые и будут вызываться из javascript-кода. Вот пример:
#import #define LOAD_REMOTE_ASSETS YES @interface NativeOcClass : NSObject +(NSString *)callNativeLoaderPathForAudioAsset:(NSString *)soundId; +(NSString *)callNativeLoaderLevelSource:(NSString *)campaign :(NSString *)levelName; @end
Я здесь объявил два метода, о втором я расскажу чуть ниже. Сейчас речь пойдет callNativeLoaderPathForAudioAsset: — данный метод возвращает абсолютный путь к аудио файлу с идентификатором soundId. Вот его реализация:
+(NSString *)callNativeLoaderPathForAudioAsset:(NSString *)soundId { if (!LOAD_REMOTE_ASSETS) return @""; NSUserDefaults * ud = [NSUserDefaults standardUserDefaults]; NSDictionary * files = [ud objectForKey: @"remoteAssets"]; NSString * revisionKey = [NSString stringWithFormat:@"revision_%@", soundId]; NSString * assetsDirPath = [NativeOcClass callNativeDocumentsResPath]; NSString * audioPath = [assetsDirPath stringByAppendingPathComponent:@"res/audio"]; NSString * soundIdRevision = [NSString stringWithFormat:@"%@_%@", soundId, [files objectForKey:revisionKey]]; NSString * filePath = [audioPath stringByAppendingPathComponent:soundIdRevision]; if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) return filePath; return @""; }
Не обращайте внимание на дополнительную логику связанную с механикой подгрузки файлов с сервера. Главная идея в том, что данный метод может вернуть абсолютный путь к аудиофайлу или пустую строку, если запрашиваемый файл отсутствует в папке Documents. В данном случае javasсript код будет использовать ассет, находящийся в бандле приложения (Application Bundle). Из javascript этот метод вызывается так:
soundPathForCode: function(soundId) { var filePath = (jsb.reflection.callStaticMethod("NativeOcClass", "callNativeLoaderPathForAudioAsset:", soundId)); if (filePath!="") return filePath; return "res/audio/" + Sounds[soundId]; },
здесь NativeOcClass — класс, внутри которого объявлен метод callNativeLoaderPathForAudioAsset: Как видите, все довольно просто. Далее, как я уже и говорил, мы указываем полученный путь в качестве аргумента вызова функции playEffect
var soundPath = this.soundPathForCode("player_jump"); cc.audioEngine.playEffect(soundPath, repeat);
Таким же образом я сделал удаленную загрузку javascript кода уровней. Для этого в классе NativeOcClass объявлен метод callNativeLoaderLevelSource — он возвращает строку с javascript кодом уровня, который так же как и аудио ассеты синхронизируется с сервера. Только в отличие от аудиофайлов данный код интерпретируется механизмом eval движка Javascript.
recreateLevelIfNeeded: function(campaign, levelNumber) { var jsFileName = "level"+levelNumber; var levelJsCode = jsb.reflection.callStaticMethod("NativeOcClass", "callNativeLoaderLevelSource::", campaign.toLowerCase(), jsFileName); if (levelJsCode != "") { eval(levelJsCode); } },