sqlite3 C++ラッパーライブラリ「SQLiteCpp」の使い方

りやさんです。
今日はsqlite c++ラッパーライブラリ「SQLiteCpp」を紹介しようと思います。

ゲームではマスターデータ、ユーザーデータ管理のために、ソースコード埋め込みではなくリソースファイルとしてデータを管理することが多いと思います。
形式が何であれ、XML、JSON、バイナリ、SQL…いろいろ選択肢はあります。

今回はSQLについて取り扱っていこうと思います。

SQLは膨大なデータを扱うのに優れていて、スレッドセーフであり、データの検索がとてもしやすい形式です。

C++カジュアルゲームでSQLを使う場合、ライブラリ単体で動作するSQLiteがオススメです。

ただ、公式のC言語向けSQLiteライブラリは、C言語ゆえの手続きじみた面倒な処理を書かなくてはなりません。

sqlite3* db;
int status = sqlite3_open_v2("filename", &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
if (status != SQLITE_OK){
  std::cout << "open failed :" << std::endl;
  exit(1);
}

const char* query = "CREATE TABLE IF EXISTS test";
char* err;
int status = sqlite3_exec(db, query, NULL, NULL, &err);
if (status != SQLITE_OK){
  std::cout << "query failed :" << err << std::endl;
  exit(1);
}
sqlite3_close(db);

openしてclose。ポインタのポインタを渡したり、どこか懐かしい構文です。

prepareでクエリの最適化やバインディングを行う際は

const char* query = "select * from value where id = ?";
sqlite3_stmt *stmt = NULL;
sqlite3_prepare(db, query, sql.size(), &stmt, NULL);
    
sqlite3_reset(stmt);

sqlite3_bind_int(stmt, 0,  0);

if(SQLITE_ROW == sqlite3_step(stmt)){
        std::cout << sqlite3_column_int(stmt, 0) << std::endl;;
}
sqlite3_finalize(stmt);

うーん面倒くさい。
プログラマは怠惰で忘れっぽい生き物なので、こういうコードを書いてるといつか破綻しそうです。
オブジェクト指向っぽく書きたい。

そんな人のために、SQLiteには多くのC++ラッパーライブラリが存在します。

その中の一つ、「SQLiteCpp」を紹介します。

SQLiteCpp

非常にシンプルに作られており、とても使い易いライブラリです。

cmakeでビルドする方法が記載されていますが、xcodeではもっと簡単にプロジェクトに組み込めます。

SQLiteCppフォルダをプロジェクトに配置し
SQLiteCpp/include をHeader Serch Pathsに追加して
SQLiteCpp/src/*.cpp、SQLiteCpp/sqlite3.cファイルすべてをCompile Sourcesに追加するだけです。

SQLiteCppの使い方

インスタンスの作り方

#include <SQLiteCpp/SQLiteCpp.h>

int main() {
try {
  SQLite::Database    db("filename");
}
catch (std::exception& e) {
        std::cout << "exception: " << e.what() << std::endl;
}
  return 0;
}

コンストラクタやクエリは例外を投げるのでtry-catchした方がいいです。
コンストラクタは第4引数まで持ちます。

第一引数:ファイル名
第二引数:オープンタイプ

OPEN_READONLY : 読み込み専用
OPEN_READWRITE : 読み書き
OPEN_CREATE : なかったら作る
OPEN_URI : URIも許可する

複数のタイプを組み合わせることができます。(例:SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE)
デフォルト値: OPEN_READONLY

第三引数:データベースがロックされている時のタイムアウト時間[ms]
デフォルト値:0

第四引数: VFSの設定
独自のファイルシステムを指定する際に使う。
デフォルト値:空文字列

成功したかどうかさえわかれば良いタイプのクエリを送る(例: CREATE TABLE)

int nb = db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)");

返り値:SQL文で変更された行の数

返り値でSQLITE_OKはかえってきません。
内部でSQLITE_OKかどうかをチェックし、例外を投げる機構が作られてるので、OKチェックをしたい場合はtry-catchしてください。

トランザクションの作成

明示的にトランザクションを使う場合はTransactionクラスを使います。

SQLite::Transaction transaction(db);
        
/* dbに関する操作 */

transaction.commit();

もしcommitせずにtransactionインスタンスが破棄された場合、デストラクタでロールバックが行われます。

データを取ってくるクエリ

Statementクラスを使います。

SQLite::Statement   query(db, "SELECT * FROM test WHERE id >= ?");
query.bind(1, 0);
while (query.executeStep()){
   int         id     = query.getColumn(0);
   std::string text   = query.getColumn(1);
   std::cout << "row: " << id << ", " << text << std::endl;
}

エラーをとりたい場合は同様にtry-catchします。

バインドはプレースホルダ「?」を使って行います。
プレースホルダインデックスは1から始まります。注意してください(本家sqlite3は0から)

getColumn関数は対応するインデックスのデータを取得しますが、返り値がColumnというデータラッパクラスです。
プリミティブ型になら何でもキャスト出来る型なので、インデックス等をよく確認するか、isIntegerメソッド等を利用するとデバッグに便利かもしれません。
非explicitキャスト演算子がオーバーロードされているため、以下の3文は等価です。

id      = query.getColumn(0);
id      = static_cast(query.getColumn(0));
id      = (int)query.getColumn(0);

1行目が暗黙キャスト、2行目がC++風キャスト、3行目がC風キャスト。
すべて内部でgetIntメソッドを呼びます。
他の型も同様です。

また、bindは内部でprepareしてくれます。
内部で扱われてるsqlite_stmtラッパクラスのコンストラクタとデストラクタが、prepare-rest-finalizeをやってくれてるからです。

こんなところでしょうか。

他にもexecAndGetとかisOkとかありますが、結局組み合わせだったり単なる問い合わせやGetterなので解説の必要はないでしょう。

このように、使いづらいC言語ライブラリでも、C++でラップすることによってたちまち使いやすくなります。
オブジェクト指向言語は偉大ですね。

今回はこの辺で。

Cocos2d-xでGame Center(GameKit)、Google Play Games Servicesを使ってランキング機能を実装する

どうも、ころさめです
今回はCocos2d-xでゲーム内のランキング機能を実装したので、メモがてらここに貼っつけていきます
LobiなんかもCocos2d-xに対応していたりするので良いと思いますが、今回のアプリはカジュアルゲームだったので、ユーザー登録が必要ないGame CenterとPlay Games Servicesにしました
また、ここでは実装のみ紹介するので、コンソール側の登録などは以下などを参考にしてください

Game Center機能を実装する – Qiita
Google Play Game Servicesをとりあえず使ってみる(登録編) – Qiita

ちなみにログイン完了時のコールバック処理や認証失敗時のエラーダイアログ表示などは行っていないので、それが気になる方は各自で実装をお願いします

各OS共通

RankingController.h

#ifndef RankingController_h
#define RankingController_h


class RankingController {
public:
    
    static RankingController* getInstance();
    static void destroyInstance();
    
    virtual bool isLogin();
    virtual void start();
    virtual void showRanking();
    virtual void updateScore(int score);
    
protected:
    static RankingController* _singletonInstancePointer;
};

#endif /* RankingController_h */

全OS共通のヘッダです
シングルトンパターンを使っているのでどこからでも呼べるようにしています
各メソッドがvirtualなのはiOSでオーバーライドを行うからです

RankingController.cpp

#include "RankingController.h"
#include "cocos2d.h"

RankingController* RankingController::_singletonInstancePointer = nullptr;

#if CC_TARGET_PLATFORM != CC_PLATFORM_IOS
RankingController* RankingController::getInstance()
{
    if (_singletonInstancePointer == nullptr)
    {
        _singletonInstancePointer = new (std::nothrow) RankingController();
    }
    return _singletonInstancePointer;
}
#endif //CC_TARGET_PLATFORM != CC_PLATFORM_IOS

void RankingController::destroyInstance()
{
    delete _singletonInstancePointer;
    _singletonInstancePointer = nullptr;
}

#if CC_TARGET_PLATFORM != CC_PLATFORM_ANDROID

bool RankingController::isLogin() {
    return false;
}

void RankingController::start() {
    
}
void RankingController::showRanking() {
    
}
void RankingController::updateScore(int score) {
    
}
#endif //CC_TARGET_PLATFORM != CC_PLATFORM_ANDROID

全OS共通の実装です
getInstanceはiOSのみRankingControllerを継承したインスタンスを生成するためここでは実装しません
また、Android以外のOSでもビルドが通るように何もしないメソッドを用意しています(iOSはオーバーライドで実装)
AndroidのisLogin等の実装はRankingController_Android.cppで行います

iOS

RankingController_iOS.h

#ifndef RankingController_iOS_h
#define RankingController_iOS_h

#import <UIKit/UIKit.h>
#import <GameKit/GameKit.h>
#include "RankingController.h"

@interface GameCenterController : NSObject<GKGameCenterControllerDelegate> {
    
}

- (void)gameCenterViewControllerDidFinish:(GKGameCenterViewController *)gameCenterViewController;
- (BOOL)isLogin;
- (void)start;
- (void)showRanking;
- (void)updateScore:(NSInteger)score;

@end

class RankingController_iOS : public RankingController {
public:
    RankingController_iOS();
    virtual ~RankingController_iOS() = default;
    
    virtual bool isLogin() override;
    virtual void start() override;
    virtual void showRanking() override;
    virtual void updateScore(int score) override;
    
    GameCenterController* _gameCenterController;
};

#endif /* RankingController_iOS_h */

iOS用のヘッダです
RankingControllerの継承クラスと、Objective-CのGameCenterControllerクラスを宣言しています
GameKitを使用するので、Build PhasesのLink Binary With LibrariesからGameKit.frameworkを追加してください
GameCenterControllerクラスを用意する理由はGKGameCenterControllerDelegateを使用するためです

RankingController_iOS.mm

#include "cocos2d.h"
#if CC_TARGET_PLATFORM == CC_PLATFORM_IOS
#include "RankingController_iOS.h"
#import "AppController.h"

@implementation GameCenterController

static NSString* const LEADER_BOARD_ID = @"XXXXXXXXXXXXXXXXXXXXXX";

- (BOOL)isLogin
{
    GKLocalPlayer* player = [GKLocalPlayer localPlayer];
    return player.isAuthenticated;
}

- (void)start
{
    GKLocalPlayer* player = [GKLocalPlayer localPlayer];
    player.authenticateHandler = ^(UIViewController* ui, NSError* error )
    {
        if( nil != ui )
        {
            AppController* appController = [UIApplication sharedApplication].delegate;
            [appController.viewController presentViewController:ui animated:YES completion:nil];
        }
    };
}

- (void)updateScore:(NSInteger)score
{
    if (![self isLogin]) {
        return;
    }
    
    GKScore* gkScore = [[GKScore alloc] initWithLeaderboardIdentifier:LEADER_BOARD_ID];
    gkScore.value = score;
    [GKScore reportScores:@[gkScore] withCompletionHandler:^(NSError *error) {
        if (error) {
            
        }
    }];
}

- (void)showRanking
{
    if (![self isLogin]) {
        return;
    }
    GKGameCenterViewController *gcView = [GKGameCenterViewController new];
    gcView.gameCenterDelegate = self;
    gcView.viewState = GKGameCenterViewControllerStateLeaderboards;
    gcView.leaderboardTimeScope = GKLeaderboardTimeScopeAllTime;
    gcView.leaderboardIdentifier = LEADER_BOARD_ID;
    AppController* appController = [UIApplication sharedApplication].delegate;
    [appController.viewController  presentViewController:gcView animated:YES completion:nil];
}

- (void)gameCenterViewControllerDidFinish:(GKGameCenterViewController *)gameCenterViewController
{
    [gameCenterViewController dismissViewControllerAnimated:YES completion:nil];
}

@end


RankingController* RankingController::getInstance()
{
    if (_singletonInstancePointer == nullptr)
    {
        _singletonInstancePointer = new (std::nothrow) RankingController_iOS();
    }
    return _singletonInstancePointer;
}

RankingController_iOS::RankingController_iOS() {
    _gameCenterController = [[GameCenterController alloc] init];
}

bool RankingController_iOS::isLogin() {
    return [_gameCenterController isLogin];
}

void RankingController_iOS::start() {
    [_gameCenterController start];
};
void RankingController_iOS::showRanking() {
    [_gameCenterController showRanking];
};
void RankingController_iOS::updateScore(int score) {
    [_gameCenterController updateScore:score];
};

#endif //CC_TARGET_PLATFORM == CC_PLATFORM_IOS

iOS用のObjective-C++の実装部分です
GameCenterControllerでは実際にGameKitを使ってログインやスコアの更新、ランキングの表示を行っています
LEADER_BOARD_IDにはiTunes Connectで設定したリーダーボードのIDを入れてください
スコア送信失敗時や未ログイン時のランキング表示失敗などのエラー表示は特に何もしていないので、必要な場合は何かしらの処理を入れてください
また、ランキングの集計期間を1日や1週間にしたい場合は、GKGameCenterViewControllerのleaderboardTimeScopeをGKLeaderboardTimeScopeTodayやGKLeaderboardTimeScopeWeekに変更してください
RankingController_iOSではGameCenterControllerの生成と呼び出しを行っています
ここでRankingControllerのgetInstanceの実装を行うことで、iOSでのみgetInstance時にRankingController_iOSを生成するようにしています

Android

RankingController_Android.cpp

#include "RankingController.h"
#include "cocos2d.h"
#if CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID
#include <jni.h>
#include "platform/android/jni/JniHelper.h"

#define  CLASS_NAME "org/cocos2dx/cpp/PlayGamesController"

USING_NS_CC;

jobject getObject() {
    JniMethodInfo t;
    bool isHave = JniHelper::getStaticMethodInfo(t,
                                                 CLASS_NAME,
                                                 "getObject",
                                                 "()Ljava/lang/Object;");
    jobject instance;
    if (isHave)
    {
        instance = t.env->CallStaticObjectMethod(t.classID, t.methodID);
    }
    return instance;
}

bool RankingController::isLogin() {
    JniMethodInfo t;
    jobject instance = getObject();
    
    bool isHave = JniHelper::getMethodInfo(t, CLASS_NAME, "isLogin", "()Z");
    
    if (!isHave) {
        return false;
    }
    
    jboolean jIsLogin = t.env->CallBooleanMethod(instance, t.methodID);
    t.env->DeleteLocalRef(t.classID);
    
    return (bool)jIsLogin;
}

void RankingController::start() {
    JniMethodInfo t;
    jobject instance = getObject();
    
    bool isHave = JniHelper::getMethodInfo(t, CLASS_NAME, "start", "()V");
    
    if (!isHave) {
        return;
    }
    
    t.env->CallVoidMethod(instance, t.methodID);
    t.env->DeleteLocalRef(t.classID);
}

void RankingController::showRanking() {
    JniMethodInfo t;
    jobject instance = getObject();
    
    bool isHave = JniHelper::getMethodInfo(t, CLASS_NAME, "showRanking", "()V");
    
    if (!isHave) {
        return;
    }
    
    t.env->CallVoidMethod(instance, t.methodID);
    t.env->DeleteLocalRef(t.classID);
}

void RankingController::updateScore(int score) {
    JniMethodInfo t;
    jobject instance = getObject();
    
    bool isHave = JniHelper::getMethodInfo(t, CLASS_NAME, "updateScore", "(I)V");
    
    if (!isHave) {
        return;
    }
    jint jScore = (jint)score;
    
    t.env->CallVoidMethod(instance, t.methodID, jScore);
    t.env->DeleteLocalRef(t.classID);
}

#endif //CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID

RankingControllerのAndroidでの実装部分です
基本的にはPlayGamesController.javaのメソッドをJNI経由で呼んでいるだけです
PlayGamesControllerはシングルトンなので、staticのメソッドを呼ぶのではなく、PlayGamesControllerのインスタンスを取得してからそのインスタンスのメソッドを呼んでいます

PlayGamesController.java

package org.cocos2dx.cpp;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.games.Games;
import com.google.android.gms.games.GamesActivityResultCodes;

import org.cocos2dx.lib.Cocos2dxActivity;

public class PlayGamesController implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {

    private GoogleApiClient mGoogleApiClient;

    private static final int RESOLUTION_REQUEST_CODE = 1000;
    private static final int LEADER_BOARD_REQUEST_CODE = 1001;

    private static PlayGamesController instance = new PlayGamesController();

    public static PlayGamesController getInstance() {
        return instance;
    }

    public static Object getObject() {
        return instance;
    }

    private PlayGamesController() {
        Context con = Cocos2dxActivity.getContext();
        mGoogleApiClient = new GoogleApiClient.Builder(con)
                .addConnectionCallbacks(this)
                .addOnConnectionFailedListener(this)
                .addApi(Games.API).addScope(Games.SCOPE_GAMES)
                .build();
    }

    public boolean isLogin() {
        return mGoogleApiClient.isConnected();
    }

    public void start() {
        if (isLogin() || mGoogleApiClient.isConnecting()) {
            return;
        }
        mGoogleApiClient.connect();
    }

    public void showRanking() {
        if (!isLogin()) {
            return;
        }
        Cocos2dxActivity activity = (Cocos2dxActivity)Cocos2dxActivity.getContext();
        activity.startActivityForResult(Games.Leaderboards.getLeaderboardIntent(mGoogleApiClient,
                        activity.getString(R.string.leaderboard_)), LEADER_BOARD_REQUEST_CODE);
    }

    public void updateScore(int score) {
        if (!isLogin()) {
            return;
        }
        Context con = Cocos2dxActivity.getContext();
        Games.Leaderboards.submitScore(mGoogleApiClient, con.getString(R.string.leaderboard_), score);
    };

    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == RESOLUTION_REQUEST_CODE) {
            if (resultCode != Activity.RESULT_OK) {
                return;
            }
            if (!mGoogleApiClient.isConnected()) {
                mGoogleApiClient.connect();
            }
        } else if (requestCode == LEADER_BOARD_REQUEST_CODE && resultCode == GamesActivityResultCodes.RESULT_RECONNECT_REQUIRED) {
            mGoogleApiClient.disconnect();
        }
    }

    @Override
    public void onConnected(@Nullable Bundle bundle) {
    }

    @Override
    public void onConnectionSuspended(int i) {
        //自動で再接続するらしいのでここいらないかも
        mGoogleApiClient.connect();
    }

    @Override
    public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
        if (connectionResult.getErrorCode() == ConnectionResult.SIGN_IN_REQUIRED
                && connectionResult.hasResolution()) {
            try {
                Cocos2dxActivity activity = (Cocos2dxActivity)Cocos2dxActivity.getContext();
                connectionResult.startResolutionForResult(activity, RESOLUTION_REQUEST_CODE);
            } catch (IntentSender.SendIntentException e) {
                mGoogleApiClient.connect();
            }
        }
    }
}

