galapagosit’s blog

本家 > http://galapagosit.com/

twitter連携

ios

http://youngforever.hatenablog.com/entry/2014/05/31/150452

上記を参考に実装

android

twitter4jによるツイート実装もあるが、認証が面倒でツイートしてくれなそうなので
シンプルに公式アプリに対してIntentを送る事にした。 (公式アプリを探す処理が入ってるので若干長い)

public class TwitterTool {
    public static int REQUEST_CODE = 777;
    public static void share(String paramsString) {
        // パラメータをjson文字列形式で貰ってる
        Log.d("TwitterTool", "paramsString:" + paramsString);
        Map<String, String> params = JSON.decode(paramsString);

        AppActivity activity = (AppActivity) AppActivity.getContext();
        
        Intent tweetIntent = new Intent(Intent.ACTION_SEND);
        tweetIntent.putExtra(Intent.EXTRA_TEXT, params.get("text"));
        //tweetIntent.setType("text/plain");
        
        // resフォルダ内の画像ファイルも追加する
        tweetIntent.setType("image/*");
        String fileName = "android.resource://" + activity.getPackageName() + "/" + R.raw.twitter_share;
        Uri uri = Uri.parse(fileName);
        tweetIntent.putExtra(Intent.EXTRA_STREAM, uri);
        
        // twitter公式アプリのみを探す処理
        PackageManager packManager = activity.getPackageManager();
        List<ResolveInfo> resolvedInfoList = packManager.queryIntentActivities(tweetIntent,  PackageManager.MATCH_DEFAULT_ONLY);

        boolean resolved = false;
        for(ResolveInfo resolveInfo: resolvedInfoList){
            if(resolveInfo.activityInfo.packageName.startsWith("com.twitter.android")){
                tweetIntent.setClassName(
                    resolveInfo.activityInfo.packageName, 
                    resolveInfo.activityInfo.name );
                resolved = true;
                break;
            }
        }
        if(resolved){
            activity.startActivityForResult(tweetIntent, REQUEST_CODE);
        }else{
            Toast.makeText(activity, "最新のtwitter公式アプリが必要です", Toast.LENGTH_LONG).show();
        }
    }
    
    public static void successCallback() {
        // ここでツイート成功処理
    }
}

MainActivity

   @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if(requestCode == TwitterTool.REQUEST_CODE){
            Log.d("AppActivity", "onActivityResult get twitter requestCode resultCode:" + resultCode);
            // ツイート成功は-1っぽい
            if(resultCode == -1){
                TwitterTool.successCallback();
            }
        }

    }

Cocos2d-JS Facebook連携 作業メモ

作業メモです

ios

基本的な流れ

http://cocos2d-x.org/docs/manual/framework/html5/facebook-sdk/facebook-sdk-on-ios/en

facebookアプリ登録

https://developers.facebook.com/docs/ios/getting-started/#appid

ハマった点

http://forums.xamarin.com/discussion/24656/error-running-app-after-upgrading-xamarin
上記のエラーが出るようになってしまったけど、よくわからないので放置。

アプリ毎に変わるところ

facebookアプリ登録
FacebookAppID, FacebookDisplayName, URL types in Info.plist file
を変更

android

基本的な流れ

http://www.cocos2d-x.org/docs/manual/framework/html5/facebook-sdk/facebook-sdk-on-android/en

facebookアプリ登録

https://developers.facebook.com/docs/android/getting-started?locale=ja_JP

ハマった点

3.1から指定方法が変更になり、下記のようにしないとコンパイルできなかった
(誤)LOCAL_WHOLE_STATIC_LIBRARIES += jsb_pluginx_static
(正)LOCAL_STATIC_LIBRARIES += jsb_pluginx_static

アプリ毎に変わるところ

facebookアプリ登録
app_id
を変更

Cocos2d-JSでflappy bird

某有名フラッピーバードのロジック部分をCocos2d-JSで再現してみました。

コードはあんま綺麗じゃないかもしれないですが、 C++で書くよりだいぶシンプルに書けるというのが伝わればいいと思います。

  • 画面サイズは720×1280
  • underscorejsを使用しています。

実際の動き

シーンのコード

