Home > Coding > Cocos2d-JS: load external audio assets and javascript codes

Cocos2d-JS: load external audio assets and javascript codes

Today I would like to tell about a quite complicated problem, which I have recently managed to resolve. The major sense of it can be found in the title of the post. One may as a question: what is the way this feature can be useful? It seems like no one was wondering this problem before, because Google was extremely poor for the answers. It is very logical and obvious: this is so much easier to debug, isn’t it? In order to try out new sounds or changes in the level logic, you do not need to recompile the entire project each time, you don’t even need to restart the application instead. Let me tell you how I managed to do it.

It is important to make a little remark. Though the project is cross-platform (target platforms – iOS and Android), the given solution is going to be basically for iOS, because the considering functionality is basically required for the period of debugging, and I prefer to debug mobile projects on iOS devices. However, this solution is applicable for Android as well, you just need to add a bit of native code. Here is the way it works briefly: a collection of game sounds is stored on an external server (where they can be manipulated: add, delete, change), also there is a sort of service screen in the game, where the assets are being loaded. Files are stored locally in the standard Documents folder. I also provided a kind of versioning based on checksums calculation – it allows to avoid not to download all files every time, only those that was really changed. I designed a web-based console for more convenience. I’m not going to describe all updating mechanics here – it is not the essence of this article. Let’s see how these files get into the JS environment. Sounds playback is performed by AudioEngine module which is the part of Cocos2d framework. You can playback audio file with following:

cc.audioEngine.playEffect(soundPath, repeat);

here soundPath is the path to audio asset. The main mysterious was that all manuals and forums claimed this parameter was a relative path, and Cocos2d is looking for specified file in the directories listed in searchPath collection. You can add the item to this collection the following way:

jsb.fileUtils.addSearchPath(resPath);

but, as I figured out, resPath is also considered by Cocos2d as a relative, so it looks for this file in the Application Bundle folder. The salvation was in the fact that as a parameter soundPath playEffect function, you can specify an absolute path! All we have to do is to supply playEffect function with the path to an audio file that can be located anywhere in the file system. How to tell a javascript code, where to get the audio file? In order to do this, the creators of cocos2d provided a binding mechanism for javascript (JSB), with the help of which native Objective-C methods can be called from javascript code. One option of this interaction is to create Objective-C class with a set of static methods, which will be called from a javascript-code. Here is the example:

#import 
 
#define LOAD_REMOTE_ASSETS YES
 
@interface NativeOcClass : NSObject 
+(NSString *)callNativeLoaderPathForAudioAsset:(NSString *)soundId;
+(NSString *)callNativeLoaderLevelSource:(NSString *)campaign :(NSString *)levelName;
 
@end

I declared two methods here (the second one will be described a bit later). For now we going to speak about first method callNativeLoaderPathForAudioAsset: – this method returns the absolute path to an audio file with soundId identifier specified. Here is the implementation:

+(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 @"";
}

Don’t look at the logic related to download mechanics. The major idea is that this method can return an absolute path to the file or empty string if it doesn’t find a requested file in the Documents folder. In this case javascript code will use asset from Application bundle folder. This method is called from javascript:

soundPathForCode: function(soundId) {
  var filePath = (jsb.reflection.callStaticMethod("NativeOcClass", "callNativeLoaderPathForAudioAsset:", soundId));
  if (filePath!="")
    return filePath;
  return "res/audio/" + Sounds[soundId];
},

here NativeOcClass is the class where callNativeLoaderPathForAudioAsset: method is declared. As you can see it’s pretty straight forward. As I previously said we specify the path we received from callNativeLoaderPathForAudioAsset methods as an argument of playEffect function.

var soundPath = this.soundPathForCode("player_jump");
cc.audioEngine.playEffect(soundPath, repeat);

I used the same approach for implementing remote downloads of level’s javascript code. callNativeLoaderLevelSource is designed for this purpose. It returns javascript code string of level which is synchronized from the server exactly as the sound assets. The only difference with audio files is that this code is being interpreted by eval mechanism of Javascript engine.

recreateLevelIfNeeded: function(campaign, levelNumber) {
  var jsFileName = "level"+levelNumber;
  var levelJsCode = jsb.reflection.callStaticMethod("NativeOcClass", "callNativeLoaderLevelSource::", campaign.toLowerCase(), jsFileName);
  if (levelJsCode != "") {
    eval(levelJsCode);
  }
},
Categories: Coding Tags: , , ,
  1. No comments yet.