読者です 読者をやめる 読者になる 読者になる

夏までにiPhone アプリつくってみっか!

趣味でiPhone/Androidアプリを開発し、日々勉強した事を書いています。オープンワールド系レースゲームをUnityで開発中です。

【cocos2d】CCActionを試してみる

cocos2d

未だに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を使うか否か決めかねていますが、次回は弾を発射したいと思います。せっかくなのでオプションから放射状に撃ってみたいですね。