var createPlayer = (function(){
    var player = {
        // 重力
        GRAVITY_Y_FORCE: -3,
        // ジャンプ力
        JUMP_Y_FORCE: 25,

        // y方向に向かう力
        y_force: 0,

        update: function(dt) {
            // y方向の移動
            this.y += this.y_force;
            // 重力を加算
            this.y_force += this.GRAVITY_Y_FORCE;
        },
        jump: function() {
            // ジャンプの分 Y方向を加算
            this.y_force = this.JUMP_Y_FORCE;
        }
    };
    return function () {
        // playerにあたるスプライトを生成
        var sprite = new cc.Sprite();
        sprite.setTextureRect(cc.rect(0, 0, 100, 100));
        sprite.color = cc.color(255, 0, 0, 128);

        // 初期位置
        sprite.x = 300;
        sprite.y = 640;

        // spriteに属性を追加
        _.extend(sprite, player);

        return sprite;
    }
})();

var createEnemy = (function(){
    var enemy = {};
    return function (y_under, y_top) {
        // 敵(土管)にあたるスプライトを生成
        var sprite = new cc.Sprite();
        sprite.color = cc.color(0, 255, 0, 128);

        sprite.x = 900; // 画面外にセット
        sprite.y = (y_top + y_under) / 2;
        sprite.setTextureRect(cc.rect(0, 0, 100, (y_top - y_under)));

        // spriteに属性を追加
        _.extend(sprite, enemy);
        return sprite;
    }
})();

var createScorePoint = (function(){
    var scorePoint = {};
    return function (y_under, y_top) {
        // スコア獲得地点にあたるスプライトを生成
        var sprite = new cc.Sprite();
        sprite.color = cc.color(0, 0, 255, 30);

        sprite.x = 900; // 画面外にセット
        sprite.y = (y_top + y_under) / 2;
        sprite.setTextureRect(cc.rect(0, 0, 20, (y_top - y_under)));

        // spriteに属性を追加
        _.extend(sprite, scorePoint);
        return sprite;
    }
})();


READY = 1;
START = 2;
OVER = 3;

