ページ

2015年6月23日火曜日

Cocos2d-xでタッチの2度押しを防止する方法

はじめに


Cocos2d-xでタッチイベントを発生させたとき、そのイベントで実行するアクションが終了するまでイベントを発生させたくない場合があります。

いわゆる2度押し防止というものですかね。

そのやり方について考えてみたのでメモします。

私が思いついたのは以下の2つです。

  1. アクション実行中はイベントを無効化する。
  2. シーングラフの最上位にイベント制御のための不可視のノードを置く。

サンプルがないと説明しにくいと思ったので作りました(Cocos2d-x v3.5)。


デフォルトのHelloWorld画面に以下のような動作を追加しています。

  • 画面をタップすると画像が180度回転する(最初は右回り)。
  • 再度タップすると逆方向に180度回転する。


2度押し防止処理をしない場合


サンプルは以下です。


アクション実行部分は以下のようになっています。

// HelloWorldScene.cpp
void HelloWorld::onTouchEnded(Touch* touch, Event* event)
{
    log("=== Call HelloWorld::onTouchEnded() ===");
    
    // Create Action
    float deltaAngle = this->getRightFlg() ? 180.0f : -180.0f;
    auto rotateBy = RotateBy::create(2.0f, deltaAngle);
    
    // Run Action
    this->getSprite()->runAction(rotateBy);
    
    // Set RightFlg
    this->setRightFlg(!this->getRightFlg());
}

これだと回転中にタッチすると、そこから逆回転してしまいます。

2度押し防止対策を行い、回転中にはタッチが無効になるようにしたいと思います。

アクション実行中はイベントを無効化する


アクション実行前にイベントリスナーを無効化できれば、二度押し防止が実現できそうです。

イベントリスナーの有効化・無効化はEventListenerクラスから継承しているsetEnabledメソッドで行えます。

アクション実行後にイベントリスナーを再度有効化するのも忘れずに行いましょう。

なお、イベントリスナーはEventDispatcherで登録後に取得する方法が見当たらないため、メンバ変数として保持します。

サンプルは以下です。


ポイント箇所を抜粋します。

// HelloWorldScene.cpp

// ...

bool HelloWorld::init()
{
    // Event Listener
    auto eventListener = EventListenerTouchOneByOne::create();
    eventListener->onTouchBegan = CC_CALLBACK_2(HelloWorld::onTouchBegan, this);
    eventListener->onTouchEnded = CC_CALLBACK_2(HelloWorld::onTouchEnded, this);
    Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority(eventListener, this);
    
    // Set Properties
    // ...
    this->setEventListener(eventListener);
    
    return true;
}

// ...

void HelloWorld::onTouchEnded(Touch* touch, Event* event)
{
    log("=== Call HelloWorld::onTouchEnded() ===");
    
    // Create Action
    auto eventListenerDisable = CallFunc::create([&]() {
        log("eventListenerDisable()");
        this->getEventListener()->setEnabled(false);
    });
    
    auto eventListenerEnable = CallFunc::create([&]() {
        log("eventListenerEnable()");
        this->getEventListener()->setEnabled(true);
    });
    
    float deltaAngle = this->getRightFlg() ? 180.0f : -180.0f;
    auto rotateBy = RotateBy::create(2.0f, deltaAngle);
    
    // Run Action
    this->getSprite()->runAction(Sequence::create(eventListenerDisable,
                                                  rotateBy,
                                                  eventListenerEnable,
                                                  nullptr));
    
    // Set RightFlg
    this->setRightFlg(!this->getRightFlg());
}

Sequenceアクションを使い、目的のアクションの実行前後でイベントリスナーの無効化・有効化を行っています。

これにより、回転中にタッチしても、逆回転しなくなりました。

シーングラフの最上位にイベント制御のための不可視のノードを置く


前述の方法だと、回転中にタッチしても逆回転は行われませんが、右下のメニューボタンは押すことが出来てしまいます。

全てのイベントリスナーを無効にしたい場合は、イベント制御のための不可視のノードを追加します。

サンプルは以下です。


ポイント箇所を抜粋します。

// HelloWorldScene.cpp

// ...

