cocos2d-x 3.0 betaの描画関連ソースを読んだ感想

cocos2d-x 3.x系はバージョンごとに大きな変更が入り、互換性保証が無理無理な状態になってますが、
betaで、ものすごく大きなジャンプが入ったようです。

これまでのcocos2d-xの描画は、
①毎フレーム、DisplayLinkDirectorのmainLoop呼び出し

②現在のシーンのdrawScene呼び出し

③Nodeツリーをvisitメソッドでトラバースしていく(=木構造をたどっていくという意味です)。トラバースしながらdrawメソッドを呼んで各ノードを描画。

という形でした。
http://www.cocos2d-x.org/news/172
こちらに、auto batchingというキーワードがありますね。

これどういう意味かわからなかったのですが、
ソースを読む限り、VBO(VertexBufferObject)を利用して描画するノードの矩形情報を、複数ノード分をまとめて
GPUに送ることで高速化をしているようです。

つまり、複数ノード分の描画をまとめて処理している(auto batching)なわけです。
どうやっているかというと、③の処理が分割され、mainLoop内で④の処理が呼ばれるようになりました。

③Nodeツリーをvisitメソッドでトラバースしていく。各ノードのdrawでは、描画コマンドを作成してキューに蓄積する。

④Rendererクラスがキューの中の描画コマンドを順番に処理する。その際、いくつかの描画コマンドをVBOにまとめて、まとめて処理する。

VBOを使うというのはOpenGLの高速化手法の一つなので、どの程度かはわかりませんが高速化されたものと思います。


ただ、このbetaの変更で、cocos2d-x外部ツールのいくつかが非対応になったものと思われます。
僕が一番気になるのがSuperAnimationConverterですね。
まあもともと3系向けの対応は入ってませんでしたが、alpha0までは動いていることは確認していました。
しかし、全てのノードが描画コマンドを発行するbetaでは、もはやvisit内で描画をしているSuperAnimNodeV2.cpp
では非対応となります。

まあ、betaはまだまだ不安定バージョンなので様子を見ましょう。
やはり3系は本格的に採用を検討するのは3.1くらいになってからが妥当のようです。

cocos2d-x 3.xでの、Eclipse(ADT)ブレークポイントデバッグ方法

cocos2d-x3.0Alpha0でのブレークポイントデバッグに成功したのでやり方をメモします。
2.x系のブレークポイントデバッグ方法については、この記事の一番下にあげる本に紹介されてるので読んでください。

①清水さんの「Cocos2d-xによるiPhone/Androidプリプログラミングガイド」の通りにワークスペースを作成する。(通常通りAndroid Applicationとして実行できる状態になれば多分大丈夫だと思います)

②アプリのプロジェクト設定のC/C++ Buildで、「bash ${ProjDirPath}/build_native.sh」→「bash ${ProjDirPath}/build_native.sh NDK_DEBUG=1」にする。

Android.mkで、$(call import-moule,cocos2dx)の行の前に以下を挿入する。
$(call import-add-path,$(LOCAL_PATH)/../../../..)
$(call import-add-path,$(LOCAL_PATH)/../../../../cocos2dx/platform/third_party/android/prebuilt)

(アプリのプロジェクトが通常通りprojectsフォルダ直下にあることを想定しています。ともあれ$(LOCAL_PATH)/../../../..の部分をcocos2dxルートのフォルダパスになってれば大丈夫だと思います。)

④アプリを「Debug As→Android Native Application」で実行。

3.x系はNativeActivityを採用し、ADT自身も最近のバージョンでAndroid Native Applicationでのデバッグ実行(つまりNDKアプリとしての実行)をサポートしたので、2.x系よりブレークポイントデバッグが簡単になっています。

手順の抜けがあったらご指摘ください。

                                          • -

2013-12-10追記
すみません、もう一点必須の手順がありました。
Eclipseワークスペースで、環境設定→Android→NDKで、NDKのパスを指定してください。
これをしないと、NDKのパスが認識されず
[2012-10-04 12:09:12 - ndk_android] Unknown Application ABI:
[2012-10-04 12:09:12 - ndk_android]
[2012-10-04 12:09:12 - ndk_android] Unable to detect application ABI's

