タッチイベント内でのRenderTextureの不具合

TiledMapを使わずにSpriteを敷き詰める場合、
描画高速化と継ぎ目を目立たなくする対策としてRenderTextureを使用しているのですが、
以下の条件を満たしているとオフスクリーンの一部の描画がされないケースがありました。

  • 一定サイズより大きいRenderTextureを作成する
  • タッチイベント内で描画を行う

理屈はよくわかりませんが、再現したのでコードをメモしておきます。

  • プロジェクト作成
cocos new cocos-test -p com.test -l cpp
  • HelloWorld に onEnter を追加します。
class HelloWorld : public cocos2d::Layer
{
public:
...
+    void onEnter();
...
}
  • HelloWorld.cpp に下記のコードを追記します。
#define SIZE 64

void HelloWorld::onEnter() {
	Layer::onEnter();
	
	testRenderTextureBug();
	
	auto listener = EventListenerTouchOneByOne::create();
	listener->onTouchBegan = [=](Touch*, Event*){
		testRenderTextureBug();
		auto delay = DelayTime::create(2);
		auto call = CallFunc::create([=]{
			this->testRenderTextureBug();
		});
		runAction(Sequence::create(delay, call, NULL));
		return true;
	};
	getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, this);
}

void HelloWorld::testRenderTextureBug() {
	
	auto tex = RenderTexture::create(SIZE*10, SIZE*10);
	tex->setPosition(SIZE*2, SIZE*5);
	tex->beginWithClear(0.3, 0.3, 0.3, 1);
	for (int i = 0; i < 10; i++) {
		for (int j = 0; j < 10; j++) {
			auto color = Color3B(0, i*16, j*16);
			auto s = Sprite::create();
			s->setTextureRect(Rect(0, 0, SIZE, SIZE));
			s->setColor(color);
			s->getTexture()->setAliasTexParameters();
			s->setPosition(j*SIZE, i*SIZE);
			s->visit();
		}
	}
	tex->end();
	addChild(tex);
}

testRenderTextureBug() を呼ぶ度にRenderTextureを生成して重ねます。(下のRenderTextureは見えなくなる)
実行し、画面タッチするとバグの再現を確認できます。

  • onEnterで生成すると、正常なRenderTextureが表示される

rt_test1

  • 画面タッチイベント内で生成すると、一部欠けたRenderTextureが表示される

rt_test2

  • 画面タッチ後のCallFunc内で生成すると、正常なRenderTextureが表示される

rt_test3

この通り、対策としてはCallFuncで生成する事でしょうか。
根が深そうなのでソースコードまでは追っていません。

そして cocos2d-x に帰ってくる

あれから結局 CocosSharp はまだ早いと断念し、本家 cocos2d-x に乗り換えました。
さすがに本家では基本的な機能で問題が出ることはなかったのですが、
いくつかの問題があったのでまとめて起きます。

ClippingNode::setStencil で SIGABRT になる

また ClippingNode かい、という感じです。
ちゃんと原因がわかると納得なんですが、同様のタイミングでエラーになった方を
検索してもあまり見かけなかったので、参考までに。

再現させるコードを簡潔に書くと、こんな感じ。

auto clip = ClippingNode::create();
addChild(clip);

auto mask = DrawNode::create();
clip->setStencil(mask);

auto move = MoveBy::create(...);
auto callfunc = CallFunc::create([clip]{
	auto newMask = DrawNode::create();
	clip->setStencil(newMask);
});
clip->runAction(Sequence::create(move, callfunc, NULL));

まず、マスク用のイメージは DrawNode を使い、動的に生成します。
その後、stencil を書き換える CallFunc を実行します。
すると以下のエラーメッセージを出力する CCASSERT に引っかかります。

Node still marked as running on node destruction! Was base class onExit() called in derived class onExit() implementations?

setStencil の中で、古い stencil に対して CC_SAFE_RELEASE が実行され、
参照カウンタが 0 になった場合はそのままデストラクタが呼ばれるという流れになるのですが、
stencil ノードが表示中の為、ASSERT に引っかかってしまうという事の用です。

最終的に DrawNode のインスタンスを保持しておいて、マスク用のイメージを書き換える事で回避しました。

auto clip = ClippingNode::create();
addChild(clip);

auto mask = DrawNode::create();
clip->setStencil(mask);

auto move = MoveBy::create(...);
auto callfunc = CallFunc::create([clip, mask]{
	// 既存のDrawNodeを書き換える
	mask->clear();
	mask->drawPolygon(...)
});
clip->runAction(Sequence::create(move, callfunc, NULL));

基本的な事なのかもしれませんが、
addChildやremoveChildでは SIGABRT になる事はなかったので少し面食らいました。
ちなみにアクション中に限らず、onTouchBegan 中に同様に setStencil すると SIGABRT が起きます。
ClippingNode のステンシルノードは入れ替えたりしない想定で使いましょう、という事で。

RemoveSelfの後のアクションが実行されない事がある

これも基本的な事っぽいのですが、
以下のコードだと “action completed.” はログ出力されません。

auto move = MoveBy::create(...);
auto remove = RemoveSelf::create();
auto delay = DelayTime::create(1);
auto cb = CallFunc::create([=]{
	log("action completed.");
});
node->runAction(Sequence::create(move, remove, delay, cb, NULL));

これは、RemoveSelf の実行後、nodeがどこからも参照されていなければ一定のタイミングで
メモリ上から解放され、その後のアクションは実行されないからです。
間に DelayTime を挟んでいなければ実行されるとは思いますが、あまり信用しない方がいい気がします。

画面上から Node を削除した後 CallFunc を実行した場合は、下記の様に記述するのが正解のようです。

auto move = MoveBy::create(...);
auto hide = Hide::create();
auto delay = DelayTime::create(1);
auto cb = CallFunc::create([=]{
	log("action completed.");
});
auto remove = RemoveSelf::create();
node->runAction(Sequence::create(move, hide, delay, cb, remove, NULL));

cocos2d-xでの本格的な開発は経験がないので、
オープンソースのコードを参考にさせてもらいたいのですが、
ゲームはあまりオープンソースでコードを公開されるケースが少ないですね。
特許だったりチートされる可能性だったり色々あるので仕方ないのですが・・・

またネタが溜まったら書きます。