AndroidのJava側の実装です
この実装を行う前に、PlayGamesServicesAPIを使うためにapp/build.gradleに以下を追加してください

dependencies {
    compile 'com.google.android.gms:play-services-games:9.4.0'
}

後ろのバージョンはその時のGooglePlayAPIのバージョンに合わせてください
また、GooglePlayDeveloperConsoleで実績を5つ以上、リーダーボードを1つ以上設定した上でリソースを取得し、res/valuesに保存してください
その上で、AndroidManifest.xmlに以下を追加してください

<meta-data android:name="com.google.android.gms.games.APP_ID"
                   android:value="@string/app_id" />

この状態で上記PlayGamesControllerを実装して、R.string.leaderboard_の部分を設定したリーダーボードの名前に書き換えてください
ちなみにこのクラスはActivityではないのでonActivityResultが呼ばれないため、AppActivityに以下を追加してください

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

これらを実装した上で、C++部分で以下のような実装を追加してください

    RankingController::getInstance()->start();
    button1->addClickEventListener([](cocos2d::Ref* ref) {
        RankingController::getInstance()->updateScore(100);
    });
    button2->addClickEventListener([](cocos2d::Ref* ref) {
        RankingController::getInstance()->showRanking();
    });

このようにすれば、iOSであればGameCenter、AndroidであればGooglePlayGamesServicesを使ってランキングの更新、ランキングの表示が行えると思います