var ActionLayer = cc.Layer.extend({
    // ゲームの状態
    gameMode: READY,
    // ゲームの状態を表すテキスト
    labelGameMode: null,
    // スコア
    score: 0,
    // スコアを表すテキスト
    labelScore: null,

    // 敵(土管)スピード
    ENEMY_X_SPEED: -10,
    // 敵(土管)の隙間
    ENEMY_Y_SPACE: 400,

    // プレイヤー
    player: null,
    // 敵(土管)
    enemies: [],
    // スコア獲得ポイント
    scorePoints: [],
    // 敵(土管)が生まれてからの時間
    enemyBornDt: 0,

    ctor:function () {
        this._super();

        // プレイヤー作成
        this.player = createPlayer();
        this.addChild(this.player);

        // タッチイベント登録
        this.initTouchEvent();

        // ゲームスタート
        this.changeGameMode(READY);

        this.enemies = [];
        this.scorePoints = [];

        return true;
    },
    update:function (dt) {
        // 敵(土管)の作成
        this.manageEnemy(dt);
        // プレイヤーの死亡判定
        this.chackDeath();
        // スコア獲得判定
        this.chackEarnScore();
    },
    initTouchEvent: function() {
        var listener = cc.EventListener.create({
            event: cc.EventListener.TOUCH_ONE_BY_ONE,
            swallowTouches: false,
            onTouchBegan: function (touch, event) {
                var target = event.getCurrentTarget();
                target.touched();
                return true;
            }
        });
        cc.eventManager.addListener(listener, this);
    },
    touched: function() {
        if (this.gameMode == READY) {
            // ゲームスタート
            this.changeGameMode(START);
        }
        if (this.gameMode == START) {
            // ジャンプ
            this.player.jump();
        }
        if (this.gameMode == OVER) {
            // シーンを読み込み直す
            cc.director.runScene(new ActionScene());
        }
    },
    changeGameMode: function(gameMode) {
        this.gameMode = gameMode;

        if(!this.labelGameMode){
            this.labelGameMode = new cc.LabelTTF();
            this.labelGameMode.setFontSize(30);
            this.labelGameMode.setPosition(100, 1180);
            this.addChild(this.labelGameMode);
        }
        this.labelGameMode.setString("gameMode:" + gameMode);

        if (this.gameMode == START) {
            // それぞれのupdateメソッドを毎フレーム呼び出すようにする
            this.scheduleUpdate();
            this.player.scheduleUpdate();
        }
        if (this.gameMode == OVER) {
            // それぞれのupdateメソッド呼び出しを停止
            this.unscheduleUpdate();
            this.player.unscheduleUpdate();
        }
    },
    manageEnemy:function (dt) {
        // 敵(土管)が生まれてからの時間
        this.enemyBornDt += dt;

        // 存在する敵(土管)を移動
        _.each(this.enemies, function(enemy) {
            // 移動
            enemy.x += this.ENEMY_X_SPEED;
        }, this);
        // スコアポイントを移動
        _.each(this.scorePoints, function(scorePoint) {
            // 移動
            scorePoint.x += this.ENEMY_X_SPEED;
        }, this);

        // 存在する敵(土管)が画面外に出たら消す
        this.enemies = _.filter(this.enemies, function(enemy) {
            if(enemy.x < -100){
                enemy.removeFromParent();
                return false;
            }
            return true;
        }, this);

        // 一定時間たったら敵(土管)を生成
        if(this.enemyBornDt > 5){
            this.enemyBornDt = 0;

            // 上下の土管の中心Y座標をランダムで取得
            var centerOfEnemy = _.random(200, 1080);

            // 上の土管の下部の座標
            var _top = centerOfEnemy + this.ENEMY_Y_SPACE / 2;
            // 下の土管の上部の座標
            var _bottom = centerOfEnemy - this.ENEMY_Y_SPACE / 2;

            // 上側
            var enemyUpper = createEnemy(_top, 1280);
            this.addChild(enemyUpper);
            this.enemies.push(enemyUpper);

            // 下側
            var enemyUnder = createEnemy(0, _bottom);
            this.addChild(enemyUnder);
            this.enemies.push(enemyUnder);

            // スコア獲得ポイント
            var scorePoint = createScorePoint(_top, _bottom);
            this.addChild(scorePoint);
            this.scorePoints.push(scorePoint);
        }
    },
    chackDeath:function () {
        // 敵(土管)と接触したら死ぬ
        _.each(this.enemies, function(enemy, index, array) {
            if(cc.rectIntersectsRect(
                this.player.getBoundingBox(),
                enemy.getBoundingBox()
            )){
                this.die();
            }
        }, this);

        // プレイヤーが画面外に出たら死ぬ
        if(this.player.y < 0 || 1280 < this.player.y){
            this.die();
        }
    },
    chackEarnScore:function () {
        // スコア獲得ポイントと接触したらスコア加算
        _.each(this.scorePoints, function(scorePoint, index, array) {
            if(cc.rectIntersectsRect(
                this.player.getBoundingBox(),
                scorePoint.getBoundingBox()
            )){
                this.addScore();
                scorePoint.removeFromParent();
                array.splice(index, 1);
            }
        }, this);
    },
    addScore:function () {
        this.score += 1;
        if(!this.labelScore){
            this.labelScore = new cc.LabelTTF();
            this.labelScore.setFontSize(30);
            this.labelScore.setPosition(100, 1080);
            this.addChild(this.labelScore);
        }
        this.labelScore.setString("score:" + this.score);
    },
    die:function () {
        // 死んだ
        this.changeGameMode(OVER);
    }
});

var ActionScene = cc.Scene.extend({
    onEnter:function () {
        this._super();
        var layer = new ActionLayer();
        this.addChild(layer);
    }
});

Cocos2d-JSのガベージコレクションについて

Cocos2d-JSでSQLite - galapagosit’s blog

上記の実装をしていた際に気になったこと。

var accesser = new jsb.SQLiteBridge("My.db");
accesser.execSql(sql);

// この後accesserは使用しないものとする

jsバインディングで生成したSQLiteBridgeインスタンス、ちゃんとdeleteされてメモリが開放されるのだろうか?
であればどこかのタイミングで下記の処理が呼ばれるはずだが、呼ばれていない。

SQLiteBridge::~SQLiteBridge()
{
    CCLOG("SQLiteBridge::~SQLiteBridge");
    sqlite3_close(useDataBase);
}

調査してみた

コンストラクタ

そもそもコンストラクタはgenerator.pyによって生成された以下のコードで実行されていた。

