#20 まさか、Cocos2d-x 使っているのに C++ 書いてるわけないよね?

Unity ではプラグイン開発専門の @Gemmbu です。

みなさん Cocos2d-x で開発してますよね?

当然のことながら Cocos2d-x で開発する際は Lua/JavaScript で開発していますよね?

Q. Lua/JavaScript で開発すると何がうれしいの?

A. スマートフォンの開発サイクルを高速化できます

スマートフォンの開発サイクルって通常以下のフローを回しますよね

  1. コーディング
  2. ビルド
  3. 実機/シミュレータへ転送
  4. テスト

このうち、開発の本質でない部分を Lua/JavaScript を使うことでさくっと省くことができます。

Lua/JavaScript を使用した場合のフローは

  1. コーディング
  2. 実機/シミュレータへ転送(修正したスクリプトのみ)
  3. テスト

ビルドがなくなった!!。また、実機/シミュレータへ転送もスクリプトファイルのみになりました。 ゲーム等画像及び音声の多いものを作っている場合は、コード以外のリソースの転送がほとんどなのでスクリプトファイルだけの転送になるのは嬉しいですよね。

Q. じゃあ、どうやるの?

A. Android は SD カードに書き込んで、それを読み出すようにちょっと修正するだけ

---- <PROJECT_ROOT>/Classes/AppDelegate.cpp ----

/* Lua スクリプトの読み込み先を SD カードからに変更する */
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
    const char *pLuaFile = "/mnt/sdcard/genius.lua";    /* 環境に合わせたパスに変更する */
    CCString *pstrFileContent = CCString::createWithContentsOfFile(pLuaFile);
    if (pstrFileContent)
    {
        pEngine->executeString(pstrFileContent->getCString());
    }
#endif

上記のように修正して

$ adb push genius.lua /mnt/sdcard/genius.lua

でスクリプトを更新したら、アプリを再起動するだけ。簡単ですね。 SDカードのパスをちゃんとするなら、jni で以下のようなコードを書きましょう

/* android.os.Environment */
typedef struct {
    jclass klass;
    jmethodID get_external_storage_directory;
} android_os_environment_class;

/* java.io.File */
typedef struct {
    jclass klass;
    jmethodID get_path;
} java_io_file_class;

static void get_sdcard_path(JNIEnv* env, char* dst)
{
    android_os_environment_class environment_class;
    java_io_file_class f_class;
    jobject f;
    jstring j_path;
    const char* n_path;
    environment_class.klass = env->FindClass("android.os.Environment");
    environment_class.get_external_storage_directory
        = env->GetStaticMethodID(environment_class.klass,
                                 "getExternalStorageDirectory",
                                 "()Ljava/io/File;");

    f_class.klass = env->FindClass("java.io.File");
    f_class.get_path = env->GetMethodID(f_class.klass,
                                        "getPath",
                                        "()Ljava/lang/String;");

    f = env->CallStaticObjectMethod(environment_class.klass,
                                    environment_class.get_external_storage_directory);

    j_path = (jstring) env->CallObjectMethod(f, f_class.get_path);
    if(j_path != NULL){
        n_path = env->GetStringUTFChars(j_path, NULL);
        strcpy(dst, n_path);
    }

    env->ReleaseStringUTFChars(j_path, n_path);
    env->DeleteLocalRef(j_path);
    env->DeleteLocalRef(f_class.klass);
    env->DeleteLocalRef(environment_class.klass);
}

Q. じゃあ、SD カードが使えない iOS はどうやるの?

A. iTunes からファイルを転送します

Q. めんどいよ

A. じゃあ、iOS に curl から post して

simple-post-server を使えば curl からファイルを post できます。

iOS のプロジェクトに移動して simple-post-server を追加します。

$ git submodule add git@github.com:KAMEDAkyosuke/simple-post-server.git <PROJECT_NAME>/ios/external/simple-post-server

また、simple-post-server は http-parserに依存しているため、以下を行います

$ cd <PROJECT_NAME>/ios/external/simple-post-server
$ git submodule init
$ git submodule update

xcode の Build Phases の Compile Sources に以下を追加します。

  • <PROJECT_NAME>/ios/external/simple-post-server/body-parser.c
  • <PROJECT_NAME>/ios/external/simple-post-server/http-parser-helper.c
  • <PROJECT_NAME>/ios/external/simple-post-server/intlist.c
  • <PROJECT_NAME>/ios/external/simple-post-server/simple-post-server.c
  • <PROJECT_NAME>/ios/external/simple-post-server/stringlist.c
  • <PROJECT_NAME>/ios/external/simple-post-server/external/http-parser/http_parser.c

サーバの起動と、post されたファイルの保存のコードを書きましょう。

---- <PROJECT_NAME>/ios/AppController.mm ----

/* ポストしたファイルを保存する */
static void simple_post_server_callback(post_content_t* post_content){
    NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentDirectory = [documentDirectories lastObject];

    NSString *path = [documentDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"%s", post_content->filename]];
    NSData *content = [NSData dataWithBytes:post_content->body
                                     length:strlen(post_content->body)];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError* error = nil;
    if([fileManager fileExistsAtPath:path]){
        [fileManager removeItemAtPath:path error:&error];
        if(error != nil){
            NSLog(@"%@", [error description]);
        }
    }
    [fileManager createFileAtPath:path
                         contents:content
                       attributes:nil];
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    /* サーバの起動 */
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
    dispatch_async(queue, ^{
        start_server(8080, simple_post_server_callback);
    });
    ... 

ファイルの保存はよくあるコード。サーバの起動もわずか 4 行。簡単ですね。

あとは android の場合と同様に読み込む先のスクリプトパスを変更すればいいだけ。

ファイルの転送を便利にするために以下のようなスクリプトを書くといいですね。

---- post_all.sh ----
#!/bin/sh

HOST=$1

for filepath in *.lua; do
    HOGE="curl -F file=@$filepath http://$HOST:8080"
    echo $HOGE
    $HOGE
done

スクリプトの編集が終わったら以下のコマンドで一気に転送しましょう

$ ./post_all.sh <your_ios_ip_address>

Q. iOSでスクリプト簡単に転送できるようになったけど、エラーログってどうやってみるの?

A. <PROJECT_NAME>/libs/cocos2dx/platform/ios/CCCommon.mm を書き換えましょう

具体的には以下のようにログ出力に NSLog を使用するようにしましょう。

void CCLog(const char * pszFormat, ...)
{
    char szBuf[kMaxLogLen];

    va_list ap;
    va_start(ap, pszFormat);
    vsnprintf(szBuf, kMaxLogLen, pszFormat, ap);
    va_end(ap);
    NSLog(@"%s", szBuf);
}

xcode につながなくても iPhone構成ユーティリティ 使えば見られるのはご存知ですよね。

Q. パフォーマンスは?

A. 弊社のアプリを手元で適当に移植した際は問題なし

いざとなったら C/C++ で書けばいいし。

まとめると

  • Cocos2d-x で C++ だけで書いているのはもったいないよ。
  • Android は SD カードで簡単にスクリプトだけの更新ができるよ。
  • iOS は面倒だから curl からの post を受け取れるサーバを書いたよ。みんな使ってね。
  • パフォーマンス困ったら C/C++ で書けばいいから心配しなくてもいいんじゃない?

明日はマヤ暦によると人類滅亡の日です。 人類滅亡が先か@hisaichi5518さんの記事が公開されるのが先か楽しみですね。