Cocos2d-xでgetWritablePath以外のファイルパスを取得する

どうも、ころさめです
前回シェア機能を実装した時に、Androidにおいてシェアする画像の保存先がgetWritablePathだと他のアプリから画像が参照できず、画像のシェアができないといった問題がありました
また、iOSにおいてはデフォルトのgetWritablePathの保存先がDocumentsになっているため、ダウンロードしたファイルをgetWritablePathに保存しているとアプリ申請でリジェクトを食らうといったことがよくあります
なので、保存先のパスを取得する処理を簡単に実装したのでメモがてらここに貼ります

FilePath.h

#ifndef FilePath_h
#define FilePath_h

#include "cocos2d.h"

class FilePath {
public:
    static std::string getDocumentPath(bool isPublic = false);
    static std::string getCachePath(bool isPublic = false);
    
#if CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID
    static std::string getExternalFilesPath();
    static std::string getExternalCachePath();
    
#endif
    
};

#endif /* FilePath_h */

各OS共通のヘッダです
AndroidにおいてはgetDocumentPathは正確にはgetFilesPathあたりの名前が正しいのですが、共通化するためiOSに合わせてます

FilePath_Apple.mm

#include "FilePath.h"
#import <UIKit/UIKit.h>

std::string getPath(NSSearchPathDirectory directory) {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(directory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    std::string strRet = [documentsDirectory UTF8String];
    strRet.append("/");
    return strRet;
}

std::string FilePath::getDocumentPath(bool isPublic) {
    return getPath(NSDocumentDirectory);
}

std::string FilePath::getCachePath(bool isPublic) {
    return getPath(NSCachesDirectory);
}

iOS、Mac用の実装です
getWritablePathの実装をコピペしてNSCachesDirectoryを選べるようにしただけです
isPublic引数はAndroidのためのものなので無視しています

FilePath_Android.cpp

#include "FilePath.h"
#include <jni.h>
#include "platform/android/jni/JniHelper.h"
#include "cocos2d.h"

#define  CLASS_NAME "org/cocos2dx/cpp/FilePath"

USING_NS_CC;

std::string getPath(const char* methodName) {
    JniMethodInfo t;
    std::string str;
    if (JniHelper::getStaticMethodInfo(t, CLASS_NAME, methodName, "()Ljava/lang/String;")) {
        jstring jpath  = (jstring)t.env->CallStaticObjectMethod(t.classID, t.methodID);
        
        const char* npath = t.env->GetStringUTFChars(jpath, NULL);
        str = npath;
        str += "/";
        
        t.env->ReleaseStringUTFChars(jpath, npath);
        t.env->DeleteLocalRef(t.classID);
    }
    return str;
}

std::string FilePath::getDocumentPath(bool isPublic) {
    if (isPublic) {
        return getExternalFilesPath();
    }
    return getPath("getFilesPath");
}

std::string FilePath::getCachePath(bool isPublic) {
    if (isPublic) {
        return getExternalCachePath();
    }
    return getPath("getCachePath");
}

std::string FilePath::getExternalFilesPath() {
    return getPath("getExternalFilesPath");
}

std::string FilePath::getExternalCachePath() {
    return getPath("getExternalCachePath");
}

Android用の実装です
isPublicがtrueならExternalの方のファイルパスを返すようにしています

FilePath.java

package org.cocos2dx.cpp;

import android.content.Context;

import org.cocos2dx.lib.Cocos2dxActivity;

import java.io.File;

public class FilePath {

    public static String getFilesPath() {
        Context con = Cocos2dxActivity.getContext();
        return con.getFilesDir().getAbsolutePath();
    }

    public static String getCachePath() {
        Context con = Cocos2dxActivity.getContext();
        return con.getCacheDir().getAbsolutePath();
    }

    public static String getExternalFilesPath() {
        Context con = Cocos2dxActivity.getContext();
        File file = con.getExternalFilesDir(null);
        if (file != null) {
            return file.getAbsolutePath();
        }
        return "";
    }

    public static String getExternalCachePath() {
        Context con = Cocos2dxActivity.getContext();
        File file = con.getExternalCacheDir();
        if (file != null) {
            return file.getAbsolutePath();
        }
        return "";
    }
}

AndroidのJavaの実装です
Externalのディレクトリがnullだと空白を返しているので注意
ここら辺適当な実装なので気になる方は各自で修正してください
また、Externalのディレクトリに書き込み・読み込みをする場合、以下の権限が必要になるのでAndroidManifest.xmlに以下を追加してください

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

おまけのFilePath.cpp

#include "FilePath.h"
#if CC_TARGET_PLATFORM != CC_PLATFORM_IOS && CC_TARGET_PLATFORM != CC_PLATFORM_MAC && CC_TARGET_PLATFORM != CC_PLATFORM_ANDROID


std::string FilePath::getDocumentPath(bool isPublic) {
    return cocos2d::FileUtils::getInstance()->getWritablePath();
}

std::string FilePath::getCachePath(bool isPublic) {
    return cocos2d::FileUtils::getInstance()->getWritablePath();
}

#endif

iOS、Mac、Android以外の場合の実装です
ビルドを通すためだけのものなので面倒なのでgetWritablePath返しちゃってます

以下この実装を使ったサンプル

        cocos2d::utils::captureScreen([](bool success, std::string filePath) {
            if (success) {
                //前回の記事で実装したシェアのメソッド
                Share::lineSendImage(filePath);
            }
        }, FilePath::getCachePath(true) + "capture.png");

こんな感じでやれば画面のスクリーンショット画像をキャッシュディレクトリに保存してLINEでシェアなんかできたりすると思います

Cocos2d-xでTwitter、Facebook、LINEの簡易的なシェア機能を実装する

どうも、ころさめです
なんかプロジェクトが変わるたびにいつも同じようなコードを書いている気がするので、シェアの機能を実装したついでにここにコードをメモしておきます
ちなみにSDKを使った本格的な実装は面倒だったのでopenURLやintentなどを使った簡易的なものになります
まずは基本のクラスから

Share.h

#ifndef Share_h
#define Share_h

#include <string>

class Share{
public:
    static void facebook(const std::string& message, const std::string& imagePath = "", const std::string& url = "");
    static void twitter(const std::string& message, const std::string& imagePath = "", const std::string& url = "");
    static void lineSendMessage(const std::string& message);
    static void lineSendImage(const std::string& imagePath);
    
private:
    
    static constexpr auto LINE_URL_SCHEME = "line://msg/%s/%s";
    static constexpr auto CONTENT_IMAGE = "image";
    static constexpr auto CONTENT_TEXT = "text";
};

#endif /* Share_h */

とりあえずシェアするものはメッセージと画像とURLにしています
LINEはそれぞれ画像かメッセージかのどちらか片方しか送れないので一つずつ関数を用意しています

Share.cpp

#include "Share.h"
#include "cocos2d.h"
#include <iomanip>

std::string url_encode(const std::string &value) {
    std::ostringstream escaped;
    escaped.fill('0');
    escaped << std::hex;
    
    for (auto i = value.begin(); i != value.end(); ++i) {
        unsigned char c = (*i);
        
        if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
            escaped << c;
            continue;
        }
        
        escaped << '%' << std::setw(2) << int(c);
    }
    
    return escaped.str();
}

