世界の隅の開発室

 

◎  スポンサーサイト 

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

◎  CallFuncNでキャプチャ付きラムダ式を使う上での寿命の問題【cocos2d-x】 



runActionメソッドで扱うことのできるアクションの一つに、関数の実行があります。


auto func = CallFunc::create(CC_CALLBACK_0(NAMESPACE::METHODSYMBOL,OBJECT));
OBJECT->runAction(func);


こんな感じで扱うのが一般的ですかね。

CC_CALLBACKでインスタンスとメソッドのシンボルをバインドして、アクションを生成。

それをNodeインスタンスのrunActionにぶん投げる、って感じでしょうか。

もちろん、これだけでは何の意味もないので、大抵は遅延をかけたり、アニメーションと組み合わせたりします。

//遅延をかける例

auto func = CallFunc::create(CC_CALLBACK_0(NAMESPACE::METHOD_SYMBOL,OBJECT));
auto delay = DelayTine::create(DELAY_TIME);
OBJECT->runAction(Sequence::create(delay,func,nullptr));


ま、これなら基本的に普通に動くんですが、仮にキャプチャ付きのラムダ式を用いて関数アクションを生成する場合は、変数のキャプチャに関して注意しなくてはならない点があります。

まずはこのコードを見てみて下さい。


int n = 10;
auto func = CallFuncN::create([&](Ref* obj){OBJECT->setValue(n);});
auto delay = DelayTine::create(DELAY_TIME);
OBJECT->runAction(Sequence::create(delay,func,nullptr));


なんとなく動きそうな感じのプログラムです。

キャプチャしたthisとnを使って、遅延後thisにnを格納するプログラムな訳ですが

このコード、かなり高い可能性でBAD_ACCESSを起こします。

なぜかといいますと、キャプチャは参照受け取りを意味しますので

まあOBJECTはともかくとしてローカル変数のnも参照受け取りしているわけですね。

となると、nの寿命はもちろんrunActionが行われているメソッド内に限られますので、このラムダ式の実行時...つまりDELAY_TIME後にはnはスコープを出て解放されている可能性がかなり高いです。

となると、nを参照しようとした時点でBADACCESSって訳です。

というわけで、基本的にrunActionに渡すラムダ式ではキャプチャを使わないようにした方がいいと思います。

上記のコードを修正しますと


int n = 10; auto func = CallFuncN::create([n,OBJECT](Ref* obj){OBJECT->setValue(n);});
auto delay = DelayTine::create(DELAY_TIME);
OBJECT->runAction(Sequence::create(delay,func,nullptr));


となります。

runActionは別スレッドで動きますので、くれぐれも寿命や競合の問題には気をつけたいところです。
スポンサーサイト

◎  タッチイベントをコード側からキャンセル【cocos2d-x】 

cocos2d-xではEventDispatcherを使ってこんな風にタッチイベントを取得します


//GameLayer.cpp

auto touchListner = EventListenerTouchOneByOne::create();
touchListner->setSwallowTouches(true);

touchListner->onTouchBegan = CC_CALLBACK_2(GameLayer::onTouchBegan,this);
touchListner->onTouchMoved = CC_CALLBACK_2(GameLayer::onTouchMoved,this);
touchListner->onTouchEnded = CC_CALLBACK_2(GameLayer::onTouchEnded,this);
touchListner->onTouchCancelled = CC_CALLBACK_2(GameLayer::onTouchCancelled,this);

_eventDispatcher->addEventListenerWithSceneGraphPriority(touchListner, this);


すると、指が離れた、もしくは着信などによってタッチが中断された場合、

onTouchMoved から onTouchEnded or Cancelledにイベントが推移します。

これらはEventDispatcherによって制御されていますが

ここで、タッチイベントの発生とは関係なく、直接タッチをキャンセルさせたい場合
つまりコード側でタッチキャンセルイベントの実行をしたい場合はどうするんでしょうか。

もちろんonTouchCancelld();と呼び出したところで意味はありません。
このメソッド自体は単なるイベントを紐付けたNodeを継承したクラスのメンバ関数であり
タッチイベントのキャンセル自体とは関連がないからです。

ここで、ではさくっとeventDispatcherの実装を見てみます。