bool js_jsb_sqlite_bridge_auto_SQLiteBridge_constructor(JSContext *cx, uint32_t argc, jsval *vp)
{
    jsval *argv = JS_ARGV(cx, vp);
    bool ok = true;
    std::string arg0;
    ok &= jsval_to_std_string(cx, argv[0], &arg0);
    JSB_PRECONDITION2(ok, cx, false, "js_jsb_sqlite_bridge_auto_SQLiteBridge_constructor : Error processing arguments");
    SQLiteBridge* cobj = new (std::nothrow) SQLiteBridge(arg0);
    TypeTest<SQLiteBridge> t;
    js_type_class_t *typeClass = nullptr;
    std::string typeName = t.s_name();
    auto typeMapIter = _js_global_type_map.find(typeName);
    CCASSERT(typeMapIter != _js_global_type_map.end(), "Can't find the class type!");
    typeClass = typeMapIter->second;
    CCASSERT(typeClass, "The value is null.");
    JSObject *obj = JS_NewObject(cx, typeClass->jsclass, typeClass->proto, typeClass->parentProto);
    JS_SET_RVAL(cx, vp, OBJECT_TO_JSVAL(obj));
    // link the native object with the javascript object
    js_proxy_t* p = jsb_new_proxy(cobj, obj);
    if (JS_HasProperty(cx, obj, "_ctor", &ok))
        ScriptingCore::getInstance()->executeFunctionWithOwner(OBJECT_TO_JSVAL(obj), "_ctor", argc, argv);
    return true;
}
デストラクタ

ではデストラクタは何処だろうか、おそらく上記と同一コード内の以下の処理

void js_SQLiteBridge_finalize(JSFreeOp *fop, JSObject *obj) {
    CCLOGINFO("jsbindings: finalizing JS object %p (SQLiteBridge)", obj);
    js_proxy_t* nproxy;
    js_proxy_t* jsproxy;
    jsproxy = jsb_get_js_proxy(obj);
    if (jsproxy) {
        nproxy = jsb_get_native_proxy(jsproxy->ptr);

        SQLiteBridge *nobj = static_cast<SQLiteBridge *>(nproxy->ptr);
        if (nobj)
            delete nobj;
        
        jsb_remove_proxy(nproxy, jsproxy);
    }
}
デストラクタはいつ呼ばれるのか

ソースを追ってみるとおそらく該当のjs変数(accesser)がガベージコレクションされるタイミングの様だ。
で、それっていつなの?デストラクタが呼ばれないと不安で夜も眠れないので意地でも呼ばれるところを見届けたいところです。

どうやら下記のコードで強制的にガベージコレクション出来るようです。

__jsc__.garbageCollect();

これを呼んだ所、ちゃんとデストラクタが呼ばれました。良かったよかった。
そもそもの疑問の答えとしては、ガベージコレクションのタイミングはSpiderMonkeyが制御しているので、すぐには呼ばれなかったということだと思います。

Cocos2d-JSでSQLite

Cocos2d-JSネイティブアプリ(not ブラウザ)でsqliteを使う。

方針

準備

C++では

  • sqlite3
  • picojson

を使用させてもらっていますので、それぞれ設定をします

実装

bindするソース

SQLiteBridge.h

#ifndef __BRIDGE_H__
#define __BRIDGE_H__

#include "sqlite3.h"
#include <iostream>

class SQLiteBridge
{
private:
    sqlite3 *useDataBase = NULL;
public:
    SQLiteBridge(std::string dbFileName);
    ~SQLiteBridge();
    
    std::string execSql(std::string sql);
};
#endif

SQLiteBridge.cpp

#include "SQLiteBridge.h"

#include "cocos2d.h"
#include "sqlite3.h"
#include "picojson.h"

using namespace cocos2d;
using namespace std;


SQLiteBridge::SQLiteBridge(std::string dbFileName)
{
    char *errorMessage;
    
    //DBファイルの保存先のパス
    auto filePath = FileUtils::getInstance()->getWritablePath();
    filePath.append(dbFileName);
    
    auto status = sqlite3_open(filePath.c_str(), &this->useDataBase);
    if (status != SQLITE_OK){
        CCLOG("open failed : %s", errorMessage);
        return;
    }
}

SQLiteBridge::~SQLiteBridge()
{
    CCLOG("SQLiteBridge::~SQLiteBridge");
    sqlite3_close(useDataBase);
}

static int execSqlCallback(void *data, int argc, char **argv, char **azColName){
    picojson::array *resultArray = (picojson::array *)data;
    picojson::object obj;
    int i;
    for(i=0; i<argc; i++){
        obj.insert(std::make_pair(azColName[i], picojson::value(argv[i])));
    }
    resultArray->push_back(picojson::value(obj));
    return 0;
}

