第14回:ボスを作る(3)

 ボスに動きを持たせます。ワンパターンな動きではなく、ボスの残り耐久度に合わせて、いくつかの動きや攻撃方法を行わせるようにしましょう。今回、CBoss::Exec()が呼び出されるたびにボスがどのようなアクションをとるかの定義はenumにて行っています。
enum BossStatus
{
    boss_pattern1,    // 第一形態
    boss_pattern2,    // 第二形態
    boss_pattern3,    // 第三(最終)形態
    boss_explosion    // 爆発状態
};
    
class CBoss : public CEnemyBase
{
    /* 省略 */
    
private:
    BossStatus status;
    int frame;

    void Action1();            // 第一形態時のアクションを実行
    void Action2();            // 第二形態時のアクションを実行
    void Action3();            // 第三形態時のアクションを実行
    bool ActionDestroy();    // 爆発時のアクションを実行
                            // (戻り値trueでアクションが終了したことを伝える)
};
CBoss::CBoss(void)
{
    status = boss_pattern1;

    /* 省略 */
}

void CBoss::Exec()
{
    // 残り耐久度に合わせアクションを変更
    switch(status){
    case boss_pattern1: Action1(); break;
    case boss_pattern2: Action2(); break;
    case boss_pattern3: Action3(); break;
    default:
        if(ActionDestroy() == true){
            // 完了フラグが返されたら自身を消去
            RemoveObject(this);
            return;
        }
    }

    /* 省略 */
}
 まずはアクションパターンその1。関数内容を差し替えれば、いろいろなことができます。
void CBoss::Action1()
{
    // 真下に移動で静止
    if(y < 80.0f) y += 2.0f;

    // 砲台をプレーヤーに向ける
    bangle[0] = -atan2f(
        player->GetPosX() - (x - BUTTERY_X),
        player->GetPosY() - (y + BUTTERY_Y));
    bangle[1] = -atan2f(
        player->GetPosX() - (x + BUTTERY_X),
        player->GetPosY() - (y + BUTTERY_Y));

    frame++;
    if(frame == 80){
        ShootFromNozzle(1.0f);
        frame = 0;
    }
}
 続いてアクションパターンその2に移りますが、その前に状態遷移をする際のデータ初期化処理などを用意しましょう。Damaged()をオーバーライドし、耐久性が一定値に達したら、次のパターンに移行するようにします。
class CBoss : public CEnemyBase
{
private:
    // 移動に関する情報を格納する変数
    int moveflag1, moveflag2;
    float mx, my;

    void MiniExplosion();
    
public:
    virtual void Damaged();
};
void CBoss::Damaged()
{
    hardness--;
    // 耐久力が最大時の3分の2になれば次に移る
    if(status == boss_pattern1 && hardness <= BOSS_HARDNESS * 2 / 3){
        // 小規模の爆発で状態遷移をユーザーにアピール
        MiniExplosion();

        // データのリセット
        frame = 0;
        moveflag1 = 0;
        bangle[0] = bangle[1] = 0.0f;

        status = boss_pattern2;
    }
}

void CBoss::MiniExplosion()
{
    // 大小からなる30個の爆炎を拡散させる
    float xx, yy, a, s;
    for(int i = 0; i < 30; i++){
        xx = x + (float)(rand() % 300 - 150);
        yy = y + (float)(rand() % 100 - 50);
        a = atan2f(x - xx, yy - y);
        s = 2.0f + 4.0f * (float)rand() / (float)RAND_MAX;
        AppendObject(new CExplosion(xx, yy,
            2.0f, a, s), EXPLOSION_PRIORITY, true);
    }
}
 アクションパターンその2。中ボスの時と動きがほとんど同じですが、サンプルなので、あまり気にしないように:-)。
void CBoss::Action2()
{
    // 左右に移動
    if(moveflag1 == 0){
        x -= 0.5f;
        if(x < 100.0f) moveflag1 = 1;
    }else{
        x += 0.5f;
        if(x > 540.0f) moveflag1 = 0;
    }

    frame++;
    if(frame % 40 == 0){
        ShootFromNozzle(2.0f);
    }

    if(frame == 240){
        // 中央部分から拡散弾
        float range[] = {30.0f, 60.0f, 90.0f, 120.0f, 150.0f};
        for(int i = 0; i < 5; i++){
            AppendObject(
                new CEnemyBullet(x, y + 50.0f, d2r(range[i]), 3.0f),
                ENEMYBULLET_PRIORITY, true);
        }

        frame = 0;
    }
}
 最終形態では、指定した座標から座標への往復移動を施します。アルゴリズムとしては、A地点の座標からB地点の座標への距離を引き算で計算し、それをもとに1フレーム当たりの移動量を算出、その結果を変数mx,myに格納し、その値分フレームごとに移動。と、いうものにしています。なお、moveflag1には目標地点の配列箇所を、moveflag2には移動方向(往路か復路か)のフラッグが格納されています。

 配列内の数値を変えるだけでも、無数のバリエーションを持つ往復運動が行えるので、いろいろ数値を変更してみてください。