すると、こんなパブリックメソッドが見つかります。


//CCEventDispatcher.h

public:
////////
void dispatchEvent(Event* event);
////////



イベントを発生させてそうなメソッドです。さて、ここに適したイベントを渡せばキャンセルできそうな感じです。

さて、ここでeventDispatcherの実装を他にも色々と見ると、タッチイベント中断の為にここに渡すべきイベントは、Eventクラスを継承したEventTouchクラスであることが分かります。



//CCEventTouch.h

public:
////////
enum class EventCode
{
BEGAN,
MOVED,
ENDED,
CANCELLED
};

EventTouch();

inline EventCode getEventCode() const { return _eventCode; };
inline const std::vector<Touch*>& getTouches() const { return _touches; };

#if TOUCH_PERF_DEBUG
void setEventCode(EventCode eventCode) { _eventCode = eventCode; };
void setTouches(const std::vector<Touch*>& touches) { _touches = touches; };
#endif

////////



かなり見えてきました。
タッチのイベントの種類を示す列挙体と、アクセッサたちです。

これらを上手く構成してeventDispatcherに渡せば、上手く動きそうです。

というわけで、onTouchMoved中に何らかのイベントによってタッチをキャンセルしたい場合、コードはこうなります。


//GameLayer.cpp

void GameLayer::onTouchMoved(cocos2d::Touch* touch,cocos2d::Event* event){
////////
EventTouch cancellEvent; //インスタンス作成
cancellEvent.setEventCode(EventTouch::EventCode::CANCELLED); //イベント種類設定
cancellEvent.setTouches(std::vector<Touch*>{touch}); //タッチ情報セット(std::initializer:c++11)

_eventDispatcher->dispatchEvent(dynamic_cast<Event*>(&cancellEvent)); //イベントdispatch
////////
}


これで動くと思います。

◎  【cocos2d-x】メモリリークとリファレンスカウンタ 

どもです。

今回はcocos2d-xにおける重要なファクター、リファレンスカウンタについて少しお話します。

そもそもcocos2dは当初Objective-C用ライブラリでして、その特徴を色濃く受け継いでおります。
Nodeアイテムの入れ子構造を構成するために、ヒープ上のインスタンスへの参照ポインタで全てのオブジェクトが管理されている、といったことですね。

getChildWithTagのようなメソッドは、各Nodeオブジェクトが保持する、自身にaddされたNodeオブジェクトアドレステーブルを持っているからこそ実現するものなのです。

話を戻しまして、リファレンスカウンタとはなんなのでしょうか。
Obj-Cを使ってる方は勿論、C++においてもスマートポインタを使用したことある方には馴染み深いとは思うのですが、少し解説します。

まず、newによっておこるメモリリークの危険性から。
newを行いインスタンスを作ると、ヒープ領域上にインスタンスが作られ、そのポインタが返ってきます。

Myclass *ptr = new Myclass;

ptrはヒープ領域上に存在する変数の、"本体"ともいうべきインスタンスを指すポインタということになります。
このインスタンスを仮にAとしましょう。
このとき、このptrがスコープを抜けたらどうなるのでしょうか。
ローカル変数はスタック上に確保され、関数コールスタックのポップとともに解放されます。
つまり、ptrはスコープを抜けると消滅します。

ですが、Aは、スタック上ではなく、ヒープ領域に宣言されています。
そのため、ptrがスコープを抜け解放されても、Aはヒープ領域に残り続けます。

そして、ヒープ領域上に宣言された変数を解放するdeleteは、その変数のアドレスを用いなければ実行出来ません。
ですが、ptrが解放された今、そのAの位置はプログラマは愚か、コンパイラにすら分からないものとなってしまうのです。
つまり、このAはプログラム実行中はもちろん、下手すればシャットダウンまで残り続けることになるのです。(この辺りは仮想メモリ割当などの素敵技術によってOSが安全に管理してくれているのですが、それはまた別の機会で議論することにして)

これがいわゆるメモリリークというものです(そのひとつです)。

ここで、このケースの具体的な問題点を考えてみましょう。

今回起きたメモリリークの原因は、Aを参照するポインタ変数が一つも無いのにも関わらず、ヒープ上に残ってしまったことです。

