第9回 自動処理装置

今回は、特定の処理をまとめて、後で何度でも使い回せるようにしたいと思います。

要旨

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

変数のスコープ

今度は2種類の n-way 弾を 30 フレーム毎に交互に発射するようにしてみましょう。最初の n-way 弾は黄色い 10 度間隔の 3-way 弾で、次の n-way 弾は白い 5 度間隔の 5-way 弾にしたいと思います。

交互に発射するには、30 フレーム目に最初の弾を、そして 60 フレーム目に次の弾を出します。

#東方弾幕風
#Title[テストスクリプト]
#Text[テストスクリプト]
#ScriptVersion[2]

script_enemy_main {
    let imgBoss   = "script\img\ExRumia.png";
    let frame     =  0;
    let angleBase = 90;

    @Initialize {
        SetX(GetCenterX);
        SetY(GetClipMinY + 120);
        SetLife(2000);

        LoadGraphic(imgBoss);
        SetTexture(imgBoss);
        SetGraphicRect(0, 0, 63, 63);
    }

    @MainLoop {
        SetCollisionA(GetX, GetY, 24);
        SetCollisionB(GetX, GetY, 24);

        frame++;
        if(frame == 30) {
            let angle = angleBase - 10;
            loop(3) {
                CreateShot01(GetX, GetY, 1, angle, YELLOW01, 0);
                angle += 10;
            }
            angleBase += 8;
        } else if(frame == 60) {
            let angle = angleBase - 10;
            loop(5) {
                CreateShot01(GetX, GetY, 1, angle, WHITE01, 0);
                angle += 5;
            }
            angleBase += 8;
            frame = 0;
        }
    }

    @DrawLoop {
        DrawGraphic(GetX, GetY);
    }

    @Finalize {
        DeleteGraphic(imgBoss);
    }
}

n-way 弾を発射する部分は前回と同じなので特に大きな問題はないと思いますが、1点だけ注意が必要です。それは、この部分です。

let angle = angleBase - 10;
let angle = angleBase - 10;

angle が2カ所で宣言されています。なぜ同じ名前の変数をわざわざ2カ所で宣言する必要があるのでしょうか? 試しに下の方の let を削除して実行してみると...何と angle が定義されていないというエラーが出ると思います。

なぜこのようなエラーが発生するかというと、変数の有効範囲はその変数を宣言した文の後から、その文を含む { } の終わりまでだからです。つまり、最初の angle の有効範囲は if(frame == 30) の後の { } の中だけであって、if(frame == 60) の後の { } の中では使えないのです。このような変数の有効範囲のことをスコープといい、有効期間の事を寿命といいます。

これを回避するには if の前で変数を宣言すればいいだけですが、これはお薦めできません。なぜなら、変数のスコープや寿命はできるだけ短い方が、バグが入るチャンスが少なくなるからです。スコープや寿命が長いと変数に変化を与えうる範囲が広くなり、変数の変化を把握する努力が増えてしまうのです。妙な処理を使って無理矢理スコープや寿命を短くする必要はありませんが、どちらでも処理が変わらないのであれば変数を { } の中に入れてしまった方が安全です。

また、同じスコープ、寿命を持った2つ以上の同じ名前の変数を作ってはいけません。既にある変数を使えばいいだけなので、2つ目をわざわざ作る必要はないかと思いますし、変数のスコープがなるべく小さくなるよう心がけていれば、そもそもそういう状況自体まれにしか起こらないはずです。

関数

上のプログラムを見てみると、n-way 弾を発射する部分が2カ所あります。この2つの処理は非常に良く似ています。前回は良く似た処理はループでまとめる事ができましたが、今回はループでは解決できそうにありません。その代わり、変数をうまく使う事で一応解決することができます。

let way;
let span;
let color;

if(frame == 30) {
    way   =  3;
    span  = 10;
    color = YELLOW01;
    angleBase += 8;
} else if(frame == 60) {
    way   =  5;
    span  =  5;
    color = WHITE01;
    angleBase += 8;
    frame = 0;
} else {
    return;
}

let angle = angleBase - (way - 1) / 2 * span;
loop(way) {
    CreateShot01(GetX, GetY, 1, angle, color, 0);
    angle += span;
}

ここで、return というのは @MainLoop強制的に終了させる命令です。つまり、30, 60 フレーム目でなければ if 文の後に進めないわけです。30, 60 フレーム目の場合のみ、その後の弾を発生するルーチンが実行されます。

また、* は掛け算を、/ は割り算を表します。

このようにすれば一応何とかなるわけですが、「変数のスコープはできるだけ狭い方がいい」ということを考えるとあまり良くない形になっていますし、わざわざ return が必要になるのも気持ち悪いです。それに、さらにもう1つ n-way 弾を使いたいなどということになると、より処理が複雑になることが予想されます。なので、この解決法は今ひとつな感じがします。

ここで、今まで何気なく使ってきた CreateShot01 に注目してみましょう。これは、発射地点の x, y 座標、速度、発射角、弾の種類、そして遅延時間を指定すれば弾を発射してくれるモノだったわけです。これと同じ様なモノを作る事ができれば、もっとスマートな形で解決できそうです。

このようなモノ関数(function)と呼ばれています。関数は次のように作る事ができます。

