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

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

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

【cocos2d】弾を撃ってみた

cocos2d

今回は弾を撃ってみました。
弾のクラスとしてMyBulletを作成。MySpriteのオプションスプライトから放射状に連射します。
こんな感じの画面になります。
f:id:takujidev:20130409221925p:plain

MyBulletクラスの実装ファイルはこうなりました。CCNodeを継承しています。
イニシャライザが受け取る引数は、
position: 弾を発射する位置
vector: 弾が進む方向のベクトル
speed: 弾のスピード(ピクセル/秒)
spriteFrame: スプライトフレーム
将来的にテクスチャアトラスから任意の画像を取り出せるようCCSpriteFrameでテクスチャを受けるようにしてみました。
スプライトを設定した後、ccpNormalizeでvectorの単位ベクトルを求めます。単位ベクトルは大きさが1のベクトルです。
ミサイルのように画像に向きがある弾にも対応できるよう、進行方向を向いて飛んで行くようにしました。cocos2dでは右向きのベクトルが0°なので、ccp(1, 0)をzeroDegreeとして定義しました。ただし、ccpAngleSigned関数はラジアンなので正確にはDegreeという名前は適切でなかったかもしれません。まあ、どちらも0の向きは一致していますので良しとしましょう。ccpAngleSigned関数は2つのベクトル方向の間の符号付き角度(ラジアン)を求めます。あとはCC_RANIANS_TO_DEGREESマクロでラジアンから度に変換しますが、cocos2dの0度が右向きなのに対し今回使うテクスチャは上向きの絵なので90度補正します。velocityは単位ベクトルに大きさ(速度=(1秒間に進む)ピクセル数)を掛けて求めます。

次にアクションを設定します。CCMoveByで1秒間にvelocity分移動するようにします。1秒後に止まって欲しくないのでCCRepeatForeverを使います。あとは[self runAction]で弾が発射されます。

updateメソッドでは弾が画面外に出て行ったかどうかのチェックをします。画面と同じ位置、大きさの長方形を用意し、弾がその範囲になければremoveFromParentAndCleanupで自分自身を親ノードから外します。他からの参照がなければ自分自身のオブジェクトがメモリ上から削除されます。パラメーターでYESを指定するとアクションを全て取り外してくれます。よくわかりませんが、アクションは通常のCCNodeとは扱いが違う(addChildしない)ので明示的に取り外す指定をしなければならないのでしょうか?なお、この画面外判定の一連のルーチンはLearn Cocos2d 2: Game Development for Iosに書かれているものを使わせていただきました。

#import "MyBullet.h"


@implementation MyBullet

- (id)initWithPosition:(CGPoint)position vector:(CGPoint)vector speed:(CGFloat)speed spriteFrame:(CCSpriteFrame *)spriteFrame
{
    if ((self = [super init])) {
        self.position = position;
        CCSprite* sprite = [CCSprite spriteWithSpriteFrame:spriteFrame];
        [self addChild:sprite];
        
        CGPoint unitVector = ccpNormalize(vector);      // 単位ベクトルを求める
        CGPoint zeroDegree = ccp(1, 0);                 // 0度のベクトル
        float bulletAngle = ccpAngleSigned(unitVector, zeroDegree); 
        self.rotation = 90.0 + CC_RADIANS_TO_DEGREES(bulletAngle); // 頭を進行方向に向ける
        CGPoint velocity = ccpMult(unitVector, speed);  // 単位ベクトルに速度を掛ける
        
        CCMoveBy* moveBy = [CCMoveBy actionWithDuration:1.0 position:velocity];
        CCRepeatForever* repeatForever = [CCRepeatForever actionWithAction:moveBy];
        [self runAction:repeatForever];
        [self scheduleUpdate];
    }
    return self;
}

+ (id)bulletWithPosition:(CGPoint)position vector:(CGPoint)vector speed:(CGFloat)speed spriteFrame:(CCSpriteFrame *)spriteFrame
{
    return [[self alloc] initWithPosition:position vector:vector speed:speed spriteFrame:spriteFrame];
}

- (void)update:(ccTime)delta
{
    CGSize screenSize = [CCDirector sharedDirector].winSize;
    CGRect screenRect = CGRectMake(0, 0, screenSize.width, screenSize.height);
    if (CGRectIntersectsRect(self.boundingBox, screenRect) == NO) {     // 画面の中にいない?
        [self removeFromParentAndCleanup:YES];                          // 親ノードから外れてオブジェクト解放
    }
}


@end

さて、MyBulletを使う側のMySpriteはこんな感じの実装になっています。initWithImageメソッドは前回とほぼ同じですが、弾を自動発射する処理を入れたいので[self schedule:@selector(shoot) interval:0.1];を追加しました。これにより、0.1秒毎にshootメソッドが自動的に実行されます。これは、scheduleUpdateのメソッド、インターバル指定可能版ですね。
で、shootメソッドででは_optionの画面上での絶対座標を求めます。前回も使ったccpRotateByAngle関数をつかいます。なぜこれが必要かというと、子ノードのpositionプロパティには親ノードからの相対位置が入っているからです。今回はCCSpriteBatchNodeを使わないコードをベースにしているのですが、結局面倒な位置計算が必要になってしまいました。なので、だんだん「じゃあCCSpriteBatchNodeを使おうかな」という気になってきました。
vectorは弾が飛んで行く方向です。_optionの位置からselfの位置を引くと画面中心から_optionに向かうベクトルが求められます。弾が飛んで行く方向はこの延長線上になります。あとは、テクスチャファイルを元にCCSpriteフレームを作成し、MyBulletインスタンスを生成します。弾は自分の位置についてきてほしくないので、自分の親であるHelloWorldLayerのインスタンスにaddChildします。

#import "MySprite.h"
#import "MyBullet.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];
        
        [self schedule:@selector(shoot) interval:0.1];
    }
    return self;
}

- (void)shoot
{
    CGPoint screenCoordinate = ccpRotateByAngle(ccpAdd(self.position,
                                                       ccpMult(_option.position, self.scale)), self.position, CC_DEGREES_TO_RADIANS(-self.rotation));
    CGPoint vector = ccpSub(screenCoordinate, self.position);
    CGFloat speed = 200;
    
    NSString* file = @"Icon-Small.png";
    CCTexture2D* texture = [[CCTextureCache sharedTextureCache] addImage:file];
    
    CGSize texSize = texture.contentSize;
    CGRect texRect = CGRectMake(0, 0, texSize.width, texSize.height);
    
    CCSpriteFrame* frame = [CCSpriteFrame frameWithTexture:texture rect:texRect];
    
    MyBullet* bullet = [MyBullet bulletWithPosition:screenCoordinate vector:vector speed:speed spriteFrame:frame];
    [self.parent addChild:bullet];
    
}


@end

次回はこのプログラムをベースに画面上に配置した敵オブジェクトに弾をぶつけてみたいと思います。