つまり、逆に言えば、Aを参照するポインタ変数が1つも無いときにAが自動的に解放されれば、それはメモリ的に安全とも言えます。


これを実現するのがリファレンスカウンタです。
ここまで長かったですね。

リファレンスカウンタとは即ち、自身を参照している変数の数のことです。

大事です。
ヒープ上のインスタンス自身が、自分を参照している変数の数を理解していれば、安全にメモリが管理できるのです。
そのため、インスタンスのメンバとしてリファレンスカウンタを持ち、そしてそれらを管理する機構をもって、リファレンスカウンタは実装されます。
リファレンスカウンタが0のとき、それは自身を参照する変数が無いということですので、ヒープ領域から解放されるべきですね。

話を戻しまして、cocos2d-xにはAutoReleasePoolというものでそれが実現されています。

cocos2d::Label* pLabel = cocos2d::Label::create();

などと行うと、内部的にはnewによるインスタンス生成と、リファレンスカウンタ管理機構への登録が行われます。

このとき、pLabelが指すLabelインスタンスは、pLabelによって参照されているため、リファレンスカウンタは1です。
そして、その後


bool TestScene::init(){
~~~~~
this->addChild(pLabel);
~~~~~
}


と行うと(TestSceneはcocos2d::Layerの継承クラス)
Labelインスタンスはthisが指すインスタンスのテーブルに登録されるため
リファレンスカウンタはpLabelとthis、つまり2です。

そのため、ローカル変数であるpLabelが解放されても(init関数が終了しても)、Labelインスタンスは解放されることはありません。

ですが、注意が必要です。
このルールにおいて逆に考えましょう。
リファレンスカウンタが0になると、勝手に解放されてしまうということを。

cocos2d::Label* TestScene::makeLabel();

というメンバ関数を考えてみます。
この関数は、ラベルを作り、そのインスタンスのアドレスを返す関数とします。

cocos2d::Label* TestScene::makeLabel(){
cocos2d::Label* plabel = cocos2d::Label::create();
return plabel;
}


もしこれが単なるnewで作られるインスタンスならば、問題はありません。(危険ではありますが)

ですが、リファレンスカウンタを考えると、これは明らかに間違っていることが分かります。

まず、createされ、それをplabelが参照しているのでインスタンスのカウンタは1です。
ですが、その後returnによって関数はコールスタックからポップされ、plabelは解放されます。
この瞬間、LabelインスタンスはAutoReleasePoolによって解放されてしまいます。
即ち、関数が終了し返ってくるアドレスは、既に無効なアドレスなのです。

これが怖いところです。リファレンスカウンタも完全ではありません。これもまた、メモリリークの危険性を大きく孕んでいます。

ではどうすればいいのでしょうか。
幸い、リファレンスカウンタを強制的に1あげるretain()という関数があります。

plabel->retain();

これを使ってあげることでplabelが解放されてもインスタンスは残りますが
retain()を行った場合、参照変数の数=カウンタの関係が崩れますので
解放時に明示的にrelease関数でカウンタを1下げる必要があります。

plabel->release();

ですが、解放処理をプログラマに委託するのは危険です。プログラマはすぐに忘れる生き物ですから。
そもそも、create処理とaddChild処理が別のモジュールで行われていること自体ナンセンスなのです。

データと処理を一体化させ、オブジェクトにしましょう。

class Myclass{
private:
cocos2d::Label* plabel;
public:
void makeLabel();
cocos2d::Label* getLabel();
};


まあこれも頭の良い設計とは言えませんが、大体は解決しました。

では今回はこの辺で。




◎  【cocos2d-x】TMXTiledMap色々弄ってみた。 

どうも。

最近タクティクスRPG(FEやファミコンウォーズなど)を作ろうかと思いまして
The Tiled Map Editorとやらを使ってみたのです。


どうやらタイルセットと呼ばれるものを用意してあげれば、cocos2d-x側でそれを呼び出すことができるというもの。
しかも、そのクラスはcocos2d::Nodeを継承しているようで、特別な知識も必要なく円滑に開発に組み込めるとか。

しっかしまあ実際に使ってみたら問題が多いこと多いこと。
私が行き詰まってしまったところを順に解説していきたいと思います