bool HelloWorld::init()
{
    // ...

    // Invisible node to prevent event listener
    auto invisibleNode = Node::create();
    this->addChild(invisibleNode, 999);
    auto preventEventListener = EventListenerTouchOneByOne::create();
    preventEventListener->onTouchBegan = [](Touch* touch, Event* event) {
        log("!!! Prevent Event Listener !!!");
        return true;
    };
    preventEventListener->setSwallowTouches(true);
    preventEventListener->setEnabled(false);
    Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority(preventEventListener, invisibleNode);

    // Set Properties
    // ...
    this->setPreventEventListener(preventEventListener);
    
    return true;
}

// ...

void HelloWorld::onTouchEnded(Touch* touch, Event* event)
{
    log("=== Call HelloWorld::onTouchEnded() ===");
    
    // Create Action
    auto eventListenerDisable = CallFunc::create([&]() {
        log("eventListenerDisable()");
        this->getPreventEventListener()->setEnabled(true);
    });
    
    auto eventListenerEnable = CallFunc::create([&]() {
        log("eventListenerEnable()");
        this->getPreventEventListener()->setEnabled(false);
    });

    float deltaAngle = this->getRightFlg() ? 180.0f : -180.0f;
    auto rotateBy = RotateBy::create(2.0f, deltaAngle);
    
    // Run Action
    this->getSprite()->runAction(Sequence::create(eventListenerDisable,
                                                  rotateBy,
                                                  eventListenerEnable,
                                                  nullptr));
    
    // Set RightFlg
    this->setRightFlg(!this->getRightFlg());
}

不可視のノードを追加し、シーングラフの優先度がもっとも高くなるようにZオーダーを設定します。

そのノードに対し、setSwallowTouches()をtrueに設定したイベントリスナーを登録します。

そして、アクションの前後にそのイベントリスナーの有効化・無効化を行えば、アクション実行中は全てのイベントリスナーを無効化できます。

なお、ここではイベントリスナーのsetEnabled()を切り替えて実現しましたが、setSwallowTouches()のほうを切り替えても可能だと思います。

さて、ここで一つ疑問が。

ノードを置かずに、addEventListenerWithFixedPriority()で優先度が最高のイベントリスナーを設定するだけでも出来るのでは…?

と思ってそれも試したのですが、なぜか出来ませんでした。。。

イベントリスナー自体は有効になっているものの、どうもsetSwallowTouches()の設定が効いてないっぽく、イベントが伝搬してしまうんですよね…。

うーむ。。。

最後に


1つのイベントリスナーを無効化したい場合は前者、全てのイベントリスナーを無効化したい場合は後者、といった使い分けになると思います。

2015年6月7日日曜日

パスワード管理ツールにKeePass2を使ってみた

だいぶ前からパスワード管理ツールを導入しないとまずいなぁと感じていたのですが、この度KeePass2を導入してみました。

1Passwordと迷い、とりあえずKeePass2を使ってみて不都合があれば1Passwordにと思っていたのですが、KeePass2で特に問題を感じません。

Dropboxで管理ファイルを同期するようにしたのですが、クロスプラットフォームでも使えてもうめっちゃ便利ですよ…!!

なぜこれまで使ってこなかったのか…。

各プラットフォームで使用したソフトは以下の通りです。

Windows



2.x系をダウンロードします。

Ubuntu


  • KeePass2

apt-get でインストールできます。

Mac


ここに「MonoでKeePass2を動かせるよ!」とあるのですが、実際やってみたらすぐにアプリが落ちてしまい、使い物になりませんでした。

なのでこちらを使います。


2.x系に対応したバージョンがあるので、そちらをダウンロードします。

まだアルファ版ですが、特に問題なく使えます。

iPhone



iPhoneのアプリはいくつかあったのですが、私はこれを選びました。

特に不具合は感じません。しかも無料…!!

パスワード管理から解放され、脳が軽くなった気分。

2015年5月12日火曜日

初めてのPythonでMongoDB

はじめに


先日、 (第54回)Python mini Hack-a-thon に参加した時、PythonでMongoDBを使ってみることを試したので、簡単にまとめておこうと思います。

といっても使ってみること自体は特に難しいところもなかったため、参考にしたドキュメントのリンクのみ貼っておきます。

参考にしたドキュメント





前者はMongoDBの入門書です。和訳感謝。

40ページくらいの本当に薄い本で、


  • MongoDBって何?
  • どんな特徴があるの?
  • どうやって使うの?