std::string SQLiteBridge::execSql(std::string sql)
{
    CCLOG("SQLiteBridge::execSql start : %s", sql.c_str());
    char *errorMessage;
    
    picojson::object result;
    picojson::array resultArray;
    
    auto status = sqlite3_exec(useDataBase, sql.c_str(), execSqlCallback, &resultArray, &errorMessage);
    result.insert(std::make_pair("status", picojson::value(double(status))));
    if( status != SQLITE_OK ){
        CCLOG("execSql failed : %s", errorMessage);
        result.insert(std::make_pair("errorMessage", picojson::value(errorMessage)));
    }else{
        result.insert(std::make_pair("result", picojson::value(resultArray)));
    }
    picojson::value json = picojson::value(result);
    return json.serialize();
}

使い方

        var accesser = new jsb.SQLiteBridge("My.db");

        var sql1 = "CREATE TABLE hoge (id integer primary key autoincrement);";
        var ret1 = accesser.execSql(sql1);
        var json1 = JSON.parse(ret1);
        if(json1.status !== 0){
            cc.log("error!!!");
            return;
        }

        var sql2 = "SELECT count(*) as cnt FROM hoge";
        var ret2 = accesser.execSql(sql2);
        var json2 = JSON.parse(ret2);
        if(json2.status !== 0){
            cc.log("error!!!");
            return;
        }
        cc.log("cnt >>> " + json2.result[0].cnt);

Cocos2d-JSでBGM・効果音をpreload

Cocos2d-JSネイティブアプリ(not ブラウザ)で音声ファイルを再生する時のメモ。
音声ファイルは下記のコードで再生できるが、初回実行時に若干遅れて聞こえる。。。

cc.audioEngine.playEffect(mp3file);

調べてみるとC++で言うところの
SimpleAudioEngine::preloadEffect
なる処理を予めやっておかないと初回読み込みに時間がかかってしまうらしい。

同様の処理をCocos2d-JSで出来ないか調べてみた。

結論から言うと、事前に下記の処理を実行しておけば良いっぽい

cc.audioEngine.preloadEffect(mp3file);
おまけ(調べた過程)

公式ドキュメント
http://www.cocos2d-x.org/reference/html5-js/V3.0/index.html
のcc.audioEngineにはそれらしきメソッドは記載されていなかった。。。
(おそらくブラウザ環境と互換性が無いため?)

cocos2d-js-v3.0/samples/js-tests/src/
配下のサンプルにも無い。。。

こういう時は黙ってソースを読むしか無いw

tools/tojs/cocos2dx.ini
に下記の記載を発見

rename_functions = ...
        SimpleAudioEngine::[preloadBackgroundMusic=preloadMusic setBackgroundMusicVolume=setMusicVolume getBackgroundMusicVolume=getMusicVolume playBackgroundMusic=playMusic stopBackgroundMusic=stopMusic pauseBackgroundMusic=pauseMusic resumeBackgroundMusic=resumeMusic rewindBackgroundMusic=rewindMusic isBackgroundMusicPlaying=isMusicPlaying willPlayBackgroundMusic=willPlayMusic],

...
rename_classes = ...
    SimpleAudioEngine::AudioEngine,

クラス名、メソッド名共に別名が付けられているっぽい!

そして

frameworks/js-bindings/bindings/script/jsb_boot.js

内で、

cc.audioEngine = cc.AudioEngine.getInstance();

されてる、って感じで結論になりました。

Cocos2d-JSでカスタムttfフォントを使用する

準備

iosの場合は下記の記事にあるように

http://kyokomi.hatenablog.com/entries/2014/01/07

Fonts provided by application を追加する必要がある。

設置方法

下記のような感じ。

ios androidでフォントの指定方法が異なるので注意

        // テキスト配置
        var label;
        if(cc.sys.OS_ANDROID === cc.sys.os){
            label = cc.LabelTTF.create("Alignment 0\nnew line\n!!", "res/fonts/Abduction.ttf", 24);
        } else {
            label = cc.LabelTTF.create("Alignment 0\nnew line\n!!", "Abduction", 24);
        }
        some_layer.addChild(label, 127, "label");
        label.color = cc.color(0, 0, 0);
        label.x = 640;
        label.y = 500;
        label.opacity = 255;