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

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

【cocos2d】色が変化するライフゲージをボスにつけてみた

現行バージョンのBeeClusterはボスキャラのダメージがどれほどなのかがわからず戦っていて不安になるのでライフゲージをつけてみました。
また、ライフの残量に合わせて色を緑から赤に変化させることでより一層のダメージ感を演出しています。
まずは、動画をどうぞ。

クワガタの上にある緑色の棒がダメージを受ける毎に短く、赤く変化して行くのがわかると思います。

BeeClusterのキャラクター表示にはCCSpriteクラスを拡張したCollisionSpriteというクラスを使っています。今回はCollisionSpriteクラスにライフゲージを表示する機能を実装しました。なので、雑魚キャラにゲージを追加することも可能です。

ライフゲージの長さの変化はスプライトを伸縮させて実現しています。
ゲージとゲージの背景用にこのようなテクスチャーを用意しました。
f:id:takujidev:20130731205802p:plain
f:id:takujidev:20130731205809p:plain
真っ白の棒と真っ黒の棒です。真っ白の方は背景が白なので見えないと思います。
真っ白の方はライフゲージのバーで真っ黒の方はバーを見やすくするための背景です。
なぜバーに真っ白のテクスチャーを使うのかというと、色をCCTintToなどで好きな色に変化させたいからです。例えば緑色のテクスチャーを使う場合、緑の成分を減らすことはできますが、赤の成分を元のテクスチャーの色以上に増やすことはできません。白にしておけばどんな色にでも変化させられます。

ライフゲージをキャラクターに装備する場合、そのキャラクターにaddChildすると位置合わせが簡単そうに思えますが、実はそうでもないです。バーがキャラクターの回転に合わせてキャラクターの周囲を回転すると非常に見にくいからです。私は、SpriteBatchNodeにaddChildし、絶対座標で位置を指定しています。実際にはccpAdd関数でキャラクターの位置とバーのオフセット座標を加算しているだけです。

バーの色変化は直線ではなく、凸型の曲線にします。ここではsin, cos関数を使いました。もっと良い方法があるかもしれませんが、思いつきませんでした。
なぜ直線的だとマズいのかというと、別にマズくはないのですが、試してみると中間色が濁って汚かったからです。これを防ぐために緑と赤が混ざる部分では輝度を高めにし、発色を良くしています。
比較画像です。
直線変化の場合:(ソースコードコメントアウトしている方)
f:id:takujidev:20130731213047j:plain:w360

曲線変化の場合:
f:id:takujidev:20130731213127j:plain:w360
同じライフ残量ですが、曲線変化させた方が明るくて奇麗な色に見えますよね?

CollisionSpriteクラスのライフゲージに関する部分の実装です。

@implementation CollisionSprite {
    float _radius;
    float _attackPoint;
    float _life;
    
    BOOL _hasLifeGause;
    float _gaugeOffsetX;
    float _gaugeOffsetY;
    float _initialLife;
    __weak CollisionSprite* _lifeGauge;
    __weak CollisionSprite* _lifeGaugeBG;
}

// _lifeの値を使用しているのでイニシャライザでlifeの値を設定した後に呼ぶ
// ライフゲージを使う場合はhasLifeGaugeプロパティをYESにする。
- (void)initLifeGauge
{
    if (_hasLifeGauge == NO) return;
    _initialLife = _life; //初期ライフ値を保存しておく
    SpriteBatch* batch = [SpriteBatch sharedSpriteBatch];
    CollisionSprite* temp;
    temp = [CCSprite spriteWithSpriteFrameName:@"lifeGauge.png"];
    _lifeGauge = temp;
    [batch addChild:_lifeGauge z:2];
    temp = [CCSprite spriteWithSpriteFrameName:@"lifeGaugeBG.png"];
    _lifeGaugeBG = temp;
    [batch addChild:_lifeGaugeBG z:1];
    _lifeGauge.tag = kNoType;
    _lifeGaugeBG.tag = kNoType;
    _lifeGauge.anchorPoint = ccp(0, 0); //左下が原点
    _lifeGaugeBG.anchorPoint = ccp(0, 0);

    float maxWidth = 100.0; //ゲージの初期幅
    float scaleX = maxWidth / _lifeGauge.contentSize.width;
    float scaleY = 1.0; //取りあえず
    //x方向がキャラの真ん中に来るように
    _gaugeOffsetX = -_lifeGauge.contentSize.width * scaleX / 2; //ゲージ幅の半分左へオフセット
    _gaugeOffsetY = self.contentSize.height/2; //スプライトの高さの半分上へオフセット
    _lifeGauge.position = ccpAdd(self.position, ccp(_gaugeOffsetX, _gaugeOffsetY));
    _lifeGaugeBG.position = ccpAdd(self.position, ccp(_gaugeOffsetX, _gaugeOffsetY));
    _lifeGauge.scaleX = scaleX;
    _lifeGauge.scaleY = scaleY;
    _lifeGaugeBG.scaleX = scaleX;
    _lifeGaugeBG.scaleY = scaleY;
    [self updateLifeGauge];
}