ということを手っ取り早く掴むことができました。

後者は公式のドキュメントです。

PythonでMongoDBを操作する方法が簡潔にまとまっており、基本的なことを短時間で理解することができました。

使ってみた感想


『MongoDBの薄い本』には次のように書かれています。

スキーマレスの本当の利点はセットアップの省略とオブジェクト指向プログラミングとの摩擦の低減です。
(原文)the real benefit of dynamic schema is the lack of setup and the reduced friction with OOP.

まさにこれを実感しました。

PostgreSQLなどのRDBMSをPythonで操作する場合、普通はドライバの他にSQLAlchemyなどのORマッパーを使うと思います。

さらにスキーマ管理などを行おうと思ったら、Alembicなどのマイグレーションツールも必要になります。

これらのパッケージの使い方の学習なども考えると、導入までの道のりが結構たいへん。。。

ところが、MongoDBの場合はとりあえずドライバだけ入れればOKです。

DDLによるスキーマの設定も不要で、ORマッパーなどがなくても、オブジェクトを操作する感覚でDBも操作することができます。

とりあえず使えるようになるまでのステップは、RDBMSの場合と較べてかなり短くて済むと感じました。

今後、色々試してもっと理解を深めていきたいです。

2015年4月10日金曜日

cocos2d-xでテキストファイルを1行ずつ処理する方法

cocos2d-xでキャラクターのセリフなどをテキストファイルから読み込んで、1行ずつ処理するにはどうしたらよいのかを検討してみたのでメモ。

バージョン


cocos2d-x v3.5

結論


先に結論を書くと、

FileUtilsのgetStringFromFile()メソッドでテキストファイルを読み込み、改行文字で分割して処理する

です。

経緯


この方法は最初に思いついた方法です。

しかし、C++の標準ライブラリには文字列を分割するAPIがないため、自分で実装するか、外部のライブラリを使用する必要があります。

それはちょっと面倒臭い上に、あまりスマートではない気がしたので、ファイルを1行ずつ読み込んで処理する方法がないかを模索したところ、std::ifstreamを使えばいけるのではと考えました。

コードはこんな感じです。

#include <fstream>

USING_NS_CC;

auto filePath = FileUtils::getInstance()->fullPathForFilename("sample.txt");

std::ifstream inFile(filePath);
for (std::string line; std::getline(inFile, line);) {
    // ここに処理を記述
    log("%s", line.c_str());
}

この例ではResourcesディレクトリ直下に「sample.txt」を配置しているという想定です。

FileUtilsのfullPathForFilename()メソッドでパスの問題を解決し、std::ifstreamでファイルを開きます。

そして、std::getlineで1行ずつ読み込んでいます。

これでちゃんと動作したので、よっしゃ!と思ったのですが、問題が起こりました。

上記のコードは、iOSとWin32では問題なく動くのですが、Androidでは動かなかったのです。

問題は以下の部分です。

std::ifstream inFile(filePath);

Androidではここでファイルを開くことが出来ませんでした。

調べた所、Androidの場合、リソースファイルはapkファイルにzip形式で格納されますが、std::ifstreamはzipに格納されたファイルを読み込むことが出来ない、というのがその理由でした。

というわけで、残念ですがstd::ifstreamを使った方法は断念し、最初の案を採用することにしたのでした。

コード例


文字列を分割する方法は色々あるようですが、こちらの方法が短く書けていいかなと思いました。


これを使って書くと、こんな感じです。

USING_NS_CC;

auto split = [](const std::string& input, char delimiter) {
    std::istringstream stream(input);
    std::string field;
    std::vector result;
    while (std::getline(stream, field, delimiter)) {
        result.push_back(field);
    }
    return result;
};

auto fileText = FileUtils::getInstance()->getStringFromFile("sample.txt");
auto lines = split(fileText, '\n');
for (const auto& line : lines) {
    // ここに処理を記述
    log("%s", line.c_str());
}

にしても、何か他に良い方法はないものだろうか…。

参考リンク


2015年3月25日水曜日

【#TechBuzz】第11回cocos2d-x勉強会 参加レポート

3/24(火)にCocos2d-xの勉強会に参加したので、簡単にレポートします。