のようなログをCosoleで見ることになります。
この設定、Android Applicationとして実行するだけならいらないんですけどね。
面倒ですね。

Cocos2d-x開発のレシピ

Cocos2d-x開発のレシピ

パーティクルの設定をプログラムで動的に変える

※こちらは、細かい使用方法の調査は同僚に行っていただきました。ありがとうございます!

パーティクルの色や放出量や半径といったプロパティをCCAction系の使用感覚で変えたいことがあると思います。

実は、任意のプロパティの値をTween的に変更する方法が用意されています。
CCActionTweenとCCActionTweenDelegateです。

CCActionTweenの使い方は他のCCAction系のクラスとほぼ同じで、違うのが、以下の2点です。
・プロパティ名を引数に与える
・アニメーションさせたいクラスにCCActionDelegateを継承させ、updateメソッドを定義して、その中でプロパティの変更の計算を実装する必要がある。

勝手に、プロパティの値を均一に補完してくれる訳ではないんですよね。
これだと、自分でscheduleUpdate()とかで1フレームごとのプロパティ値設定を実装するのと大して変わらないと思われるかもしれません。

ただ、CCActionIntervalの派生クラスなので、他の派生クラスとのCCSequenceやCCSpawnによる組み合わせが期待できるのが大きいです。
やはり生にupdateメソッドを書くより扱いやすく読みやすいコードになります。

具体的にパーティクルに対して適用するときは、CCPartilceSystemQuadとCCActionTweenDelegateを多重継承したクラスを作って、それをCCPartilceSystemQuadの代わりに使うのが楽でしょう。

プログラムから好きなように動的に設定値を変えられると、パーティクルの表現の幅が広がります。
パフォーマンスにはご注意を。

パーティクルを移動した時に残像を出さなくする

パーティクルでCCParticleSystemQuadを使っていて、CCMoveToなどでノードごと移動すると、残像現象が起こります。
どういうことかというと、一度放出された粒子は、ノードのsetPositionに追随せず、
発生点からの計算された軌道にしたがって動くわけです。

こちら、なんとかする方法がないかと調べてましたが、CCParticleSystem.hを見たら普通に設定するenumがありました。

setPositionType(kCCPositionTypeRelative);
を使えばOKです。

ちなみにデフォルト値はkCCPositionTypeFreeで、放出後の粒子はノードの動きに影響されません。

ちなみにこちら、ParticleDesigner1.x系、2.x系の両方で、設定するメニューがありません。
あくまでソースコードで設定してください。

普段からAPI Referenceやヘッダファイルを見ておくといろいろ便利な機能があることに気づかされます。

CCMoveTo, CCMoveByなどのCCAction系の競合対策

CCMoveToなどのアクションでは、到達時間と目標値(By系は変更量)を指定します。

これらは、あるアクションをやっている途中にもうひとつ別のアクションを重ねると、思わぬ動作をすることがあります。
一つ例を挙げます。
あるCCSpriteオブジェクトが(0,0)にあるとします。
CCMoveToで座標(100,100)に1秒で移動するようにしたとします。
1秒が終わる前に、もうひとつ同じ内容のCCMoveToを実行したら、最終的には(100,100)には着きません。
もっと先に進んでます。
どこ辺りにつくかは、2番目のCCMoveToを起動したタイミング次第です。

これがなぜ起こるか説明します。
CCMoveToなどのCCAction系は、runActionで起動すると、毎フレームどれだけ移動すればいいか計算し、
対象のCCNode系オブジェクトの毎フレームのループで、変更量を積み重ねます。
CCMoveToで言えば、setPositionによってCCNodeの位置を毎フレーム変更します。
2つのCCMoveToが同時に走ると、positionプロパティに、毎フレーム、別の値がプラスされることになります。
当然、目的の場所には着きません。

このような、ひとつのリソースや変数(ここで言えばposotionプロパティ)に2つのものから書き込みがされることを、情報系の用語では「競合」と言います。

今回は、僕が競合による期待しない動作を回避するために対策した方法をいくつか紹介します。



