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

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

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

【cocos2d】自作のパーティクルエフェクトを作ってみた

cocos2d

前回はcocos2dのあり物のパーティクルを使ってみましたが爆発というにはカラフルすぎたので自分でそれっぽいパーティクルエフェクトを作ってみたいと思います。パーティクルを作るのに便利なParticle Designerなどというツールもありますが、今回は自分でパーティクルのクラスを作って手動でパラメーターを設定して行きたいと思います。cocos2dのパーティクルにはkCCParticleModeGravity, kCCParticleModeRadiusという2つのモードがあり、設定できるパラメーターが一部異なります。今回はkCCParticleModeRadiusでエフェクトを作って行きます。こちらのモードはパーティクルが円形にあるいは渦巻き形に吐き出され、吐き出されたパーティクルは飛び散らずにその場で消えて行きます。kCCParticleModeGravityの方はもっと複雑な動きをつけられそうで、cocos2dのサンプルはこちらしか使っていないようです。kCCParticleModeGravityはまた今度試してみます。
で、今回作ったMyExplosionクラスのヘッダファイルがこちら。CCParticleSystemQuadというクラスを継承しています。CCParticleSystemというクラスもあるのですが、CCParticleSystemQuadの方が処理が速いらしいです。

@interface MyExplosion : CCParticleSystemQuad {
    
}

- (id)initWithTotalParticles:(NSUInteger)numberOfParticles;
- (id)init;
+ (id)explosion;

@end

そして、実装ファイルがこちら。
durationはパーティクルを吐き出す時間です。爆発し、だんだん中心に収束して行くような効果を出すため、endRadiusを0にしています。これにより、startRadiusの半径からだんだん小さくなって行きます。rotatePerSecondは1秒当たりの回転角度です。大きめの値に設定しないと渦巻いて行くのが見えてしまいます。また、rotatePerSecondVarでばらつきを与えることで人工的なパターンにならないようにします。positionTypeで親の動きにつられて動くかどうかを設定できますが、バッチノードにくっつける使い方だと多分意味をなさないような気がします。startSize, endSizeでパーティクルの大きさがどう変化するかを設定します。angleは今回のような高速回転させる使い方ではあまり意味がないです。lifeは個々のパーティクルが消えるまでの時間です。emissionRateはパーティクルをチョロチョロ出すのがドバッと出すのかを決めますが、totalParticlesとlifeから計算して出す事でいい塩梅に設定できるようです。色については適当に炎っぽい色になるよう赤を強めにしています。そして、爆発っぽく見せるのに一番重要なのがblendAdditiveです。これをYESにするとパーティクルが集まったときに色が合成され明るくなるので爆発っぽさが出せます。

@implementation MyExplosion

- (id)initWithTotalParticles:(NSUInteger)numberOfParticles
{
    if ((self = [super initWithTotalParticles:numberOfParticles])) {
        self.duration = 0.1; // パーティクルを吐き出す時間
        self.emitterMode = kCCParticleModeRadius; // 回転モード
        self.startRadius = 50.0; // 中心からエミッターへの距離(スタート時)
        self.startRadiusVar = 0.0;
        self.endRadius = 0.0; // 中心からエミッターへの距離(終了時)
        self.endRadiusVar = 0.0;
        self.rotatePerSecond = 100000.0; // 1秒当たりのエミッターの回転角度(degree)。かなり大きくしないと動いているのが見える
        self.rotatePerSecondVar = 1000.0; // ばらつきを与えないとパターンが見える
        
        self.position = ccp(0, 0); //親がバッチノードだと使う側で本当の親に合わせて設定する必要あり
        self.posVar = ccp(0, 0);
        self.positionType = kCCPositionTypeFree; // 親ノードに合わせて動かない。Batchノードは動かないので無意味?
        
        self.startSize = 10.0; // スタート時のパーティクルの大きさ
        self.startSizeVar = 0.0;
        self.endSize = 100.0; // 終了時のパーティクルの大きさ
        self.endSizeVar = 0.0;
        
        self.angle = 0.0; // エミッターの開始時の角度
        self.angleVar = 0.0;
        
        self.life = 0.4; // パーティクルが消えるまでの時間
        self.lifeVar = 0.0;
        
        self.emissionRate = self.totalParticles/self.life; // 1秒間に吐き出すスピード
        self.totalParticles = numberOfParticles; // 最大のパーティクル数
        
        self.startColor = (ccColor4F) {0.9, 0.5, 0.3, 1.0}; // スタート時のRGBα
        self.startColorVar = (ccColor4F) {0.2, 0.2, 0.2, 0.2};
        
        self.endColor = (ccColor4F) {0.0, 0.0, 0.0, 1.0}; // 終了時のRGBα
        self.endColorVar = (ccColor4F) {0.0, 0.0, 0.0, 0.0};
        
        self.blendAdditive = YES; // パーティクルが重なると明るく光って見えるブレンドモード
        
        self.texture = [[CCTextureCache sharedTextureCache] addImage:@"fire.png"];
    }
    return self;
}

- (id)init
{
    return [self initWithTotalParticles:340];
}

+ (id)explosion
{
    return [[self alloc] init];
}

@end

このMyExplosionクラスを使う、MyEnemyクラスの該当部分はこちらです。updateメソッドで当たり判定でコリジョンが検出されたらまずあたった相手のノードにそれを知らせます。次にunscheduleUpdateで次のフレーム以降updateが呼ばれないようにします。こうしないと数フレームに渡ってコリジョンが検出され、同じ処理が繰り返されてしまいます。一発で死なないような敵の場合は、unscheduleUpdateであたり判定をとめ、しばらくしてから再開する必要がありますね。
gotHitメソッドでは、まずMyExplosionのインスタンスを作成し、場所を設定します。そして、前回描いたとおり忘れずにautoRemoveOnFinishを設定し、パーティクルバッチノードのaddChildします。次に[self stopAllActions];でオブジェクトの動きを止めます。
スプライトがパッと消えると味気ないのでフェードアウトのアクションをいれました。その次のCCCallBlockアクションはフェードアウトが終わりこのアクションが実行される時点でブロック内の処理が実行するためのものです。ここでは、オブジェクトを親ノードから取り外してあげます。この敵オブジェクトを作ったHelloWorldLayerではインスタンス変数にこのオブジェクトを保持していないので、この時点でメモリから消去されます。例えば、一発で死なないオブジェクトの場合、フェードアウトではなく点滅アクションを入れ、その後でscheduleUpdateであたり判定を再開するような感じでCCCallBlockアクションを使えると思います。

- (void)update:(ccTime)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
{
    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];
}

実行画面です。結構それっぽい爆発になったと思います。
f:id:takujidev:20130414181403p:plain
せっかく絵で爆発を表現したのに無音では味気ないので、次回は爆発音のサウンドエフェクトを入れてみたいと思います。