cocos2d-consoleでパッケージ管理(@giginet




Cocos2d-x 3.4 でcocosコンソールに追加されたパッケージマネージャのお話。

ライブラリのインストールや管理を簡単に行えるパッケージマネージャ機能はすごく便利そう!

と思いきや…、

現時点ではドキュメントもなく、パッケージもまだ3つしか登録されていない。

さらに、パッケージを登録する方法も用意されていないようです。

というわけで、今はまだ利用できそうにないですね。。。

これからに期待です。

cocos2d-xで『LWF』を利用する(@albatrus_jp



第11回 cocos2d-x勉強会 「cocos2dxでLWFを利用する」 from Naoki Yokota

Flashで作られたswfファイルをlwfという形式に変換して、HTML5やUnity、Cocos2d-xで利用できるようにしてくれるフレームワークLWFについての発表。

導入自体はそんなに難しくないが、位置調整などで苦労されたとのこと。

確かにこれを使うと表現の幅が広がりそうです。

ただ、私が個人で作るようなゲームで果たして出番があるかどうか…。

swfファイルを作るにもAdobeの有料ツールが必要になるので、ちょっと試してみるにしても、今の自分にはちょっと敷居が高いかも。

ネットワーキングタイム


やはりゲームを作っているという人が多く参加しており、自分が今実装面で困っていることについて個人的に教えて貰うことが出来た。

また、個人でCocos2d-xを使い趣味でアプリをリリースされている方ともお話出来て、なんだか自分もやれそうな気になりました。

2015年3月22日日曜日

PythonでGmailを送る

PythonでGmailを送るツールを作ろうと思い、色々調べたのでメモ。

Gmailに限らず、標準パッケージを使った基本的なメール送信手順は以下の通りです。

  1. emailパッケージを使ってMIME文書を作成
  2. smtplibを使って送信

ここで、MIMEってなんだっけ?となったので、合わせて調べてました。

MIMEとは


Multipurpose Internet Mail Extension(多目的インターネットメール拡張)の略。

当初、Eメールの文字コードは英数字といくつかの記号を表現する7bitの「US-ASCII」しか利用できず、英語以外の文章やテキスト以外のデータの送受信ができませんでした。

そこで新たなヘッダ情報を追加し、「US-ASCII」以外のデータも扱えるようにした規格がMIME。

MIMEにより追加されるヘッダは以下の通りです。

  • MIME-Version:MIMEであることを示すヘッダ。現在は1.0のみ利用される。
  • Content-Type:メッセージ中のデータタイプの指定。text、imageなど。
  • Content-Transfer-Encoding:データの符号化方法の指定。7bit、base64など。

Gmailの場合


GmailのWebクライアントで文章のみのメールを送り、ソースを見たところMIMEのヘッダは以下のように設定されていました。

MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: base64

ソースコード


上記を踏まえたコードを書いてみます。Pythonは3.4.2です。

from email.mime.text import MIMEText
from email.header import Header
from email.utils import formatdate
from smtplib import SMTP_SSL


def create_message(from_addr, to_addrs, subject, body, encoding='utf-8'):
    msg = MIMEText(body, 'plain', encoding)
    msg['Subject'] = Header(subject, encoding)
    msg['From'] = from_addr
    msg['To'] = ','.join(to_addrs)
    msg['Date'] = formatdate(localtime=True)
    return msg


def send_via_gmail(from_addr, to_addrs, password, msg):
    s = SMTP_SSL('smtp.gmail.com', 465)
    s.login(from_addr, password)
    s.sendmail(from_addr, to_addrs, msg.as_string())
    s.close()


if __name__ == '__main__':
    from_addr = 'spam@gmail.com'
    to_addrs = ['egg1@gmail.com', 'egg2@gmail.com']
    subject = 'test subject'
    body = 'test body'
    password = 'password'

    msg = create_message(from_addr, to_addrs, subject, body)
    send_via_gmail(from_addr, to_addrs, password, msg)

MIMETextでencoding='utf-8'を指定すると、Content-Transfer-Encodingもbase64に設定されます。

また、 formatdateはデフォルトではUTCで生成されてしまいます。
localtime=Trueを指定することで、現地時間(日本の場合はUTC+9)になります。

参考

2015年3月17日火曜日

Cocos2d-xにおけるメモリ管理方法


Cocos2d-xでのメモリ管理方法について、自分の理解を整理するためにメモ。

執筆時のバージョン


Cocos2d-x v3.4.2

参照カウント


Cocos2d-xではメモリ管理の方法として、参照カウント方式を採用しています。

参照カウントはガベージコレクション戦略の1つで、その概要は以下の通りです。

  • オブジェクトに対して、参照カウントと呼ばれる整数値を付加しておく。
  • オブジェクトに対する参照がどこかに保存されたら参照カウントをインクリメントし、オブジェクトに対する参照が削除されたらデクリメントする。
  • 参照カウントがゼロになったら、オブジェクトへの最後の参照が削除されたことになり、オブジェクトを破棄する。

Refクラス


これをCocos2d-xではRefクラスに実装しており、Nodeクラスなどほとんどのクラスの基底クラスになっているようです。

Refクラスのメンバは以下の通り。


Public Member Functions


void retain () 参照カウンタを+1。
void release () 参照カウンタを-1。0になったらオブジェクトは破棄される。
Ref * autorelease () オブジェクトをAutoreleasePoolに登録。自動的に破棄されるようになる。
unsigned int getReferenceCount () const 参照カウンタを取得。
virtual ~Ref () デストラクタ。


Protected Member Functions


Ref () コンストラクタ。参照カウンタを1で初期化する。


Protected Attributes


unsigned int _referenceCount 参照カウンタ。


参照カウンタ_referenceCountを、retain()とrelease()で操作します。

AutoreleasePool


私はiOSアプリの開発経験はないのですが、iOS SDKではNSAutoreleasePoolが参照カウント方式のメカニズムをラップしているらしいです。

これをCocos2d-xでもAutoreleasePoolとしてクローンしています。

代わりにstd::shared_ptrを使おうと試みたりもしたそうですが、パフォーマンスの問題で断念したとのこと。

AutoreleasePoolの仕組みはこうです。

  1. object->autorelease()を呼び出した時、このオブジェクトはAutoReleasePoolに登録される。
  2. AutoReleasePoolはこのオブジェクトのライフサイクルを現在のフレームの終了まで保持する。
  3. 現在のフレーム(スタックフレームのこと?)の終了時に、このオブジェクトが他のクラスやコンテナに保持されていなければ、自動的に破棄する。

例えば、layer->addChild(sprite)とすると、spriteはlayerの子リストに追加されます。

この場合、spriteのライフサイクルは現在のフレームの終わりではなく、layerが破棄されるまでとなります。

create関数


RefクラスのAPIを見れば分かるように、Refクラスのコンストラクタはprotectedであるため直接newすることはできません。

ではどうするのかというと、代わりにcreateおよびcreateから始まるスタティックなメンバ関数が用意されています。

この関数の処理の概要は以下の通りです。

  • 新しくインスタンスを生成する。
  • 必要な初期化を行う。
  • autorelease()を呼び出す。
  • 生成したインスタンスへのポインタを返却する。

create関数を使うことで、忘れずにautorelease()を呼び出し、AutoReleasePoolに登録するわけですね。

試しに1つ実装を見てみます。

以下は、Nodeクラスのcreat関数の実装です。

Node * Node::create()
{
    Node * ret = new (std::nothrow) Node();
    if (ret && ret->init())
    {
        ret->autorelease();
    }
    else
    {
        CC_SAFE_DELETE(ret);
    }
    return ret;
}

LayerやSpriteなど、既存のクラスを継承して独自のクラスを定義するとき、create関数にあたるものを作ることになりますが、その場合はCREATE_FUNCが用意されているので、引数が不要な場合はこれを使います。

引数が必要な場合も、既存のcreateを参考にすれば良さそうです。

まとめ

  • cocos2d-xではメモリ管理は参照カウント方式。
  • Refクラスがそのためのクラス。多くのクラスの基底クラスになっている。
  • retain()とrelease()を使ってリファレンスカウントの管理を行う。
  • release()を忘れずに行うために、AutoReleasePoolを使う。
  • AutoReleasePoolにオブジェクトを登録すると、スタックフレームの終了時に自動で解放してくれる。
  • AutoReleasePoolへの登録は、autorelease()を呼び出すことで行う。
  • autorelease()はcreate関数の中で呼び出される。
  • cocos2d-xのオブジェクトを生成するときはcreate関数を使う。