XCode4でAd-hocビルドが作れないという話はけっこう出ているのだけど、自分で直面してみるとイマイチよく分からず。
「Skip Install」をYESにするといいよ、なんて話をいくつか目にしたのだけど、どこをどう設定すればいいのかこれまたよく分からず。
で、しばらく悩んでやっと分かったのは、「Cocos2d Library」の方について「Skip Install」をYESにする、ということでした。
仕組みをちゃんと分かってればなんてことないんでしょうが、ハマりポイントですねぇ。
プログラミングのことを書くぞ。I'm gonna write programming-related topics here.
XCode4でAd-hocビルドが作れないという話はけっこう出ているのだけど、自分で直面してみるとイマイチよく分からず。
「Skip Install」をYESにするといいよ、なんて話をいくつか目にしたのだけど、どこをどう設定すればいいのかこれまたよく分からず。
で、しばらく悩んでやっと分かったのは、「Cocos2d Library」の方について「Skip Install」をYESにする、ということでした。
仕組みをちゃんと分かってればなんてことないんでしょうが、ハマりポイントですねぇ。
■テクスチャアトラス作成ツールとの連携
テクスチャアトラスを駆使してアニメーションを実装する際には、アトラスの切り出し位置の管理が必要である。
各画像の配置が単純な場合は、切り出し位置を CGRectMake で直接指定すればよいので大した問題ではない。たとえば、横100px・縦150pxの絵が横に5枚並んだ .png ファイルから5枚のフレームを作成するには、
NSMutableArray* frames = [NSMutableArray initWithCapacity:5];
CCTexture* texture = [[CCTexture alloc] initWithImage:@"allFrames.png"];
for(int i=0; i<5; i++){
[frames addObject:[CCSpriteFrame frameWithTexture:texture
rect:CGRectMake(100*i, 0, 100, 150)]];
}
しかし、もっと切り出し位置が複雑になると、その管理が大変になることがある。
そうした場合、「TexturePacker」と「Zwoptex」といったアトラス作成ツールを使うと、この切り出し情報をplistファイルとして書きだしてくれるので、個別指定する手間が省ける。
そんなわけでいくつかのサイトではこうしたツールを使うことを前提でcocos2dアニメーションの実装方法を紹介しているが、実際のところは、以下で述べるように話が相当ややこしくなるし、コード量もかえって多くなるので、アトラス構造の複雑さや容量の余裕次第では、上記の基本的な方法で実現できないか検討した方が良いかもしれない。
そのあたりのメリット・デメリットは後述するとして、ひとまず解説。
アトラス作成ツールが作ってくれるのは、CCSpriteFrameに相当する切り出し位置情報などのパラメータ。これを、xml (.plist)で書きだしてくれる。
この plist を読み込むことで各CCSpriteFrameに与えるべき情報=フレーム情報が生成される。
・・・となると、plist に書かれた情報から CCSpraiteFrame 作ってそれを NSArray に格納してくれるような関数があるのを期待するが、残念ながらそういうものは用意されていない。
plistファイルを読み込んだ際に行われるのは CCSpriteFrameCache にフレーム情報を登録することである。
CCSpriteFrameCache はどう使うかというと、まず
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"frames.plist"];
としてフレーム情報を sharedSpriteFrameCache に読み込み、次にこの sharedSpriteFrameCache に対して
CCSpriteFrame* aFrame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"frameName"];
filenameN.ext
(filename:元の画像名 , N:番号 , ext:元の画像の拡張子)
となっている。例えば、元画像名 frame.png の 3 番目のフレーム名は
@"frame3.png"
である。
拡張子が付いてくるのでファイル名みたいに見えるが、単なる名前である。(当初自分はサンプルコードだけ追いかけて使い方を探っていたのでここがすごく混乱した)
さて、これがきわめて不毛なのだが、この sharedSpriteFrameCache から1つずつフレーム情報を引き出して
NSArrayに格納する作業が必要なのである。
たとえば5フレームある場合はこんな感じ。
NSMutableArray *animFrames = [NSMutableArray array];
for(int i = 1; i <= 5; ++i) {
[animFrames addObject:
[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:
[NSString stringWithFormat:@"frame%d.png", i]]];
}
id anim = [CCAnimation animationWithFrames:animFrames];
とし、さらに CCAnimate を使ってアクションオブジェクトに変換した上でスプライトの runAction に指定する
CCAnimate* animAction = [CCAnimate actionWithAnimation:anim];
[sprite1 runAction:animAction];
■どういう場合に使うべきか
以上のようにけっこうな手間がかかるので、単にコマアニメーションを1つ作るのにツール連携を利用するのは正直不毛だと思う。
一般的に考えて、ツール連携を使うメリットがあるのは次のような場合だろうか。
*大量の異なるスプライトを利用する必要があり、かつきちんと名前ベースでフレームを管理する必要がある
*一つのアトラスにサイズの違う多種類のキャラクターの複数コマのアニメーションが入っている等、切り出し情報の管理が複雑
*すでに上記の手順がメソッド化されている。もしくは、今回作っておくと今後繰り返し利用が見込める
*アニメーションとは関係なくとも、できるだけコンパクトに1つのアトラスに画像を詰め込みたい
ちなみに、zwoptexもTexturePackerも、画像を等間隔で並べる操作が簡単で、photoshopでグリッドを駆使したりしながら並べるよりかなり効率がいいので、冒頭の例のように機械的に数値指定で切り出す際も画像並べツールとして利用できる。
■CCSpriteBatchNode
コマアニメーションを行う方法をWebで検索すると、CCSpriteBatchNode(旧CCSpriteSheet)を使う方法が紹介されているページにたどり着くことが多い。
確かに、後述のように有利な点も多いので利用価値はあるし推奨もされているのだが、利用するメリット、および利用の手順がなぜそうなるのか、を理解するには、「CCSpriteBatchNode の本来の目的はコマアニメーションではない」という点を知っていた方がいいだろう。
OpenGLにはバッチといわれる仕組みがあり、これを用いると、一回のOpenGL命令で同じ内容のテクスチャを大量に描画することができる(・・・らしい。公式ドキュメントにそう書いてあったので受け売りです; でも共通なテクスチャの描画処理をまとめれば、高速化できるというのはなんととなく分かる。)
で、同様の仕組みを cocos2d でも利用するために導入されたのが CCSpriteBatchNode である。
ところで、スプライトというのは、親ノード(CCLayerなどのCCNode系クラス)の child として登録し([parentNode addChild:aSprite]等)、親から描画依頼を受けるという使い方をする。
CCSpriteBatchNodeも、こうしたスプライトと同列に child として登録して使うのだが、描画依頼を受けても自分では描画せず、自身のchildに描画依頼を投げる。
[CCNode]─(描画依頼)┬→[CCSprite]
├→[CCSprite]
├→[CCSprite]
├→[CCSpriteBatchNode]─(描画依頼)┬→[CCSprite]
├→[CCSprite] ├→[CCSprite]
・ ├→[CCSprite]
・ ・
・ ・
このように描画依頼を child に投げるオブジェクトというのは、要は CCNode そのものであって(*1)、CCSpriteBatchNode は、それに加えてテクスチャ情報も持っているノードということになる。
CCSpriteBatchNode に child として登録したスプライトは、テクスチャが共通しているという前提のもとで描画処理を行えるので、特に数が多い場合 普通のCCNodeより高速に描画できる。
*1)なのでこんな回りくどい説明は本来必要ないのだが、自分が最初「CCSpriteBatchNodeは共通のテクスチャを共有するノードである」という説明を読んだときは何のことやらチンプンカンプンだったので、あえて説明してみた次第である。
ちなみに、以上の説明は、リファレンスでは次の一文で済まされている。
CCSpriteBatchNode is like a batch node: if it contains children, it will draw them in 1 single OpenGL call (often known as "batch draw").
以上を踏まえると、こういうコードになるのも理解されるだろう。
//テクスチャの生成
CCTexture2d* texture = [[CCTexture2d alloc] initWithImage:@"texture.png"];//バッチノードの作成・シーンへの追加
CCSpriteBatchNode *batch = [CCSpriteBatchNode
batchNodeWithTexture:texture];
[self addChild:batch];//スプライトの作成・batchへの追加
sprite1 = [CCSprite spriteWithTexture:texture rect:rect1]; //バッチノードのテクスチャと同じでなければならない
[batch addChild:sprite1]; //batchNodeのchildとして追加。sprite1はbatchから描画依頼を受ける。
//異なるテクスチャが指定してあるとここでエラーとなる。
以下のように2個目・3個目も追加できる。もちろん、テクスチャは共通していなければならない。
この個数が増加してくるとバッチノードのメリットが生きてくるということになる。
sprite2 = [CCSprite spriteWithTexture:texture rect:rect2];
[batch addChild:sprite2];
sprite3 = [CCSprite spriteWithTexture:texture rect:rect3];
[batch addChild:sprite3];
・
・
このように、本来バッチノードはコマアニメーションやアトラス切り出しとは特に関係ない概念であり、CCSpriteBatchNodeを使うメリットは、あくまで大量のスプライトを扱う場合にパフォーマンスが良くなる点である。
したがって、バッチノードを作ってさらにそこにスプライトを追加、という風に一手間増えることを考えると、一度にそれほど多くのスプライトを使用しない、もしくはパフォーマンスを気にしないという場合無理して使う必要はないといえる。
逆に、アニメーションやアトラス切り出しが必要ない場面でも、同じスプライトを大量に使用する場合には積極的に使うべきだろう。
ちなみに、上記ではテクスチャが共通であることを分かりやすくするために CCTexture2d を明示的に作成しているが、
CCSpriteBatchNode *batch = [CCSpriteBatchNode batchNodeWithFile:@"texture.png"];
のようにすればテクスチャ作成(とテクスチャキャッシュへの登録)を自動的に行ってくれる。
この場合、batchに設定されているテクスチャは batch.textureAtlas で参照することができる。
また、CCSpriteBatchNode に 追加するスプライトの作り方として次のメソッドも用意されている。
sprite4 = [CCSprite spriteWithBatchNode:batch rect:rect4];
必ずバッチノードが持ってるテクスチャから切り出してスプライトを作るわけなのでこういうメソッドがあるのは親切なのだが、どうせならバッチノードに addChild までやってくれるともっと楽な気がするのは僕だけだろうか・・・。
■CCSpriteFrameによるコマアニメーション
アニメーションは、一定時間ごとにスプライトの画像情報を変更させることなので、考え方としては前回説明したアクション系クラスと同じである。
そのためのクラスとしては CCAnimation が用意されており、これに何コマ目にどの画像を表示させるかを指定すればよい。
単純に考えれば画像ファイル名が記述された配列でも渡せば済みそうだが、ここでもテクスチャアトラスの考え方がベースにあるため、テクスチャ名と切り出し情報の組み合わせによって各コマの情報を指定することになる。
その1コマ分に相当するのが CCSpriteFrame である。
CCTexture2D* texture = [[CCTexture2d alloc]
initWithImage:@"texture.png"]; //テクスチャを読み込み※1
CCSpriteFrame* frame1 = [CCSpriteFrame frameWithTexture:texture rect:rect1]; //※2
CCSpriteFrame* frame2 = [CCSpriteFrame frameWithTexture:texture rect:rect2];
.
.
このようにすると各コマの情報を生成できる。
これら複数の CCSpriteFrame を NSArrayに格納して CCAnimationに渡すことで画像切り替え情報、つまりアニメーションの内容がセットされる。
NSArray* animFrames = [NSArray arrayWithObjects: frame1, frame2, …. , nil];
CCAnimation *anim = [CCAnimation animationWithFrames:animFrames delay:0.1f];
この CCAnimation がアクション系オブジェクトとしてそのまま使用できれば良いのだが、クラスの継承関係上は別物となっており、アクションとして使用する際にはアクションオブジェクトである CCAnimate の形に変換してから runAction: する必要がある。
CCAnimate *animate = [CCAnimate actionWithAnimation:anim];
[aSprite runAction:anim];
以上の操作によりコマアニメーションが動作する。
※1)もちろんテクスチャキャッシュを使用してもよい
※2)CCSpriteFrameの初期化コードがCCSpriteと似ているのでなんとなく同じもののような気がしてしまうが(少なくとも僕は違いが理解できず???となった)、CCSpriteFrameはあくまで各コマの情報更新なのでscaleやpositionの情報を持っていないし(CCSpriteFrame.hを見れば、実際それしか変数を持っていないのがわかる)、CCNodeにaddChildすることなどもできない。
■各種アクション系クラスの考え方
先の記事で触れた通り、スプライトのパラメータ変更は即時に表示へ反映されるので、位置やスケール・アルファを時々刻々変化させたり、あるいはテクスチャ切り出し場所やテクスチャそのものを順々に入れ替えたりすれば、スプライトを動かすことができる。
なので、タイマーを設定して一定時間ごとにスプライトの状態を変化させるコードを自前で書けば動きを作ることは可能である。
が、スプライトの数が多くなったり動きが複雑になると管理が複雑になってくるし、何より面倒くさい。
そうした場合には各種アニメーション用のクラスを利用すると、上述のような定期的な状態変更を自動で実行してくれるので、簡単にアニメーションを実現できる。
たとえば「スプライト aSprite の scale を1秒間の間に 1.0 から 2.0 に変化させる」は次のように記述する。
CCSprite* sprite1 = [CCSprite spriteWithFile:@"dog1.png"]; //スプライト作成
CCActionTween* scaleAction1 = [CCActionTween actionWithDuration:1.0 key:@"scale" from:1.0 to:2.0]; // アクションオブジェクト※1
[sprite1 runAction:scaleAction]; // パラメータ操作を受けるスプライトを指定※2
CCActionTweenは、パラメータ変化を定期的に実行するためのクラスで、key: を変更すればスケールや位置・アルファなど任意のパラメータを操作できる。
このようにすると、一定時間ごと(この場合1フレームごと)に scaleAction が sprite1 のパラメータ変更メソッド([sprite1 setScale: ])を呼び出し、結果的に sprite1 の拡大アニメーションが実現する。
他のアニメーション系クラスも同様に一定時間ごとに指定したスプライトのパラメータを変化させる仕組みになっている。
cocos2dの用語では、こうしたアニメーションを総称してアクションと呼び、その中で特にテクスチャを入れ替えるコマアニメーションだけをアニメーションと呼ぶ。
以下もそれにならうことにする。
※1)cocos2d付属のサンプルを見ると、アクション系クラスの宣言は、 id scaleAction1 = … などと id で宣言されていることが多い。そのようにしている理由は調べてないので分からないが、個人的な感想として、アクションを合成したりいろいろやってるとコロコロとクラス名が変わるので id で宣言したくなる気分にはなる。
※2)これも個人的な感想だが、 scaleAction が sprite1 に命令するという構図なのにコード上は「sprite1 に scaleAction を付加する」という形式になっているのがちょっと違和感。
CCAction に startWithTarget:target というメソッドがあるので これを使ってもいいのだが、あくまで cocos2d の作法に則った方がいろいろと都合が良いので頭の中で読み替えるにとどめてます。
■アクションのターゲットについて
CCAction.h を見ると、アクションがどのスプライトを操作するのか=すなわちターゲットの定義は
id target_;
[sprite1 runAction:action];
[sprite2 runAction:action];
action.target_ = sprite1;
action.target_ = sprite2; //(実際にはできません)
■アクションの合成
アクション系クラスは、パラメータ変化を命令するだけが仕事なので、同時にいくつも runAction すればスプライトのパラメータが同時に何種類も変更され、結果的にアクションの合成ができる。
ちょうど、上述の関係と逆である(「アクション1個 を 複数スプライトにかける」は不可だが、「複数アクション を スプライト1個にかける」は可能)。
あえて明示的に合成したい場合などは、CCSpawnを使うと合成済みのアクションオブジェクトを取得できる。スプライトの runAction には合成済みアクションを指定すればよい。
id doubleAction = [CCSpawn actionOne:action1 two:action2];
[sprite1 runAction:doubleAction];
さらに、合成とは異なるが、「繰り返す」「このアクションの後にこのアクション」とかいったことも指定できる。
id newAction1 = [CCRepeat action:anAction times:times]; // 繰り返し
id newAction2 = [CCSequence actions:action1, action2,nil]; // action1の次にaction2を実行
また、CCCallFunc と CCSequence を併用すると終わった後に Objective-C のメソッドを呼ぶことも可能だ。例えば以下のようにすれば action1 と action2 が実行された後に self の finishMethod が呼ばれる。
id callFuncAction = [CCCallFunc actionWithTarget:self selector:@selector(finishMethod)];
id newAction3 = [CCSequence actions:action1, action2, callFuncAction, nil];
■画像ファイルとテクスチャ
テクスチャを使用する際には、使用するたびに画像をファイルから読み出すのではなく、まずメモリ上にビットマップとして展開して、それを読み出して使用することになる。
なので、画像を指して "テクスチャ" と言った場合には このメモリ上のビットマップを指している。
前回の記事で示した方法でスプライトを作る場合、内部的には画像を元にテクスチャをメモリ上に作成→そのテクスチャを指定してスプライト作成という処理が行われている。つまり、ごく大雑把にはこのような内容である。
CCTexture2d* texture = [[CCTexture2d alloc] initWithImage:@"texture.png"];
sprite1 = [CCSprite spriteWithTexture:texture rect:rect1];
■テクスチャキャッシュ
spriteWithFile: を使った場合の挙動は、単なるテクスチャ作成に加えてテクスチャキャッシュ(CCTextureCache)への登録という処理も含まれるので、
CCTexture2d* texture = [[CCTextureCache sharedTextureCache] addImage:@"texture.png"];
sprite1 = [CCSprite spriteWithTexture:texture rect:rect1];
とするとより近い挙動となる。
テクスチャキャッシュは、メモリ上にあるテクスチャへのポインタをまとめて持ってくれている仕組み。アプリの起動中ずっとインスタンスが存在しているので、アプリ内のどこからでも参照することが出来る(いわゆるシングルトン)。
なので、違うシーンで同じテクスチャを使いたい場合など、一度テクスチャを作ってテクスチャキャッシュに登録すれば、簡単に使い回しができる。
上記の addImage: はテクスチャの作成とキャッシュへの登録を同時に行ってくれるメソッドということになる。
便利ではあるが、不要なテクスチャの掃除などは自動では行ってくれないので、メモリが厳しい場合は適宜
[[CCTextureCache sharedTextureCache] removeUnusedTextures];
などとして使わなくなったテクスチャを破棄する必要がある。
あまり使い回しをしない場合(シーンが変わると使うテクスチャもほとんど入れ替わる場合など)は、むしろ自前でテクスチャをalloc/releaseした方がメモリの管理としては合理的かもしれない。
その場合都度画像読み込みのオーバーヘッドがかかるのがデメリットである。
公式ドキュメントなどでは基本的にテクスチャキャッシュの利用が
推奨されてはいるが、それはそれとしてあくまでバランスを考えて選ぶことが必要、と思っておいた方がいいだろう。
なお、他にも initWithImage 系の初期化メソッドは自動的にキャッシュを利用するので、テクスチャの生成・破棄を自分でやりたい場合には留意しておきたい。
■テクスチャアトラスについて
ゲームキャラのコマアニメーションのように小さい画像がたくさん必要な場合、一つの画像ファイルに複数の絵を押し込めて
使用時に切り出すという方法がよく用いられる。
これは、描画パフォーマンス上の利点があるほか、画像ファイル数が異様に増加してしまって面倒なことになるのを避けることができる。
この複数の絵が入った画像をテクスチャアトラス(以下アトラス)と呼ぶ。
CCSpriteはアトラスの使用を前提としているため、必ずテクスチャの切り出し情報も保持している。
アトラスから切り出してスプライトを作る場合はこんな感じ。
//画像の左上から 100px×100px部分を切り出してスプライトを作成
CGRect rect1 = CGRectMake(0, 0, 100, 100);
CCSprite* aSprite = [CCSprite spriteWithFile:@"texture.png" rect:rect1];
前回の最も単純な例では rect:を指定しなかったが、この場合画像の大きさがそのまま切り出し範囲となる。
cocos2dのアニメーション周りがけっこうややこしくて理解するのに苦労したので、使い方・・・というよりはどうしてそういう使い方になるのか、といったあたりを 自分で理解する目的も含めまとめてみようと思う次第。
コードは実際に動作してるものを切り取ってきてるわけではないので、間違い多いかも知れません。
■そもそもスプライト
OpenGLで二次元画像を表示しようと思うと、テクスチャ画像読み込み→頂点座標を指定して面を作成→面にテクスチャを貼り付け・・・ということを長々とOpenGL命令で指定する必要があり、さらにそれを移動・拡大・縮小・・・などやろうとすると同じくらい面倒な手続きが延々と必要となる。
cocos2dのスプライトはそのあたりをまとめてやってくれる非常に便利な仕組み。
自分は OpenGL の入門書を開いた時点で挫折したクチなので、これはものすごく助かる。
「テクスチャ」はもともと3D描画でポリゴンの面に貼り付ける画像のことであるが、2D描画の場合も正面向きのポリゴンを作ってそれに画像を貼り付けるという処理を行っているので、同じようにテクスチャと呼ばれる。
スプライトの最も単純な作り方は、
CCSprite* aSprite = [CCSprite spriteWithFile:@"texture.png"];
でOK。この場合、画像の大きさに対応したスプライトができる。これを
[self addChild:aSprite];
などとして現在のシーンに追加すれば描画が完了する。
スプライトは、位置・スケール・アルファ、それにテクスチャ画像への参照と切り出し情報(後述)などを保持しており、これらは変更すればすぐに反映されるので、移動や変形などは簡単に行える。たとえば位置を変えたいなら
aSprite.position = CGPointMake(160, 240); //(*1)
などとすればよい。
*1)CGPointMakeを省略した ccpというマクロが用意されているのでそれを使うとタイプ量を軽減できる。