【cocos2d】CCActionを試してみる
未だにCCSpriteBatchNodeを使うかどうか迷っているのですが、とりあえず、CCSpriteBatchNodeを使わない自然なノード階層(node hierarchy)になおし、CCActionを使ってみたいと思います。
で、完成したのがこのMySpriteクラス。前回のプログラムと同様、メインのスプライトの横に半透明のスプライト(オプション)を配置します。前回と異なるのは2つのスプライトをバッチノードではなくselfにaddChildしているところです。これに伴い、イニシャライザでバッチノードのポインタを受け取る必要がなくなりました。
また、スプライトの位置決めも親ノードであるselfからの相対位置で指定します。前回はバッチノードからの相対位置なのですが、バッチノードの位置が(0, 0)なので=絶対位置となります。
そして、このプログラムではupdateメソッドがありません。スプライトの位置・角度・スケールは親ノード=selfの状態に合わせて自動的に更新されるからです。
そして、今回新たに実装したのがCCRotateByで始まるCCAction関連の設定です。
rotatebyは2秒かけて360°時計回りに回転するアクション
scaleUpは1秒かけて2倍に拡大するアクション
scaleDownは1秒かけて半分に縮小(=元のサイズ)するアクションです。scaleの値を1.0/2.0ではなく1/2とするとどんどん小さくなり消えてしまいますのでご注意を。1/2は整数で計算されるため小数点以下が切り捨てられ0になるからです。
これらのアクションを順番に実行したいので、sequenceに順番に渡し、終わりの印のnilを渡します。
そしてこのシーケンスを終わりなく繰り返したいので、repeatに渡します。
こんな感じでアクションを積み重ねて行き、ノードへのrunActionメッセージの引数として渡します。
するとそのノードは指定された通りに動き、その子ノードもつられて動きます。
cocos2dのアクション機能を使えば敵キャラの動きが簡単に作れそうです。
#import "MySprite.h" @implementation MySprite { CCSprite* _sprite; CCSprite* _option; } - (id)initWithImage { if ((self = [super init])) { _sprite = [CCSprite spriteWithFile:@"Icon-72.png"]; [self addChild:_sprite]; _option = [CCSprite spriteWithFile:@"Icon-72.png"]; [self addChild:_option]; _option.opacity = 255 * 0.5; //半透明にする CGSize screen = [CCDirector sharedDirector].winSize; self.position = ccp(screen.width / 2, screen.height / 2); _sprite.position = CGPointZero; _option.position = ccpAdd(_sprite.position, ccp(_sprite.contentSize.width * 2, 0)); self.contentSize = _sprite.contentSize; CCRotateBy* rotateby = [CCRotateBy actionWithDuration:2 angle:360]; CCScaleBy* scaleUp = [CCScaleBy actionWithDuration:1 scale:2.0]; CCScaleBy* scaleDown = [CCScaleBy actionWithDuration:1 scale:1.0/2.0]; CCSequence* sequence = [CCSequence actions:rotateby, scaleUp, scaleDown, nil]; CCRepeatForever* repeat = [CCRepeatForever actionWithAction:sequence]; [self runAction:repeat]; } return self; } @end
上のプログラムはノード階層構造的には自然なのですが、CCSpriteBatchNodeによる高速化の恩恵を受ける事ができません。そこで、CCSpriteBatchNodeを使って同じ機能を実装したのが下のプログラムです。イニシャライザに上のプログラムと同じアクションを実行するコードを追加しています。
こちらはスプライトの位置を自動的に更新してくれないのでupdateメソッドで更新してあげます。メインのスプライトの位置・角度・スケールは親ノードの値をそのままコピーします。オプションの方は少し厄介です。角度とスケールは親ノードの値そのままでいいのですが、位置は親の位置、角度、スケールすべてを駆使して求めます。これをやってくれるのがccpRotateByAngle関数です。
CGPoint ccpRotateByAngle(CGPoint v, CGPoint pivot, float angle)
Rotates a point counter clockwise by the angle around a pivot
Parameters:
v is the point to rotate
pivot is the pivot, naturally
angle is the angle of rotation cw in radians
Returns:
the rotated point
第1引数は動かしたいオブジェクトの位置です。文字で加来と(親のx座標+(オフセット*スケール), 親のy座標)となります。ccpAddはベクトルの足し算です。内部的には引数として渡される2つのCGPoint構造体のx座標とy座標をそれぞれ足しているだけです。
第2引数は回転の中心です。これは親の座標となります。
第3引数は回転角度です。時計回りの角度(cocos2dでは正の値)をラジアンで渡すと反時計方向に回転します。従いまして、時計回りに回すにはマイナスの値を渡します。なお、self.rotationの値は度(degree)なのでCC_DEGREES_TO_RADIANSマクロでラジアンに変換しています。
#import "MySprite.h" @implementation MySprite { CCSprite* _sprite; CCSprite* _option; } - (id)initWithImageAndAddToBatch:(CCSpriteBatchNode *)batch { if ((self = [super init])) { _sprite = [CCSprite spriteWithFile:@"Icon-72.png"]; [batch addChild:_sprite]; _option = [CCSprite spriteWithFile:@"Icon-72.png"]; [batch addChild:_option]; _option.opacity = 255 * 0.5; //半透明にする CGSize screen = [CCDirector sharedDirector].winSize; CGPoint myPosition = ccp(screen.width / 2, screen.height / 2); self.position = _sprite.position = myPosition; myPosition.x += _sprite.contentSize.width * 2; //オプションはちょっと右側に表示 _option.position = myPosition; self.contentSize = _sprite.contentSize; [self scheduleUpdate]; CCRotateBy* rotateby = [CCRotateBy actionWithDuration:2 angle:360]; CCScaleBy* scaleUp = [CCScaleBy actionWithDuration:1 scale:2.0]; CCScaleBy* scaleDown = [CCScaleBy actionWithDuration:1 scale:1.0/2.0]; CCSequence* sequence = [CCSequence actions:rotateby, scaleUp, scaleDown, nil]; CCRepeatForever* repeat = [CCRepeatForever actionWithAction:sequence]; [self runAction:repeat]; } return self; } - (void)update:(ccTime)delta { _sprite.position = self.position; _sprite.rotation = self.rotation; _sprite.scale = self.scale; _option.position = ccpRotateByAngle(ccpAdd(self.position, ccp(_sprite.contentSize.width * 2 * self.scale, 0)), self.position, CC_DEGREES_TO_RADIANS(-self.rotation)); _option.rotation = self.rotation; _option.scale = self.scale; } @end
どちらの方法でも同じ事はできましたが、やはり気になるのがCCSpriteBatchNodeを使ったときのノード階層の汚さです。例えば弾を例にとって考えると、弾が画面外に出たとき自分自身のオブジェクトをremoveFromParentAndCleanupメソッドで解放するだけでなく、保持するスプライトも同様に親(CCSpriteBatchNode)から外してあげる必要があります。たいした手間ではありませんが、美しさに欠ける気がします。ちなみに前回書いた親ノードをCCSpriteにしてSpriteBatchNodeにaddする方法はうまくいかない事がわかりました。CCSpriteBatchNodeにaddされる子供や孫はすべてCCSpriteでなくてはならないそうです。試しにCCNodeをaddしてみたところ次のエラーでクラッシュしました。
2013-04-08 20:56:40.530 spriteTestWithoutBatchNode[4183:c07] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'CCSprite only supports CCSprites as children when using CCSpriteBatchNode'
というわけで、まだSpriteBatchNodeを使うか否か決めかねていますが、次回は弾を発射したいと思います。せっかくなのでオプションから放射状に撃ってみたいですね。