第23回 野符「武烈クライシス」 (1)

前回までは、使い魔のないスペルばかりを作ってきました。今回からは使い魔のいるスペルを作ってみたいと思います。

要旨

それではいってみましょう。

敵スクリプト

東方永夜抄から、使い魔という仕様が追加されました。使い魔はある敵に従属した敵であり、使い魔の親分を倒せば使い魔も倒されます。使い魔を攻撃すれば、親分にもダメージが与えられます。他にも点アイテムや刻符に関する仕様がありますが、今のところそのあたりは気にしないことにします。

使い魔によく似たものは既に東方紅魔郷の時代から存在していて、例えば咲夜はいくつかの魔方陣を従えていることがあります。この魔方陣は倒せない敵であるかのように、咲夜からはほぼ独立した振舞いを見せます。

このように、敵本体に従属する敵(または魔方陣など)を作るにはどうすればよいのでしょうか? もし、その敵に当たり判定がないのであれば、オブジェクト弾のような感じで何とかできなくもないかもしれません。しかし、当たり判定があるような敵は、今までの知識だけでは作れそうもありません。

このように、ある敵に従属する敵を作るには、敵スクリプトというものを使います。独自の弾を作るときに弾スクリプトというのがあると話しましたが、これはその敵バージョンです。

敵スクリプトはどう作るのか...というのは、実はもうほとんど知っているはずです。実際今までもずっと敵を作ってきたわけで、敵スクリプトも今までやってきたことと大して違いはありません。違う部分は、script_enemy_main ではなく script_enemy を使うという点と、敵を出現させるのに CreateEnemyFromScript という関数を使うという点のみです。実に簡単ですね。

CreateEnemyFromScript(enemy, x, y, v, angle, arg);
    // enemy : 敵スクリプト名(文字列)
    // x, y  : デフォルトの位置
    // v     : デフォルトの速度
    // angle : デフォルトの移動方向
    // arg   : 何らかの値(自由に使えます)

CreateEnemyFromScript で作られた敵は、script_enemy_main の敵が倒されると、自動的に倒されます

敵スクリプトの使用例として、今回からは東方永夜抄の野符「武烈クライシス」を作ってみましょう。赤眼催眠と違って解析が難しかったので、細かい挙動を完全に再現することは諦めて、悟入幻想のようにそれっぽいものを作るに止めることにします。

使い魔の登場

武烈クライシスでは、使い魔がボスの左右の空間から登場し、回転しながら定位置へと広がりつつ移動します。今回はこの登場の部分だけ作ってみましょう。

先ず、使い魔の登場位置はボスと画面の端との丁度真ん中です。使い魔は左右両方に対称に出現するので、x 座標は中央からの距離で扱えばいいでしょう。

let xFamCenter = (GetCenterX - GetClipMinX) / 2;
let yFamCenter = yIni;

次に、使い魔をどう動かすかですが、回転しながら広がるというのはどうやって実装すればいいのでしょうか? これは、位置を x, y 座標で管理するのではなく、中心位置からの半径と角度で管理すればいいのです。毎フレーム半径を増やしつつ角度を変えていけば、回転しながら定位置へと自然に誘導できます。つまり、CreateEnemyFromScript には速度と移動方向を指定できますが、真面目にこれを利用するのではなく、とりあえず移動方向のところにデフォルトの位置の角度情報を渡すだけにしておいて、速度は 0 にしておきましょう。

あと、使い魔は右と左で回転方向が違います。この回転方向を ±1 で表現し、使い魔のスクリプトに渡してやりたいと思います。このような、各スクリプトに特有の情報を渡すには、CreateEnemyFromScript の最後の引数を使います。ここに渡した値は、GetArgument という関数で取得できます。

では、使い魔を配置するサブルーチンを作ってみましょう。

// 使い魔の配置に必要な時間
let wSetFam = 30;

// 使い魔をセット
sub setFam {
    // 使い魔の三角形の中心位置
    // x 座標は中心からの距離になっています。
    let xFamCenter = (GetCenterX - GetClipMinX) / 2;
    let yFamCenter = yIni;

    // 使い魔の位置(中心からの向き)
    let angleFam   = [0, 120, -120];

    ascent(i in 0..3) {
        CreateEnemyFromScript("Familiar", GetCenterX - xFamCenter, yFamCenter,
                              0, angleFam[i], 1);
    }

    ascent(i in 0..3) {
        CreateEnemyFromScript("Familiar", GetCenterX + xFamCenter, yFamCenter,
                              0, angleFam[i] + 180, -1);
    }

    wait(wSetFam);
}