void Share::lineSendMessage(const std::string& message) {
    std::string encodedMessage = url_encode(message);
    std::string urlString = cocos2d::StringUtils::format(LINE_URL_SCHEME, CONTENT_TEXT, encodedMessage.c_str());
    cocos2d::Application::getInstance()->openURL(urlString);
}


#if CC_TARGET_PLATFORM != CC_PLATFORM_IOS && CC_TARGET_PLATFORM != CC_PLATFORM_ANDROID

void Share::facebook(const std::string& message, const std::string& imagePath, const std::string& url) {
    CCLOG("This OS is not implemented yet");
}

void Share::twitter(const std::string& message, const std::string& imagePath, const std::string& url) {
    CCLOG("This OS is not implemented yet");
}

void Share::lineSendImage(const std::string& imagePath) {
    CCLOG("This OS is not implemented yet");
}
#endif

LINEのメッセージ送信のみiOSとAndroidで実装を共通化可能なためここで実装しています
LINEのメッセージはURLエンコーディングする必要があるのでエンコードしてからopenURLを叩いています
URLエンコードの実装はほぼStackOverflowからパクっています
iOSとAndroid以外では一応ビルドエラーが出ないようにしています(英語は適当)

ここから下はOSごとの実装になります

Share_iOS.mm

#include "Share.h"
#include "cocos2d.h"
#import "AppController.h"
#import <Social/Social.h>

void SLComposeViewShare(NSString* serviceType, const std::string& message, const std::string& imagePath, const std::string& url) {
    
    SLComposeViewController *composeViewController = [SLComposeViewController composeViewControllerForServiceType:serviceType];
    void (^completion) (SLComposeViewControllerResult result) = ^(SLComposeViewControllerResult result) {
        [composeViewController dismissViewControllerAnimated:YES completion:nil];
    };
    [composeViewController setCompletionHandler:completion];
    [composeViewController setInitialText:[NSString stringWithUTF8String:message.c_str()]];
    if (!imagePath.empty()) {
        UIImage* image = [UIImage imageWithContentsOfFile:[NSString stringWithUTF8String:imagePath.c_str()]];
        [composeViewController addImage:image];
    }
    if (!url.empty()) {
        [composeViewController addURL:[NSURL URLWithString:[NSString stringWithUTF8String:url.c_str()]]];
    }
    
    AppController* appController = [UIApplication sharedApplication].delegate;
    [appController.viewController presentViewController:composeViewController animated:YES completion:Nil];
}


void Share::facebook(const std::string& message, const std::string& imagePath, const std::string& url) {
    SLComposeViewShare(SLServiceTypeFacebook, message, imagePath, url);
}
void Share::twitter(const std::string& message, const std::string& imagePath, const std::string& url) {
    SLComposeViewShare(SLServiceTypeTwitter, message, imagePath, url);
}

void Share::lineSendImage(const std::string& imagePath) {
    UIImage* image = [UIImage imageWithContentsOfFile:[NSString stringWithUTF8String:imagePath.c_str()]];
    
    UIPasteboard *pasteboard;
    
    if ([[UIDevice currentDevice].systemVersion floatValue] >= 7.0) {
        pasteboard = [UIPasteboard generalPasteboard];
    } else {
        pasteboard = [UIPasteboard pasteboardWithUniqueName];
    }
    [pasteboard setImage:image];
    
    std::string urlString = cocos2d::StringUtils::format(LINE_URL_SCHEME, CONTENT_IMAGE, [pasteboard.name UTF8String]);
    cocos2d::Application::getInstance()->openURL(urlString);
}

