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

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

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

【cocos2d】ぎこちないCCActionの動きを滑らかにしてみた Part 3

cocos2d Objective-C Steering Behaviors

前々回前回で作成したSteering BehaviorsのArrivalアルゴリズムを実装したMySteeringクラスを2つの方法で使ってみたいと思います。
1つ目は継承です。これまで作ってきたMyEnemyクラスを修正し、MySteeringクラスのサブクラスに変更します。MyEnemyクラスの内部でターゲットとなるオブジェクト生成し、これを追いかけるように設定します。
そして、2つ目は使用です。MyShipクラスでMySteeringクラスのインスタンスを生成し、自分を追いかけるように設定します。
実行画面はこんな感じになりました。

ピンク色のキャラクターは1つ目の継承で作成したMyEnemyクラスのオブジェクトです。ターゲットとなるオブジェクトにはテクスチャーを設定していないため表示されていません。当たり判定も設定していません。
実装ファイルは次のようになっています。
コンビニエンスコンストラクタとイニシャライザに処理が分かれているためわかりにくいですが、MySteeringクラスが必要とする情報は全てコンビニエンスコンストラクタの中で用意しています。maxAccelは60/60.0, maxSpeedは600/60.0, brakeDistanceは100.0に設定しました。maxAccelとmaxSpeedの値を60.0で割っているのは人間のわかりやすさのためです。"/frame"の単位だとあまりピンと来ませんが60.0で割って、分子だけを見れば毎秒の値として考える事ができます。なお、60ではなく60.0で割っているところにご注意ください。60で割ると整数で計算され少数部は切り捨てられるので、例えばば50/60は秒間50ピクセルではなく秒間0ピクセルという計算結果になり、前回NSAssertの説明したとおり、エラーでプログラムが停止します。
イニシャライザでは受け取ったtargetオブジェクトをインスタンス変数の_targetにいれますが、この_targetは__weak指定にしてあります。cocos2dではノードは必ず親ノードに保持されますのでインスタンス変数に入れる場合は基本的に__weakにしておくのが良いと思います。そして、その親ノードとなるのが新たに作ったMyParentNodeクラスのシングルトンインスタンスです。MyParentNodeクラスはCCNodeを継承した子ノードを保持する以外には何もしないクラスです。バッチノードにaddChildすると当たり判定のfor...inループで無駄にチェックされるので避けました。また、selfにaddChildするとselfを原点としてしまうのでこれもダメです。
deallocでは_targetが参照するオブジェクトを削除するため親ノードから外してあげます。selfを親から外すときについでに_targetを外すのではなく、わざわざdeallocメソッドを作るのには理由があります。selfはHelloWorldLayerから生成されるのですが、もしかするとHelloWorldLayerが何らかの理由でselfをremoveするかも知れません。deallocに_targetを親から外す処理を入れておけば確実にオブジェクトは削除されますが、そうでない場合はメモリを占有し続けます。

@implementation MyEnemy {
    __weak CollisionSprite* _target; // 親ノード(MyParentNode)が保持するのでここはweakで参照
}

- (id)initWithPosition:(CGPoint)position frame:(CCSpriteFrame *)frame action:(CCAction *)action target:(CollisionSprite *)target maxAccel:(float)maxAccel maxSpeed:(float)maxSpeed brakeDistance:(float)brakeDistance
{
    if ((self = [super initWithTarget:target frame:frame maxAccel:maxAccel maxSpeed:maxSpeed brakeDistance:brakeDistance])) {
        self.radius = self.contentSize.width/2;
        self.tag = kEnemy;
        _target = target;
        [[MyParentNode sharedParentNode] addChild:_target];
        [_target runAction:action];
    }
    return self;
}

-(void)dealloc
{
    [_target removeFromParentAndCleanup:YES];
}