Familiar というのが、敵スクリプトの名前(予定)です。分かりにくくなるので、左の使い魔用の処理と右の使い魔用の処理をループでまとめるのはやめておきました。

使い魔の位置は、最初はボスの方に頂点を向けた形にしてあります。半径を無視して角度だけを見ると、次の図 1 のようになります。

図 1. 使い魔登場時の角度変化
図 1. 使い魔登場時の角度変化

左側の使い魔は時計回りに 210 度回転し、右側の使い魔は反時計回りに 210 度回転します。つまり、左の使い魔のうち、最初に右(0 度)にいた使い魔は、最終的に左上(210 度)に来ます。

では、敵スクリプトを書いてみましょう。敵スクリプトの書き方は script_enemy_main の時とほとんど同じです。つまり、中に @Initialize, @MainLoop などを書いて、敵の挙動を指定します。少し違う点は、script_enemy の後に敵スクリプト名を書くという点です。実際に書いてみたのが、次のプログラムになります。

// 使い魔
script_enemy Familiar {
    let csd     = GetCurrentScriptDirectory;
    let imgFam  = csd ~ "img\familiar.png";

    // 使い魔の中心位置
    let xCenter = GetX;
    let yCenter = GetY;

    // 中心位置からの距離と角度
    // 実際の角度は angBase + angle になります。
    let r       = 0;
    let angBase = GetAngle;
    let angle   = 0;

    // 移動方向(±1)
    let dir     = GetArgument;

    // 使い魔の配置に必要な時間
    let wSetFam = 30;

    @Initialize {
        SetLife(2000);
        SetScore(10000);
        SetDamageRate(50, 50);

        SetTexture(imgFam);
        setGraphicFast;

        TMain;
    }

    @MainLoop {
        SetCollisionA(GetX, GetY, 32);

        yield;
    }

    @DrawLoop {
        DrawGraphic(GetX, GetY);
    }

    task TMain {
        yield;

        standBy;
    }

    // 初期位置へ移動
    sub standBy {
        let dr =  32 / wSetFam;    // 動径方向の速度
        let w  = 210 / wSetFam;    // 角速度

        loop(wSetFam) {
            r += dr;
            move(w);
            yield;
        }
    }

    // 次の位置へ移動
    //   w : 角速度
    function move(w) {
        angle += w * dir;

        SetX(xCenter + r * cos(angBase + angle));
        SetY(yCenter + r * sin(angBase + angle));
        SetGraphicAngle(0, 0, angle);    // グラフィックを傾けます
    }

    // グラフィックの設定
    sub setGraphicFast { SetGraphicRect( 0,  0, 48, 48); }

    // w フレームだけ待つ
    function wait(w) {
        loop(w) { yield; }
    }
}

それでは、順番に見ていきましょう。先ずは、必要となる変数です。

最初にあるのは、使い魔のグラフィックへのパスです。

let csd     = GetCurrentScriptDirectory;
let imgFam  = csd ~ "img\familiar.png";

familiar.png は次の図 2 のようなものです。

図 2. familiar.png
図 2. familiar.png

ここでは一番左上のグラフィックを使います。そのグラフィックを設定するのをサブルーチン化したものがこの部分です。前回話した通り、こういった画像の縦と横のピクセル数は 2 の累乗にします。ここでは縦が 128( = 27)ピクセルで、横が 256( = 28)ピクセルになっています。

// グラフィックの設定
sub setGraphicFast { SetGraphicRect( 0,  0, 48, 48); }

次にあるのは、使い魔の中心位置です。

// 使い魔の中心位置
let xCenter = GetX;
let yCenter = GetY;

次にあるのは、使い魔の中心位置です。これは使い魔が登場した時の初期位置と同じです。ただ、もし初期位置での半径が 0 でない場合であっても、CreateEnemyFromScriptx, y には中心位置を渡しておいたのでもいいでしょう。その場合、ちゃんとした初期位置を @Initialize で設定すればいいのです。