function nway(let dir, let way, let span, let color) {
    let angle = dir - (way - 1) / 2 * span;
    loop(way) {
        CreateShot01(GetX, GetY, 1, angle, color, 0);
        angle += span;
    }
}

このような関数を定義しておけば、if 以降の部分は

if(frame == 30) {
    nway(angleBase, 3, 10, YELLOW01);
    angleBase += 8;
} else if(frame == 60) {
    nway(angleBase, 5,  5, WHITE01);
    angleBase += 8;
    frame = 0;
}

という風に簡単に書く事ができます。

では、何をやってるかを詳しく見ていきましょう。先ず、nway という関数を使っている部分を見てみましょう。

nway(angleBase, 3, 10, YELLOW01);

この部分を見れば、CreateShot01 と同じような感じになっていると思います。このようにすれば、関数 nway として定義された処理が実行されます。このように、関数を実行する事を関数を呼ぶと、関数に渡される値のことを実引数(じつひきすう)と言います。

では次に、nway を定義している部分を見てみましょう。

function nway(let dir, let way, let span, let color) { ... }

関数を呼ぶ時には ( ) の中に実引数を書きましたが、関数を定義する時には何か変数宣言のようなものがあります。これを仮引数(かりひきすう)と言います。関数が呼ばれた時には、仮引数は関数に渡された実引数で初期化されます。そうやって初期化された仮引数は、その関数の中で使用されます。関数に渡した実引数が違えば、処理も変わる事になります。また、仮引数のスコープと寿命は関数が終了するまでです。

まだ混乱している方もいるかもしれません。ループの場合と同じく、関数を使わなかった場合の処理に置き換えてみましょう。

if(frame == 30) {
    let dir   = angleBase;
    let way   =  3;
    let span  = 10;
    let color = YELLOW01;

    let angle = dir - (way - 1) / 2 * span;
    loop(way) {
        CreateShot01(GetX, GetY, 1, angle, color, 0);
        angle += span;
    }

    angleBase += 8;
} else if(frame == 60) {
    let dir   = angleBase;
    let way   =  5;
    let span  =  5;
    let color = WHITE01;

    let angle = dir - (way - 1) / 2 * span;
    loop(way) {
        CreateShot01(GetX, GetY, 1, angle, color, 0);
        angle += span;
    }

    angleBase += 8;
    frame = 0;
}

先ず、実引数に相当する値で仮引数に相当する変数が初期化されています。

let dir   = angleBase;
let way   =  3;
let span  = 10;
let color = YELLOW01;

次に、関数の中身に相当する処理が実行されます。

let angle = dir - (way - 1) / 2 * span;
loop(way) {
    CreateShot01(GetX, GetY, 1, angle, color, 0);
    angle += span;
}

変数のスコープと寿命に若干の違いはありますが、関数を呼んだ場合にはこのような処理が行われるわけです。

最後に、関数を使った場合のプログラム全体を書いておきましょう。

#東方弾幕風
#Title[テストスクリプト]
#Text[テストスクリプト]
#ScriptVersion[2]

script_enemy_main {
    let imgBoss   = "script\img\ExRumia.png";
    let frame     =  0;
    let angleBase = 90;

    @Initialize {
        SetX(GetCenterX);
        SetY(GetClipMinY + 120);
        SetLife(2000);

        LoadGraphic(imgBoss);
        SetTexture(imgBoss);
        SetGraphicRect(0, 0, 63, 63);
    }

    @MainLoop {
        SetCollisionA(GetX, GetY, 24);
        SetCollisionB(GetX, GetY, 24);

        frame++;
        if(frame == 30) {
            nway(angleBase, 3, 10, YELLOW01);
            angleBase += 8;
        } else if(frame == 60) {
            nway(angleBase, 5,  5, WHITE01);
            angleBase += 8;
            frame = 0;
        }
    }

    @DrawLoop {
        DrawGraphic(GetX, GetY);
    }

    @Finalize {
        DeleteGraphic(imgBoss);
    }

    function nway(let dir, let way, let span, let color) {
        let angle = dir - (way - 1) / 2 * span;
        loop(way) {
            CreateShot01(GetX, GetY, 1, angle, color, 0);
            angle += span;
        }
    }
}

関数は @MainLoop 等の内部で定義することもできますが、ここでは外で定義しています。関数は使い回してなんぼなところがあるので、汎用性のある形にして、script_enemy_main に直接定義する事が多いです。

関数の文法

関数定義の文法を詳しく書くと次のようになります。

function <関数名>[([<仮引数リスト>])] {
    <処理>
}

そして、関数呼出の文法を詳しく書くと次のようになります。

<関数名>[([<実引数リスト>])];

仮引数の let は省略可能です。普通は省略します。

引数は必ずしも必要ではなく、もし引数が必要なければ仮引数リストを書く必要はありません。つまり、() だけ書いておけばいいですし、() を省略する事すら可能です。その場合、関数を呼ぶ時も実引数リストを書く必要は無く、() だけ書いておけばいいですし、() を省略する事も可能です。

関数から強制的に脱出するには、return を使います。

return <戻り値>;

戻り値に関しては、次回話したいと思います。

要旨

次回も関数について考えていきましょう。

第8回 | 第10回 | 目次へ戻る

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