iOS側のObjective-C++の実装になります
TwitterとFacebookのシェアではSLComposeViewControllerを使っているため、必ずSocial.frameworkをBuild Phasesから追加してください
LINEの画像のシェアはline://のURLスキームでopenURLから行いますが、iOSの場合はペーストボード名をURLに含める必要があります

Share_Android.cpp

#include "Share.h"
#include <jni.h>
#include "platform/android/jni/JniHelper.h"
#include "cocos2d.h"

#define  CLASS_NAME "org/cocos2dx/cpp/Share"

USING_NS_CC;

void Share::facebook(const std::string& message, const std::string& imagePath, const std::string& url) {
    if (imagePath.empty() && url.empty()) {
        //Facebookは画像かURLのどちらかのみシェア可能
        return;
    }
    JniMethodInfo t;
    if (JniHelper::getStaticMethodInfo(t, CLASS_NAME, "facebook", "(Ljava/lang/String;Ljava/lang/String;)V")) {
        jstring jUrl = t.env->NewStringUTF(url.c_str());
        jstring jFilePath = t.env->NewStringUTF(imagePath.c_str());
        
        t.env->CallStaticVoidMethod(t.classID, t.methodID, jFilePath, jUrl);
        
        t.env->DeleteLocalRef(jFilePath);
        t.env->DeleteLocalRef(jUrl);
        t.env->DeleteLocalRef(t.classID);
    }
    
}
void Share::twitter(const std::string& message, const std::string& imagePath, const std::string& url) {
    
    JniMethodInfo t;
    if (JniHelper::getStaticMethodInfo(t, CLASS_NAME, "twitter", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V")) {
        jstring jMessage = t.env->NewStringUTF(message.c_str());
        jstring jFilePath = t.env->NewStringUTF(imagePath.c_str());
        jstring jUrl = t.env->NewStringUTF(url.c_str());
        
        
        t.env->CallStaticVoidMethod(t.classID, t.methodID, jMessage, jFilePath, jUrl);
        
        t.env->DeleteLocalRef(jMessage);
        t.env->DeleteLocalRef(jFilePath);
        t.env->DeleteLocalRef(jUrl);
        t.env->DeleteLocalRef(t.classID);
    }
}

void Share::lineSendImage(const std::string& imagePath) {
    std::string urlString = cocos2d::StringUtils::format(LINE_URL_SCHEME, CONTENT_IMAGE, imagePath.c_str());
    cocos2d::Application::getInstance()->openURL(urlString);
}

Android側のJNI他の実装になります
このファイルはiOSのビルド対象に含めないでください
AndroidのTwitterとFacebookのシェアはIntentを使ってシェアしますが、Facebookはメッセージのシェアは行えないので、画像かURLがあった場合のみシェアを行います
AndroidのLINEはURLにファイルパスを含めるだけで画像のシェアを行えます
ただ、Androidの画像のシェアはgetWritablePathで取得するファイルパスが他のアプリから参照できないため、Cocos2d-xで保存した画像をシェアする場合はファイルパスを工夫する必要があります

Share.java

package org.cocos2dx.cpp;

import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.webkit.MimeTypeMap;

import org.cocos2dx.lib.Cocos2dxActivity;

import java.io.File;

public class Share {

    public static void twitter(String message, String filePath, String url) {
        Context con = Cocos2dxActivity.getContext();

        Intent intent = new Intent(Intent.ACTION_SEND);

        File file = new File(filePath);
        if (file.exists()) {
            int index = file.getName().lastIndexOf('.');
            String ext = (index >= 0) ? file.getName().substring(index + 1).toLowerCase() : "";
            String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
            intent.setType(mimeType);
            intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file));
        } else {
            intent.setType("text/plain");
        }

        if (url.isEmpty()) {
            intent.putExtra(Intent.EXTRA_TEXT, message);
        } else {
            intent.putExtra(Intent.EXTRA_TEXT, message + " " + url);
        }
        intent.setPackage("com.twitter.android");

        con.startActivity(intent);
    }


    public static void facebook(String filePath, String url) {
        Context con = Cocos2dxActivity.getContext();

        Intent intent = new Intent(Intent.ACTION_SEND);

        File file = new File(filePath);
        if (file.exists()) {
            int index = file.getName().lastIndexOf('.');
            String ext = (index >= 0) ? file.getName().substring(index + 1).toLowerCase() : "";
            String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
            intent.setType(mimeType);
            intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file));
        } else {
            intent.setType("text/plain");
            intent.putExtra(Intent.EXTRA_TEXT, url);
        }

        intent.setPackage("com.facebook.katana");

        con.startActivity(intent);
    }
}

AndroidのJNIで呼ぶJavaの実装になります
TwitterはURLがある場合はメッセージの後ろに空白を入れてURLを入れています
Facebookは画像とURLが両方ある場合は画像を優先してシェアするようにしています

これで一応一通りシェアが可能になるかと思います
最初はgithubで公開しようかと思ったのですが、既に似たようなものがあったのでやめました
Androidの画像のファイル保存先の取得については次の記事でやろうと思います

階層型ステートマシンによるステートデザインパターン

新卒のりやさんです。

こいつブログばっか書いてんな、もしかして暇なのか? とか思わないでください。

今回は、ゲームプログラミングの中でもとても重要なデザインパターン、ステートデザインパターンについて紹介しようかと思います。

通常、クラスというものは「属性」と「振る舞い」の二種類の要素を兼ね備えています。
一般に、データとインターフェースと呼ばれるものですね。

クラスは自身の状態を保持し、きっとカプセル化されたデータに応じて要求された操作に対し適切な振る舞いをすることでしょう。
そのようなコードは列挙体とswitch-caseで簡単に表現できます。

例として、村によくいる住人のオブジェクトを考えます。
住人は、走ったり、歩いたり、或いは居眠りをする挙動を考えます。

class NPC {
  enum class State {
    WALK,
    RUN,
    SLEEP
  }

/*-----適当な実装-----*/

  void update(float deltaTime);
  void changeState();

  private:
    State _state;
    float _elapsedTime;

    static constexpr float CHANGE_STATE_TIME = 50;
};

void NPC::update(float deltaTime) {
  _elapsedTime += deltaTime;
  if(_elapsedTime >= CHANGE_STATE_TIME) {
    _elapsedTime -= CHANGE_STATE_TIME;
    changeState();
  }

  switch(_state){
    case State::WALK:
    //歩きます
    break;
    case State::RUN:
  //走ります
    break;
    case State::SLEEP:
    //眠ります
    break;
  }
}

void NPC::changeState() {
  switch(_state){
    case State::WALK:
    _state = State::SLEEP; //歩き疲れたので眠ります
    break;
    case State::RUN:
  _state = State::WALK; //走り疲れたので歩きます
    break;
    case State::SLEEP:
    _state = State::RUN; //眠って元気が出たので走ります
    break;
  }
}

この程度のコードならまあ、これでいいんでしょうが、あまりにも拡張性に欠けます。
状態(State)は今3つだけですが、当然追加が発生することもありますし、遷移の順番も変わるかもしれません。

さらに、状態というものはネストすることがあります。例えば、「機嫌が良い/悪い」という状態が加わったらどうでしょう。「歩いている」と「機嫌が良い」は同時に発生しうる状態であり、両方とも別変数で管理し、switch-caseをネストさせて条件分岐させなくてはなりません。

//地獄のswitch-caseのネスト
switch(_state1){
    case State1::WALK:
    switch(_state2) {
        case State2::FeelBad:
        /*...*/
        break;
        case State2::FeelGood:
        /*...*/
    }
    break;
    /*...*/
  }