+ (id)enemyType2WithPosition:(CGPoint)position
{
    CCSpriteFrame* frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"Icon-72-red.png"];
    id moveBy = [CCMoveBy actionWithDuration:0.6 position:ccp(250, 0)];
    id moveBy2 = [CCMoveBy actionWithDuration:0.3 position:ccp(0, -200)];
    id moveByRev = [moveBy reverse];
    id moveBy2Rev = [moveBy2 reverse];
    id sequence = [CCSequence actions:moveBy, moveBy2, moveByRev, moveBy2Rev, nil];
    id repeat = [CCRepeatForever actionWithAction:sequence];
    CollisionSprite* target = [CollisionSprite node];
    target.position = position;
    return [[self alloc] initWithPosition:position frame:frame action:repeat target:target maxAccel:60/60.0 maxSpeed:600/60.0 brakeDistance:100.0];
}

- (void)update:(ccTime)delta
{
    // 当たり判定
    [super update:delta];
    CCArray* array = [MySpriteBatch sharedMyBatch].children;
    CollisionSprite* hitObject = [[MyCollision sharedMyCollision]
                                  checkCollisionBetween:self andType:kMyShip | kMyBullet inArray:array];
    if (hitObject != nil) {
        [hitObject collisionDetectedWith:self];
        [self unscheduleUpdate];   // 連続して当たり判定しないようupdateを止める
        [self gotHit];
    }
}

- (void)gotHit
{
    [[SimpleAudioEngine sharedEngine] playEffect:@"explosion.caf"];
    MyExplosion* particle = [MyExplosion explosion];
    particle.position = self.position;
    particle.autoRemoveOnFinish = YES;
    [[MyParticleBatch sharedMyBatch] addChild:particle];
    [self stopAllActions];  // 動きを止める
    id fadeOut = [CCFadeOut actionWithDuration:0.2];
    // フェードアウトが終わったら親ノードから外れる
    id callBlock = [CCCallBlock actionWithBlock:^{
        [self removeFromParentAndCleanup:YES];
    }];
    id sequence = [CCSequence actions:fadeOut, callBlock, nil];
    [self runAction:sequence];
}

- (void)collisionDetectedWith:(CollisionSprite *)sender
{
// 今のところ何もしない
}

@end

つぎは、MySteeringクラスを使用しているMySpriteクラスの実装です。MySteeringクラスのインスタンスを一旦ローカル変数tempOptionで受けてからインスタンス変数_optionに入れていますが、これは念のためです。なぜこうしているのか、詳細はこちらの記事をご覧ください。_optionは高速化のためスプライトバッチノードにaddします。自分自身のオブジェクトが削除されるときに確実に自分が管理するMySteeringのインスタンスを解放するためdeallocでremoveFromParentしています。

@implementation MyShip {
    __weak CollisionSprite* _option;
}

- (id)init
{
    if ((self = [super initWithSpriteFrameName:@"Icon-72.png"])) {
        self.radius = self.contentSize.width/2;
        self.tag = kMyShip;
        CGSize screen = [CCDirector sharedDirector].winSize;
        self.position = ccp(screen.width / 2, screen.height / 2);

        CCSpriteFrame* frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"Icon-72.png"];
        CollisionSprite* tempOption = [MySteering steeringWithTarget:self frame:frame maxAccel:100/60.0 maxSpeed:300/60.0 brakeDistance:200.0];
        _option = tempOption;
        [[MySpriteBatch sharedMyBatch] addChild:_option];
        _option.opacity = 255 * 0.5;
        _option.tag = kNoType;
        
        [self schedule:@selector(shoot) interval:0.5];
    }
    return self;
}

- (void)dealloc
{
    [_option removeFromParentAndCleanup:YES];
}

- (void)shoot
{
    [[SimpleAudioEngine sharedEngine] playEffect:@"missile.caf"];
    CGPoint vector = ccpForAngle(-CC_DEGREES_TO_RADIANS(_option.rotation - 90.0));
    CGFloat speed = 500.0;
    
    CCSpriteFrame* frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"Icon-Small.png"];
    
    MyBullet* bullet = [MyBullet bulletWithPosition:_option.position vector:vector speed:speed spriteFrame:frame];
    [[MySpriteBatch sharedMyBatch] addChild:bullet];
    
}

- (void)collisionDetectedWith:(CollisionSprite *)sender
{
    [self removeFromParentAndCleanup:YES];
    
}

@end

そろそろ縦スクロールシューティングゲームらしく縦持ちに変更したいと思います。また、背景のスクロールにも手を付けたいと思います。