- (void)updateLifeGauge
{
    if (_hasLifeGauge == NO) return;
    // 現在のライフのパーセンテージに合わせゲージのスケールを変化させる
    // _lifeGaugeBG.scaleXはscaleXの初期値として使用
    float newScale = _lifeGaugeBG.scaleX * _life/_initialLife;
    id scaleTo = [CCScaleTo actionWithDuration:0.5 scaleX:newScale scaleY:1.0];

    // 現在のライフのパーセンテージに合わせてゲージの色を変化させる
    GLubyte green = 255 * sin(M_PI_2 * _life/_initialLife);
    GLubyte red = 255 * cos(M_PI_2 * _life/_initialLife);
//    GLubyte green = 255 * _life/_initialLife;
//    GLubyte red = 255 - green;
    GLubyte blue = 0;
    id tintTo = [CCTintTo actionWithDuration:0.5 red:red green:green blue:blue];
    
    id spawn = [CCSpawn actions:scaleTo, tintTo, nil];
    [_lifeGauge runAction:spawn];
}

- (void)fadeOutLifeGauge
{
    if (_hasLifeGauge == NO) return;
    id fadeOut = [CCFadeTo actionWithDuration:0.5 opacity:0];
    id remove = [CCCallBlock actionWithBlock:^{
        [_lifeGauge removeFromParentAndCleanup:YES];
    }];
    id sequence = [CCSequence actions:fadeOut, remove, nil];
    
    id fadeOutBG = [CCFadeTo actionWithDuration:0.5 opacity:0];
    id removeBG = [CCCallBlock actionWithBlock:^{
        [_lifeGaugeBG removeFromParentAndCleanup:YES];
    }];
    id sequenceBG = [CCSequence actions:fadeOutBG, removeBG, nil];
    
    [_lifeGauge runAction:sequence];
    [_lifeGaugeBG runAction:sequenceBG];
}

- (void)updateLifeGaugePosition
{
    if (_hasLifeGauge == NO) return;
    _lifeGauge.position = ccpAdd(self.position, ccp(_gaugeOffsetX, _gaugeOffsetY));
    _lifeGaugeBG.position = ccpAdd(self.position, ccp(_gaugeOffsetX, _gaugeOffsetY));
}

使う側に対するインタフェースは次の通りです。
● キャラクターにライフゲージを実装する場合はイニシャライザでhasLifeGaugeプロパティをYESにし、initLifeGaugeメソッドを呼びます。このときキャラクターのlifeプロパティに初期値が入っている必要があります。

● ダメージを喰らってlife残量が変化した時にupdateLifeGaugeメソッドを呼びライフゲージの長さと色をlife残量にシンクロさせます。

● updateLifeGaugePositionは毎フレームコールし、キャラクターの位置とシンクロさせます。

● キャラクターがやられたときにはfadeOutLifeGaugeをコールします。これによりゲージはフェードアウトし解放されます。

基本的にはメソッドの呼び出しだけで使えるようになっていますが、キャラクターにより表示位置を調整したい場合はgaugeOffsetX, gaugeOffsetYプロパティで調整可能です。

- (void)initLifeGauge;
- (void)updateLifeGauge;
- (void)fadeOutLifeGauge;
- (void)updateLifeGaugePosition;

@property (assign) float radius;
@property (assign) float attackPoint;
@property (assign) float life;
@property (assign) BOOL hasLifeGauge;
@property (assign) float gaugeOffsetX;
@property (assign) float gaugeOffsetY;
@property (assign) float initialLife;
@property (weak, nonatomic) CCSprite* lifeGauge;
@property (weak, nonatomic) CCSprite* lifeGaugeBG;