そんなswitch-caseのコードのメンテナンスをしていると、とんでもないことになります(なりました)。

具体的にどうするべきかというと、「状態」というものをクラスにしカプセル化しましょう。
NPCクラス自身が次の状態は何か、今どうするべきかを判断する構造は良くないです。

class StateBase {
  StateBase():_next(nullptr){}
  virtual StateBase* update(float deltaTime) = 0;
  /* --- 適当な実装 --- */
  private:
  StateBase* _next;
};

class Walk : public StateBase {
  StateBase* update(float deltaTime) override {
    /* ある条件下で_nextを更新 */
    return _next;
  }
};

class Run : public StateBase {
  //同様
};

class Sleep : public StateBase {
  //同様
};

class NPC {
void update(float deltaTime);
private:
  StateBase* _current;
};

void NPC::update(float deltaTime) {
  auto next = _current->update();
  if(next) {
   changeState(next); 
  }
}

NPCクラスが呼び出すのはStateのupdateだけ。
あとは勝手に状態遷移をやってくれそうな感じです。

ステートデザインパターンとしてはまだまだ実装すべき項目がありますが、コンセプトとしてはこの方針で問題なさそうです。

私が作成するステートマシンの基本要件は以下。

 

ステートマシンクラス

      1. ステートマシンは必ず「現在のステート」を持つ

2. ステートマシンは階層構造になっており、ステートマシンには親ステートマシンや子ステートマシンが存在する場合がある

3. ステートマシンは特定の間隔(ゲームであるならば、毎フレームが望ましい)でステートに次の遷移先ステートを問い合わせ、状態が更新される

4. 遷移先ステートが同一である場合、遷移は行わないと定義する(本来ステートマシンの元となる有限オートマトンにおいては、この操作は「自己遷移」と呼ばれるものだが、省略する)

5. ステートの遷移が行われた場合、遷移前のステートのイグジット処理と、遷移後のステートのエントリー処理を実行する

6. ステートマシンはイベントの通知を受けた時、現在のステートに処理すべきかどうか問い合わせる。この操作は、イベント処理するステートが見つかるまで子ステートマシンより再帰的に行われる

ステートクラス

      1. ステートは「次のステート」を持つ

2. ステートは「イベント」と「アクション/トランジション」のマップを持つ。ステートマシンより問い合わせが来た時、該当するイベントアクションが存在する場合、実行する

3. アクションは関数の実行、トランジションは遷移の発生である。ただし、拡張性のため、トランジションにはファクトリデザインパターンを採用する

 

そんなこんなで、実装したステートマシンが以下.
(業務のために本格的に作成したものですので、少し大規模になっています)

//HFStateMachine.hpp
#include "StateInterface.hpp"
#include "StateMachineUtils.hpp"
#include <memory>
#include <vector>
#include <map>

/* ステートマシンのベースクラスです ContextTypeは継承したクラスを渡します */
template <class ContextType, class Event>
class HFStateMachine : public std::enable_shared_from_this<HFStateMachine<ContextType, Event>>
{
public:
    typedef ContextType context_t;
    typedef HFStateBase<context_t, Event> state_t;
    typedef SMUtils<context_t, Event> Utils;
    typedef actionInterface<context_t> action_t;
    typedef factoryInterface<context_t> factory_t;
    friend std::shared_ptr<context_t>;
    
/*ステートマシンの生成はこの関数を使います(コンストラクタは不可)*/
    static std::shared_ptr<context_t> create(state_t* firstState) {
        auto context = std::make_shared<context_t>(new typename Utils::start);
        context->changeState(std::shared_ptr<state_t>(firstState));
        return context;
    }
    
    void setParent(std::shared_ptr<context_t> parent) {
        _parent = parent;
    }
    
    void addChild(std::shared_ptr<context_t> child) {
        child->setParent(derived());
        _children.push_back(child);
    }
    
    std::weak_ptr<context_t>& getParent() {
        return _parent;
    }
    
    const std::vector<std::shared_ptr<context_t>>& getChildren() {
        return _children;
    }
    
    const std::shared_ptr<state_t>& getState() {
        return _current;
    }
    
    void removeFromParent() {
        for(auto &&child: _children) {
            child->removeFromParent();
        }
        if(!_parent.expired()) {
            _parent.lock()->removeChild(derived());
        }
    }
    
    void removeChild(std::shared_ptr<context_t> child) {
        auto removeIt = std::find(_children.begin(), _children.end(), child);
        if(removeIt != _children.end()) {
            _children.erase(removeIt);
        }
    }
    
    void removeChildren() {
        _children.clear();
    }
    
/* このupdate関数は外側から呼び出してください */
    void update(float deltaTime)
    {
/* ステートに問い合わせします */
        const auto& _next = _current->update(deltaTime);
        changeState(_next);
        for(auto &&child: _children) {
            child->update(deltaTime);
        }
    }
    
    void changeState(const std::shared_ptr<state_t>& destState)
    {
        if(!destState) {
            return;
        }
        /* 自己遷移は何もしません */
        if( _current == destState ) {
            return;
        }
        /* ステートの遷移を行います */       
        _current->exit();
        _current->setNext(nullptr);
        
        removeChildren();
        
        destState->setNext(destState);
        destState->setContext(derived());
        
        _current = destState;
        
        destState->entry(derived());
    }
    
    std::shared_ptr<ContextType> derived()
    {
        return std::static_pointer_cast<ContextType>(this->shared_from_this());
    }
    
    bool dispatchEvent(const Event& e) {
        bool isSwallowed = false;
        /* 子ステートマシンに先にイベントを伝播します */
        if(!_children.empty()){
            for(auto &&child: _children){
                isSwallowed |= child->dispatchEvent(e);
            }
        }
        /* 子ステートマシンでイベントが処理されなかった場合 */
        if(!isSwallowed) {
            return _current->processEvent(e);
        }
        return false;
    }
    
    void end() {
        _current->setNext(new struct Utils::end);
    }
    
protected:
    std::shared_ptr<state_t> _current;
    std::weak_ptr<context_t> _parent;
    
    std::vector<std::shared_ptr<context_t>> _children;
    
    HFStateMachine(state_t* firstState)
    : _current(firstState)
    {
        
    }
    virtual ~HFStateMachine() {
        _current->exit();
    }
};

//StateInterface.hpp

#include "../SpiralLibrary/Utility/TupleUtils.hpp"
#include <map>
#include <memory>
#include <type_traits>

/* アクションはこのインターフェースを継承します */
template <class ContextType>
class actionInterface;

template <class ContextType>
class actionInterface {
public:
    virtual void exe(const std::shared_ptr<ContextType>& context) = 0;
};

/* トランジションはこのインターフェースを継承します */
template <class ContextType>
class factoryInterface {
public:
    virtual std::shared_ptr<typename ContextType::state_t> create(const std::shared_ptr<ContextType>& context) = 0;
};

/* トランジションが単純create関数の場合はこのヘルパを使います */
template<class ContextType, class State, bool hasDefaultConstructor, class... Args>
class _factoryDefault;


template<class ContextType, class State, class... Args>
using factoryDefault = _factoryDefault<ContextType, State, std::is_default_constructible<State>::value, Args...>;

/* デフォルトコンストラクタ版 */
template<class ContextType, class State, class... Args>
class _factoryDefault<ContextType, State, true, Args...> : public factoryInterface<ContextType>{
public:
    std::shared_ptr<typename ContextType::state_t> create(const std::shared_ptr<ContextType>& context) {
        return std::make_shared<State>();
    }
};