このように、敵スクリプトの初期設定は @Initialize でいくらでも変えられるので、CreateEnemyFromScript に渡した引数というのは必ずしも本当の初期設定である必要はありません。ただ、だからといって arg(最後の引数)の代わりのように使うのは良くありません。それはプログラムを読みにくくするだけです。多少意味の違う値を渡すにしても、ある程度の関連性のある値を渡すべきでしょう。全く関係ない値は、arg に渡すべきです。

で、次にあるのが使い魔の位置です。

// 中心位置からの距離と角度
// 実際の角度は angBase + angle になります。
let r       = 0;
let angBase = GetAngle;
let angle   = 0;

中心からの半径と、角度で扱っています。半径は最初 0 から始まって、最終的に 32 になります。角度は、ベースとなる角度 angBase と、そこからのズレ angle に分けています。これは、angle だけを別なところで使うつもりだからです。

次にあるのが移動方向です。

// 移動方向(±1)
let dir     = GetArgument;

移動方向というのは CreateEnemyFromScript のどの引数とも関連性がないので、最後の引数 arg を通して渡してやります。その最後の引数を取得するには、GetArgument を使います。

そして、最後にあるのが使い魔の配置に必要な時間です。これは使い魔をセットするサブルーチン setFam で使ったのと同じ値にします。

では、他の部分も見てみましょう。@MainLoop は、敵当たり判定がない点以外は今までと同じです。@DrawLoop は今までと全く同じですね。

ところが、@Initialize は少し違います。スペルカード宣言や時間設定がないのは当たり前としても、LoadGraphic もありませんLoadGraphic がないのに使い魔のグラフィックを描画できるのでしょうか?

実は、使い魔のグラフィックのロードは script_enemy_main で行うのです。なぜなら、script_enemy に書いてしまうと、使い魔を呼ぶたびにグラフィックをロードすることになってしまうからです。これは非常に効率が悪いですね。

同様に、DeleteGraphicscript_enemy_main に書きます。これはもっと重要です。script_enemy@Finalize に書いてしまうと、使い魔をどれか1体倒した時点でグラフィックが破棄されてしまうからです。

では、次に TMain を見てみましょう。現在は、使い魔が登場する時の挙動を担当するサブルーチン standBy を呼んでいるだけです。この standBy は、次のようになっています。

// 初期位置へ移動
sub standBy {
    let dr =  32 / wSetFam;    // 動径方向の速度
    let w  = 210 / wSetFam;    // 角速度

    loop(wSetFam) {
        r += dr;
        move(w);
        yield;
    }
}

使い魔は、30 フレームで登場するようにしています。半径の増加速度 dr と角度の変化速度 w は、最終的な変化量をフレーム数で割れば得られます。この値をフレーム数倍すれば最終的な変化量になるのだから、当然ですね。で、毎フレーム半径と角度を変化させ、使い魔の位置を変えていっています。角度変化と使い魔の位置の設定は、move という関数の中で行っています。

// 次の位置へ移動
//   w : 角速度
function move(w) {
    angle += w * dir;

    SetX(xCenter + r * cos(angBase + angle));
    SetY(yCenter + r * sin(angBase + angle));
    SetGraphicAngle(0, 0, angle);    // グラフィックを傾けます
}

ここで、角度を変化させるわけですが、回転方向 dir をかけるのを忘れないようにしましょう。そして、具体的な位置を計算して、その位置へと敵を移動させています。三角関数を使って円状の座標を求めるのはもう定型句で何度も出てきたので、そろそろ慣れてきましたか?

そして、使い魔のグラフィックを回転と同時にクルクルと回転させています。SetGraphicAngle という関数を使えば、グラフィックの角度を変化させることができます。SetGraphicAngle の引数は、それぞれ x 軸まわりの回転角、y 軸周りの回転角、z 軸周りの回転角です。普通は、x, y 軸まわりには回転せず、z 軸(画面に垂直な方向の軸)まわりにのみ回転させることになると思います。

それでは以上を使って、使い魔が登場するだけのプログラムを作ってみましょう。ここにそのプログラムを置いておきます。クルクルと回りながら登場することを確認して下さい。

要旨

次回も、引き続き武烈クライシスを作っていきましょう。

第22回 | 第24回 | 目次へ戻る

この講座はロベールが作成しました。
inserted by FC2 system