①競合が起こるようなアクションを同時に2つ 走らせない
そもそも競合を起こさない、という話です。
大体、CCRotationとCCMoveToのような別種のものを2つ重ねるケースはあるでしょうが、同種のものを重ねなければならないケースなど滅多にないでしょう。
最初から競合が絶対に発生しないようなソースを書くのが一番です。

それができない場合は、1つ目が終わってから2つ目のものを起動するか、2つ目のものを起動する直前に1つ目のものを停止させればOKです。

前者はCCCallFuncなどのコールバックで実現出来ますね。
後者のための一番簡単なメソッドはstopAllActions()です。
全て止めるのが無理なら、一つ目のアクションにsetTag(int タグ値)でタグをセットしておいて、stopActionByTag(タグ値)で止めてやりましょう。


②ccConfig.hのCC_ENABLE_STACKABLE_ACTIONSマクロを0に
実は、CCMoveToなどのpositionを変えるアクションが2つ重なった時に、ちゃんと目的地で止めるか、それとも2つ分の変更値を合算するようにするか、というのは、コンフィグマクロで指定出来ます。
それがccConfig.hのCC_ENABLE_STACKABLE_ACTIONSです。
バージョン2.1以降は2つ分の変更値が合算されるようになっているらしいです。
実際は合算させねばならないようなケースはあまりないでしょう。
基本的に0にして使って問題ないと思います。


③番外編、ccTouchMoveメソッド内でアクションを起動する処理を書かない
あまり気を使わずにゲームを作っていると、ccTouchMoveメソッド内でCCMoveTo系のアクションを起動するコードを書いちゃうこともあるかと思います。
ただ、ccTouch系のタッチデリゲートメソッドは、現在のゲームのループに割り込んで呼ばれます。
cocos2d-xは、毎フレーム、update系のループメソッドを実行することで動いているわけですが、ccTouch系のメソッド呼び出しは、それに対して割り込んできます。
ccTouchMoveメソッドの中でCCMoveToを起動していたりしたら、当然競合が発生します。


ていうか、1フレームの間に何度も呼ばれるメソッドでそんな処理するのはCPU負荷的に無駄です。
(2013-11-14追記:人に聞いた情報ですが、上記は誤りのようです。ccTouchMovedの頻度は、そんなに高くないらしいです。頻度はデバイスに依存し、測定した方によるとiOSで4~5回/秒、Androidで頻度が高い端末だと12回/秒くらいだったそうです。cocos2d-x界隈で有名な方に聞いた情報ですが、製品開発のときは必要であれば調査するのをおすすめします。)

ゲームの開発では、ユーザからの入力もフレーム単位でしか扱わないのが通常だと思います。
cocos2d-xでそれに従うなら次のようにするといいとおもいます。

1,TouchContainerクラスを用意する

class TouchContainer {
public:
CCTouch* touchBegan;
CCTouch* touchMove;
CCTouch* touchEnd;
CCTouch* touchCanceled;
};

2,ccTouch系のメソッドでは上記のクラスに引数のCCTouchの値をコピーするだけにする。

3,updateメソッドなどのフレーム処理で、上記の値を使って、タッチに対する処理を行うようにする。

これで、通常のゲーム開発と同じやり方でユーザ入力が扱えると思います。


今回の記事は以上です。

cocos2d-xでCocosDenshionのデバッグログを出力する方法

CocosDenshionのデバッグ出力マクロCDLOGは、デフォルトの条件付きコンパイルでは何も出力しません。
SimpleAudioEngineは、エラーになってもCDLOGでログを吐いて終了するだけで、他に何もエラー通知をしないメソッドを含んでいる(loadなど)ので、CDLOGを有効にした方がいいでしょう。

やり方は簡単。CDConfig.hで
#define CD_DEBUG 1
コメントアウトを外すだけです。

短いけどこれでこの記事は終わり!

cocos2d-xでメモリリークを防ぐ開発習慣

cocos2d-xはC++で開発するため、メモリ管理が自由にできてパフォーマンス最適化がしやすい反面、人為的ミスでメモリリークや解放処理ミスを起こしやすい環境だと言えます。

メモリリークや解放処理ミスは再現や修正が非常に難しいバグであり、開発工程終盤になればなるほど難易度が増します。
発生させたらすぐに発見できるような開発習慣をつけておくことが大切です。