void CBoss::Action3()
{
    // W文字型に往復運動を行う
    const float movedata[][2] = 
    {
        {120.0f, 80.0f},
        {220.0f, 180.0f},
        {320.0f, 80.0f},
        {420.0f, 180.0f},
        {520.0f, 80.0f},
    };

    x += mx;
    y += my;

    // 座標は浮動小数点のため、誤差を考慮している
    if(x > movedata[moveflag1][0] - 0.5f
    && x < movedata[moveflag1][0] + 0.5f
    && y > movedata[moveflag1][1] - 0.5f
    && y < movedata[moveflag1][1] + 0.5f)
    {
        // 目標位置に到着

        // 誤差をなくす
        x = movedata[moveflag1][0];
        y = movedata[moveflag1][1];

        // 次の目標位置への移動量を計算
        if(moveflag2 == 0){
            mx = movedata[moveflag1 + 1][0] - movedata[moveflag1][0];
            my = movedata[moveflag1 + 1][1] - movedata[moveflag1][1];
        
            moveflag1++;
            if(moveflag1 == 4) moveflag2 = 1;
        }else{
            mx = movedata[moveflag1 - 1][0] - movedata[moveflag1][0];
            my = movedata[moveflag1 - 1][1] - movedata[moveflag1][1];
        
            moveflag1--;
            if(moveflag1 == 0) moveflag2 = 0;
        }

        // 速度を調整
        mx /= 80.0f;
        my /= 80.0f;
    }

    frame++;
    if(frame < 400){
        // 徐々に砲台を外側へ移動
        bangle[0] = d2r(75.0f * (float)frame / 400.0f);
        bangle[1] = d2r(-75.0f * (float)frame / 400.0f);
    }else if(frame < 600){
        int frame2 = frame - 400;

        // 一気に砲台を内側に戻す
        bangle[0] = d2r(75.0f - 140.0f * (float)frame2 / 200.0f);
        bangle[1] = d2r(-75.0f + 140.0f * (float)frame2 / 200.0f);

        // 砲台を戻しながら弾をばらまく
        if(frame2 % 15 == 0){
            ShootFromNozzle(1.0f);
        }
    }else if(frame < 1000){
        int frame2 = frame - 600;

        // 砲台を元の位置へ戻す
        bangle[0] = d2r(-65.0f + 65.0f * (float)frame2 / 400.0f);
        bangle[1] = d2r(65.0f - 65.0f * (float)frame2 / 400.0f);
    }else{
        frame = 0;
    }
}
 ボスである以上、散りざまも華々しくしたいもの。耐久性が0になると、爆破処理が行われる専用アクションをプログラム化します。
bool CBoss::ActionDestroy()
{
    frame++;
    if(frame < 500){
        // 500フレーム目まではボス周辺でランダムに爆炎を発生させる
        if(frame % 5 == 0){
            float xx = x + (float)(rand() % 500 - 250);
            float yy = y + (float)(rand() % 300 - 150);
            float sz = 1.0f + 2.0f * (float)rand() / (float)RAND_MAX;
            AppendObject(new CExplosion(xx, yy, sz, 0.0f, 0.0f),
                EXPLOSION_PRIORITY, true);
        }

        y += 0.2f;

        return false;
    }else{
        // 500フレーム目で大爆発を起こす
        float xx, yy, a, s, z;
        for(int i = 0; i < 50; i++){
            xx = x + (float)(rand() % 100 - 50);
            yy = y + (float)(rand() % 100 - 50);
            a = atan2f(x - xx, yy - y);
            s = 5.0f + 10.0f * (float)rand() / (float)RAND_MAX;
            z = 3.0f + 8.0f * (float)rand() / (float)RAND_MAX;
            AppendObject(new CExplosion(xx, yy,
                2.0f, a, s), EXPLOSION_PRIORITY, true);
        }

        return true;
    }
}
 最後に、CBoss::Damaged()に遷移処理を追加記述すれば、ボスに関するプログラムは完成です。
void CBoss::Damaged()
{
    hardness--;
    if(status == boss_pattern1 && hardness <= BOSS_HARDNESS * 2 / 3){
        /* 省略 */
    }else if(status == boss_pattern2 && hardness <= BOSS_HARDNESS / 3){
        MiniExplosion();

        // 移動フラグのセット
        frame = 0;
        moveflag1 = 2;
        moveflag2 = 0;
        mx = -(x - 320.0f) / 50.0f;
        my = -(y - 80.0f) / 50.0f;

        // 砲台位置のリセット
        bangle[0] = bangle[1] = 0.0f;

        status = boss_pattern3;
    }else if(status == boss_pattern3 && hardness <= 0){
        // 共倒れを防ぐため、プレイヤーを無敵状態にする
        player->SetNoEnemy(true);

        frame = 0;
        status = boss_explosion;
    }
}