/* ユーザーコンストラクタ版 */
template<class ContextType, class State, class... Args>
class _factoryDefault<ContextType, State, false, Args...> : public factoryInterface<ContextType>{
public:
    _factoryDefault(Args... args) : _arguments(std::tie(args...)){}
    std::shared_ptr<typename ContextType::state_t> create(const std::shared_ptr<ContextType>& context) {
        return libspiral::tupleVariadicApply(static_cast<std::shared_ptr<State> (&)(Args&...)>(std::make_shared<State>), _arguments);
    }
    std::tuple<Args...> _arguments;
};

/* イベントはこのベースクラスを継承します */
template<class Enumration>
class EventBase {
public:
    EventBase(Enumration value) : _value(value) {}
    
    bool operator==(EventBase const& lhs)const;
    bool operator<(EventBase const& lhs)const;
    
private:
    Enumration _value;
};

template<class Enumration>
bool EventBase<Enumration>::operator==(EventBase const& lhs)const{
    return _value == lhs._value;
}

template<class Enumration>
bool EventBase<Enumration>::operator<(EventBase const& lhs)const{
    return _value < lhs._value;
}

/* ステートのベースクラスです */
template < class ContextType, class Event >
class HFStateBase : public std::enable_shared_from_this<HFStateBase<ContextType, Event>>
{
public:
    typedef ContextType context_t;
    typedef HFStateBase state_t;
    typedef actionInterface<ContextType> action_t;
    typedef factoryInterface<ContextType> factory_t;
    typedef std::map<Event, std::shared_ptr<factory_t>> transition_map;
    typedef std::map<Event, std::shared_ptr<action_t>> action_map;
    
    HFStateBase(){}
    virtual ~HFStateBase() = default;
    /* 遷移のエントリー処理です 必要に応じてオーバーライドしてください */
    virtual void entry(const std::shared_ptr<ContextType>& context){}
    
    /* 遷移のイグジット処理です 必要に応じてオーバーライドしてください */
    virtual void exit(){}
    
    /* 遷移先ステートの問い合わせです。また、経過時間を取得したい場合はこの関数をオーバーライドしますが、要件を満たすように記述してください */
    virtual std::shared_ptr<HFStateBase> update(float deltaTime) {
        if(_next) {
            return _next;
        }
        return this->shared_from_this();
    }
    
    void setContext(const std::shared_ptr<context_t>& context) {
        _context = context;
    }
    
    std::weak_ptr<context_t> getContext() {
        return _context;
    }
    
    void setNext(const std::shared_ptr<state_t>& state) {
        _next = state;
    }
    
   /* トランジションとイベントのペアを登録します */
    void addTransition(const Event& e, factory_t* factory) {
        _transition.insert(std::make_pair(e, std::shared_ptr<factory_t>(factory)));
    }

    /* アクションとイベントのペアを登録します */
    void addAction(const Event& e, action_t* act) { 
        _action.insert(std::make_pair(e, std::shared_ptr<action_t>(act)) ); 
    }
    
    /* イベントに対応するアクション、トランジションを追加します */
    bool processEvent(const Event& e) {
        auto itAction = _action.find(e);
        auto itTrans = _transition.find(e);
        
        if(itAction == _action.end() && itTrans == _transition.end()) {
            return false;
        }
        
        if(itAction != _action.end()) {
            itAction->second->exe(_context.lock());
        }
        if(itTrans != _transition.end()) {
            _next =  itTrans->second->create(_context.lock());
        }
        return true;
    }
    
    
    
protected:
    std::shared_ptr<state_t> _next;
    std::weak_ptr<context_t> _context;
    transition_map _transition;
    action_map _action;
    
};

//StateMachineUtils.hpp
#include "StateInterface.hpp"

template<class context_t, class event_t>
struct SMUtils {
    typedef actionInterface<context_t> action_t;
    typedef HFStateBase<context_t, event_t> state_t;
public:
    struct releaseChildMachines : public action_t {
        void exe(const std::shared_ptr<context_t>& context) {
            context->removeChildren();
        }
    };
    
    struct start : public state_t {
        
    };
    
    struct end : public state_t{
        void entry(const std::shared_ptr<context_t>& context) override{
            context->removeChildren();
            context->removeFromParent();
        }
    };
};

ステートマシンはupdate関数によってステートの状態を毎回チェックしています。
返ってきたステートが同一ではない場合、ステートの遷移を行います。
その際、ステートのentry関数とexit関数を実行します。

また、dispatchEvent関数によってイベントの処理を行います。
子ステートマシンに先に問い合わせた後、イベントが処理されるまで再帰的に実行されます。

また、メモリの安全性の考慮し、スマートポインタを使用します。

ステートクラスはアクションマップとトランジションマップを持ち、該当するイベントをprocessEvent関数によって実行します。
_nextメンバは次の遷移先ステートを表しており、update関数によって次のステートの問い合わせを受けた場合、_nextが存在するなら_nextを、存在しないならば自身を返します。

こんな感じで、実際にNPCが歩くだけのプログラムを作ってみました。
リポジトリを置いとくので、ステートマシンの使い方の参考がてらどうぞ!
cocos2dx v3.11が必要です。

https://github.com/Riyaaaaa/StateMachineSample

実行結果

ZjItNIl46X

こいつ…動くぞ!