1.タイルセットに用いるPNG画像はインデックス・カラーが使えない

透過PNGを扱う上で、ドット絵などを描く際にはよく使う保存フォーマットの、インデックス・カラー。
フルカラーと違い非常にサイズがコンパクトになる上、透過の設定もやり易いので、ドット絵を弄るときにはこのフォーマットを愛用しているのですが
どうやらTiledMapEditorでは読み込めるのですが、cocos2d-xのTMXTiledMapクラスでは読み込めない模様。
いや、なら最初からエディタ側ではじいてよ!って感じですね。
なまじマップが作れちゃっただけに、これに気づくのにやたらと時間かかりました

2.タイルセットもエクスポートする必要がある

マップを作ると.tmxっというファイルが生成されるのですが
これだけじゃcocos2d-x側で読み込めません。
Editor側でタイルセットのエクスポートを行い、tsxファイルを作る必要があります。
そのため、プロジェクト側ではマップデータのtmxファイル、タイルセットデータのtsxファイル、そしてタイルセットの元にしたpngファイル(いらないかも)が必要になります。

この2つをとりあえずクリアすると、作ったマップが読み込めます。


TMXTiledMap* pTileMap;
pTileMap = TMXTiledMap::create("testmap.tmx");



このTMXTiledMapはNodeを継承しているので
addchild、setPositionなどがいつも通りできます。

また、独自のメソッドとしてタイル情報取得や、タイル切り替えなどが可能なのですが....


3.座標系がcocos2d-x標準と違う

cocos2d-xは左下原点、数学の標準の座標系を採用しているんですが

TMXTiledMapインスタンスにたいして、getTileGIDAtのような座標指定のメソッドを実行する際
座標系は左上原点になることに注意しなくてはなりません。

コンピュータグラフィクスなどやっておりますと、左上原点の方が自然に考えられたりするのですが
cocos2d-xの座標系と異なると少し大変。
例えばタッチしたタイルに対する操作などを行いたい場合

①.TMXTiledMapのインスタンスにsetAnchorPoint(Vec2::ZERO);を実行し、左下を基準とする

②.タッチ地点の座標よりタイルの座標を計算


タッチ座標をpos、タイルのサイズをMAP_TIP_SIZEとして
pos.x = static_cast<int>((pos.x - pTileMap->getPosition().x)/ MAP_TIP_SIZE);
pos.y = static_cast<int>((pos.y - pTileMap->getPosition().y)/ MAP_TIP_SIZE);

このように、タイルマップにおける相対座標になるよう変換します。

③.座標系変換し、タイルID取得

マップのタイルの横/縦の数を MAP_TILE(width/height)として
TMXLayer* layer = pTileMap->getLayer("testmap"); //layer取得
int tileType = layer->getTileGIDAt(Vec2(pos.x, MAP_TILE.width-1-pos.y));

このようにyの座標を反転させて、IDを取得します。


ちょっとめんどくさいですね。

また、先ほどgetLayerメソッドでLayer情報を取得していましたが...

4.Editorで非表示にしたLayerを取得しようとすると、NULLが返ってくる

なんでやねん!
そもそも衝突判定のためのcollisionレイヤーを作ろうと思ったのでして。
ちらほらcollisionと命名したレイヤーは勝手に非表示になるとか、いろんな情報が錯綜していましたが、結果そんなことは一切無く
0 or elseで衝突判定したかったので、適当なタイルを選んで衝突部分に配置したcollisionレイヤーを非表示にして使おうと思ったところ、NULLが返ってきた次第です。

getLayerさえできれば、ID取得の際、なにもないところは0、なにかあるときは0以外が返ってくるので、適当なタイルを衝突させたいところに置くことで実装できるんですが、非表示でバグるなら、表示にするしかない。表示したら、当然衝突判定がある部分に配置したタイルが見えてしまう....というジレンマを抱え
結果的に レイヤーの透明度(Opacity)を0.00にして使うという荒技でなんとかしました。
他に良い方法があれば教えて頂きたいです。



まあこんなことをあれこれやりまして
なんとかそれっぽいものが作ることができた感じです。

近いうちにきちんとした形で公開したいですね。

ではまた次回。






back to TOP

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。