この記事では、僕が良い習慣だと考えているものをあげます。
随時追記し説明を詳細化していく予定です。


①いろんなタイミングでシーンを開放してみる
Cocos2dxでシーンの解放は非常に簡単です。
今、GameSceneというシーンの中でのメモリリークを検知したいとしましょう。
適当なテスト用のシーンを用意しておき(GameOverScene)、GameScene内の任意の場所に以下を記載します。
CCDirector::sharedDirector()->replaceScene(GameOverScene::create());

これにより、GameSceneを開放した上でGameOverSceneに移行します。
シーン内で確保したメモリが全て解放されていればメモリリーク無しといえます。
解放処理に重大なミスがあると、EXC_BAD_ACCESSなどの例外で停止します。

常にメモリ使用量を表示しておくのも有効です。
GameScene起動前と解放後のメモリ使用量が異なっている場合はメモリリークが起きている可能性があります。

デストラクタは、いつ呼ばれようとも確保したメモリは全て解放できるように書くのがベストです。
いろいろな場所に上記のシーン移動処理を入れてみて、メモリリークが発生しないことを確認するといいでしょう。

メモリ使用量表示方法はsyuhariさんが紹介しておられます。
http://blog.syuhari.jp/archives/2352
こちらでは、CCDirectorを修正する方法をとっていますが、Cocos2d内部のファイルには手を入れたくない方が多いと思います。
そのため、専用のCCLabelTTF派生クラスを作成しました。
このブログの最後に記載しますのでお使いください。





メモリリーク箇所探索の手助けにInstrumentsのLeaksツールを使う
http://tks2.net/memo/?p=122
清水さんが使い方を簡単に紹介しておられました。
僕はまだC++でInstrumentsをつかったことがないので、使ってみてまたこの記事で共有します。





■メモリ使用量(残りメモリ量)表示クラスMemoryUsedMetor

(2013-07-17追記:CCLabelTTFを毎フレーム描画更新しているので描画処理負荷が大きいという指摘をいただいています。)

・使い方
貼り付けたいLayerに対して以下でaddChildします。
MemoryUsedMetor* memoryMetor = MemoryUsedMetor::create();
memoryMetor->setAnchorPoint(ccp(1, 0));
memoryMetor->setPosition(ccp(CCDirector::sharedDirector()->getWinSize().width, 0));
layer->addChild(memoryMetor);


・MemoryUsedMetor.h

#ifndef __MemoryUsedMetor__
#define __MemoryUsedMetor__

#include
#include "cocos2d.h"
USING_NS_CC;

class MemoryUsedMetor : public CCLabelTTF {
private:
bool init();
void update(float delta);
double getAvailableBytes();
double getAvailableKiloBytes();
double getAvailableMegaBytes();
public:
CREATE_FUNC(MemoryUsedMetor);
~MemoryUsedMetor();
};

#endif /* defined(__MemoryUsedMetor__) */



・MemoryUsedMetor.cpp
#include "MemoryUsedMetor.h"
#include
#import
#import

bool MemoryUsedMetor::init() {
if (!CCLabelTTF::initWithString("000.0", "Arial", 48)) {
return false;
}

scheduleUpdate();
return true;
}

MemoryUsedMetor::~MemoryUsedMetor() {
unscheduleUpdate();
}

void MemoryUsedMetor::update(float delta) {
char megaBytes[12];
sprintf(megaBytes, "%.1f MB", getAvailableMegaBytes());
setString(megaBytes);
}

double MemoryUsedMetor::getAvailableBytes() {
vm_statistics_data_t vmStats;
mach_msg_type_number_t infoCount = HOST_VM_INFO_COUNT;
kern_return_t kernReturn = host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&vmStats, &infoCount);
if (kernReturn != KERN_SUCCESS) {
return 0.0f;
}

return (vm_page_size * vmStats.free_count);
}

double MemoryUsedMetor::getAvailableKiloBytes() {
return getAvailableBytes() / 1024.0f;

}

double MemoryUsedMetor::getAvailableMegaBytes() {
return getAvailableKiloBytes() / 1024.0f;
}