素材提供者 直江ジャスティス様
(https://twitter.com/justicenaoe)

【Android】CrystaX NDKでgcc5を使いC++14のコードをコンパイルする【Cocos2dx】

どうも、ぺーぺーの新人のりやさんです

ちょこっとCosos2dxのゲームのAIを作る上でコンパイル時計算を使っていたら

コンパイラにconstexpr関数でnot return statementを使っていると怒られてしまいました。

Android NDKのgccにおいてc++14の機能は使えるものと使えないものがあることがわかり、gccのバージョンを調べてみると

なんと4.9! 最新のAndroid NDKでもこれは変わらず!

Relaxing requirements on constexpr functions:(constexpr関数の制限緩和)

に対応しているのはgcc5からなんですよね…

この機能によってconstexpr関数は

  • globally-visible side-effects, such as modification of an object of static storage duration (other than the object being constructed, if any);

  • expressions which cannot be evaluated during translation, such as dynamic memory allocation, comparisons with unspecified results and lvalue-to-rvalue conversions on objects which are neither constant nor created during the evaluation;

  • non-portable constructs, such as an attempt to violate type-safety or to inspect the underlying storage of the abstract machine (for instance, through a reinterpret_cast), or use of inline asm;

  • invocation of a function which is not constexpr; or

  • undefined behavior.

<http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3597.htmlより引用>

(グローバル、スタティックオブジェクトへのアクセス/動的割り当て/型安全性に違反する行為(reinterpret_cast等)、インラインアセンブラの利用/非constexpr関数の呼び出し/未定義動作)
以上に該当しない場合はコンパイル時に評価されます。

つまり副作用がなければ何でもあり。

逆にC++11仕様の場合、関数内にはreturn statement以外かけません。forやwhileは使えないので再帰を駆使、コンテナは独自のイテレータを使う、参照を取れないので複数の値はタプルで返す等、地獄の実装になります。

絶対Cocos2dxでC++14コードをAndroid向けビルドしたい! そんな時は

Android NDKを消しとばしてCrystaX NDKを使いましょう! CrystaX NDK ならgcc5.3に対応しています! しかもgcc6への対応も意欲的!

ただし、推奨環境ではないので色々弄る必要があります。

 

MacOSX 環境でのCocos2dx向けCrystaX構築方法

1. まずbrewを用いてCrystaXをインストールします。

ターミナルを開きます。brewは適宜インストールしてください。

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" < /dev/null 2> /dev/null

brew install crystax-ndk

2. ~/.bash_profileを弄ります。

このプロファイルには、皆さんがcocos2dxのsetup.pyを実行した時に入力したNDK_ROOTなるものがexportされているはずです。

AndroidNDKからCrystaXに変えましょう。

export NDK_ROOT=/usr/local/Cellar/crystax-ndk/<version>

 

この辺は環境依存なので自分のbrewのパッケージ保管場所を入力してください。

 

3. android-studioの設定をいじります。(既存のプロジェクトがある場合)

現在のcocos2dxプロジェクトを開いて、タブのFile->Project Structure->SDK Locatinに飛び、Android NDK locationに先ほどと同じパスを入力します。

 

4. コンパイラのバージョンを指定します。
cocos2dxはAndroidNDKを想定しているので、toolchainを4.9に指定しています。

cocos2d-x-3.11.1/tools/cocos2d-console/plugins/plugin_compile/build_android.py

を開き、206行目あたりにある

for dir_name in dir_names:

if dir_name.endswith('4.9'):

return 4.9

を4.9から5に書き換えます。

for dir_name in dir_names:

if dir_name.endswith('5'):

return 5

環境によってはまずい感じの変更ですが、慈悲はなし。C++14道は険しいのだ。

 

5. cocos2dxのフレームワークでなぜかビルドエラーが出るので修正します。

YOUR_PROJECT_ROOT/cocos/uiUIWebViewImpl-android.h/cppにおいて

uint32_tがビルドで死ぬので、全部intに書き変えましょう。これもまずそうな変更ですが、おそらく今時32bit環境のAndroidなんてないと思います! きっとintは4byteですよ…

 

さて、ここまでやれば

cocos run -p android –android-studio

で実行です!ビルドが通れば勝ち!

もしerrorが出てきても大丈夫。環境が変わりシンボルリンクがかなり厳しくなります。今errorを吐いているSTLは本当に必要なヘッダをincludeしていますか? functional, type_traits, utility等、使ったSTLの所属するヘッダはしっかりincludeしましょう。

 

return statement以外を持つconstexpr関数を書いてみたり、変数テンプレートを使ってみたり(gcc 5から対応した機能)して、gccのメジャーバージョンが5になってるか確認してみてくださいね。

(ちなみにCrystaX NDKにおけるconstexpr関数の挙動がおかしい感じ…誰か調査してください)

今回はこの辺で!

Cocos2d-x(JS)でCocosStudioのcsdファイル内csdファイルのアニメーションを取得、再生する その2

どうも、エンジニアのころさめです
前回からの続きで、今回は作成したCocosStudioのcsdファイル内csdのNodeのアニメーションをコード上で制御する方法を紹介します

まず、Nodeからアニメーション(ccs.ActionTimeline)を取得するために以下のようなメソッドを用意します

var utils = utils || {};

/**
 * @param {cc.Node} node
 * @returns {ccs.ActionTimeline|cc.Action|null}
 */
utils.getActionTimeLine = function (node) {
    return node.getActionByTag(node.getTag());
};

このメソッドを利用して、ccs.loadをしているところで以下のように実装します

var HelloWorldLayer = cc.Layer.extend({
    ctor:function () {
        this._super();

        /** @type {{node: cc.Node, action: cc.Action}} */
        var mainscene = ccs.load(res.MainScene_json);
        this.addChild(mainscene.node);

        /** @type {cc.Node} */
        var rotateIcon = mainscene.node.getChildByName("RotateIcon");
        /** @type {ccs.ActionTimeline} */
        var iconTimeLine = utils.getActionTimeLine(rotateIcon);
        iconTimeLine.play("rotate", true);

        return true;
    }
});

この状態でproject.jsonやresource.jsなどを修正してから起動すると、子要素のアニメーションがループ再生されるのが確認できると思います

なぜこれで子要素のアニメーションが取得できるかというと、ccs.loadをした際に読み込むcsd(json)データが、CocosStudio v2以降のものであれば、timelineParser-2.x.jsが呼ばれ、そこで以下のような処理をしているからになります

    parser.initProjectNode = function(json, resourcePath){
        var projectFile = json["FileData"];
        if(projectFile != null && projectFile["Path"]){
            var file = resourcePath + projectFile["Path"];
            if(cc.loader.getRes(file)){
                var obj = ccs.load(file, resourcePath);
                parser.generalAttributes(obj.node, json);
                if(obj.action && obj.node){
                    obj.action.tag = obj.node.tag;
                    var InnerActionSpeed = json["InnerActionSpeed"];
                    if(InnerActionSpeed !== undefined)
                        obj.action.setTimeSpeed(InnerActionSpeed);
                    obj.node.runAction(obj.action);
                    obj.action.gotoFrameAndPause(0);
                }
                return obj.node;
            } else
                cc.log("%s need to be preloaded", file);
        }
    };

読み込んだactionをnodeのタグと同じにした上でrunActionしているのがわかると思います
そのため、nodeからnodeのタグでgetActionByTagをするとccs.ActionTimelineが取得できるというわけです

これを応用して、以下のようなccs.loadのwrapperを用意すると非常に捗ります

/**
 * @param {string} file
 * @param {string} [path]
 * @returns {cc.Node}
 */
utils.loadCCSNode = function (file, path) {
    var ccsObj = ccs.load(file, path);
    var node = ccsObj.node;
    var action = ccsObj.action;

    if(action && node){
        action.tag = node.tag;
        node.runAction(action);
        action.gotoFrameAndPause(0);
    }
    return node;
};

こうすることにより、nodeとactionを分けずに扱えるため、actionが必要になった時だけutils.getActionTimeLineを呼ぶといった使い方ができるようになるため、オススメです

まだCocos2d-x(JS)とCocosStudioの両方を使って開発する人は少ないですが、慣れればC++やCocosBuilderよりもより早く開発できるようになるので、試してみる価値はあると思います

Cocos2d-x(JS)でCocosStudioのcsdファイル内csdファイルのアニメーションを取得、再生する その1

どうも、スパイラルセンスに入社してから1年半ぐらいのエンジニアのころさめです
直近の仕事がなくなったため社内ニートしようと思ってたらブログを書けと言われて渋々書いております
一応これまでCocos2d-xを使った開発に2年半ほど携わってきたので、私の記事はCocos2d-x関連の記事が多くなると思います

さて、最近まで私はCocos2d-x(JS)とCocos Studioを使って10ヶ月ほど開発を行っていました
ただ、Cocos2d-x(JS)用の新しいエディタであるCocos Creatorが発表されたにも関わらず、これまでにCocos StudioとCocos2d-x(JS)を利用した基本的な実装方法についての情報がweb上にほとんどありませんでした
しかしこの前、ようやく非常に参考になる情報が以下に掲載されました

Cocos2d-x (JS) ハンズオン #5 「Cocos StudioとCocos2d-x (JS)との連携」

基本的な実装方法についてはこれで十分なのですが、ちょっと変わったことをしようとするとどこにも情報はなく、自分で試したりCocos2d-xの実装を確認する必要があります
今回はそのうちの一つ、csdファイル内csdファイルのNodeのアニメーションを取得、再生する方法についてになります

まず、CocosStudioで以下のようにアニメーションを設定したcsdファイルを用意します

Icon

次に、アニメーションを設定したcsdファイルを別のcsdファイル表示時にドラッグ&ドロップします

drag

すると、csdファイル内に別のcsdファイルを子要素のNodeとして持たせることができます
Begin to record frameにチェックを入れて、この子要素のNested Actionという項目を設定すると、親のアニメーションの特定のタイミングで子要素をアニメーションさせることができます

nested_action

しかし、これだと親のアニメーションの時間が子要素より短い場合、子要素のアニメーションが途中で止まってしまったり、クリックイベントのタイミングでアニメーションを再生させたい場合などの時に、複雑なタイムラインになっていまいます

そのため、この子要素のアニメーションの再生をコード上で制御する必要があるのですが・・・長くなったので続きは次の記事で