第12回 三十人三十一脚

今回もマイクロスレッドについて話していきたいと思います。

要旨

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

複数起動

では、今回は敵を動かしてみましょう。敵を動かすには、よく SetMovePosition02 が使われます。これは、指定した座標へ指定したフレーム数をかけて敵を移動させる関数です。

SetMovePosition02(x, y, frame);
    x     : x 座標
    y     : y 座標
    frame : 移動に要するフレーム数

東方的には、特定のフレーム数が経過するごとに自機の方向に移動させるのがいいでしょう。

// なるべくプレイヤーの方向に移動
//   xMove  : x 方向の移動量(正の数)
//   yAdd   : y 方向の移動量
//   frame  : 移動に要するフレーム数
//   left   : 以下、可動範囲
//   top    :
//   right  :
//   bottom :
function moveToPlayer(xMove, yAdd, frame, left, top, right, bottom) {
    let x;
    let y;

    if(GetPlayerX < GetX) {
        // プレイヤーより右に敵がいれば、敵は左に動きます。
        x = GetX - xMove;

        // 但し、敵が可動領域の左端よりも左にいくようなら、右に動きます。
        if(x < left) {
            x = GetX + xMove;
        }
    } else {
        // さもなくば、敵は右に動きます。
        x = GetX + xMove;

        // 但し、敵が可動領域の右端よりも右にいくようなら、左に動きます。
        if(right < x) {
            x = GetX - xMove;
        }
    }

    // 可動領域の外に行く場合は、端っこで止めます。
    y = GetY + yAdd;
    if(y < top) {
        y = top;
    } else if(bottom < y) {
        y = bottom;
    }

    SetMovePosition02(x, y, frame);
}

プログラムの中にある // から始まる行はコメントと言います。コメントはプログラムの動作には全く影響を与えないもので、主に説明書きをする時に使います。コメントをどれだけ入れても実行速度は変わりません(解析速度にはごくごく微弱な影響はあると思いますが)。// を使う以外にも、コメントにしたい部分を /**/ で囲む方法もあります。

この関数に関する説明は全部コメントに書いてあるので、詳細は省きます。1つ注意するところがあるとすれば、GetPlayerX はプレイヤーの x 座標を取得する関数だというくらいですか。動作が今ひとつ分からない人は、コメントを参考にプログラムを追ってみて下さい。

では、この関数を使って、120 フレーム毎に移動するようにしてみましょう。120 フレーム毎に移動となると、前回の 30 フレーム毎に弾を撃つ処理を参考にすれば、次のようなマイクロスレッドを作れば良い事が予想されます。

// キャラを移動させるタスク
task TMove {
    yield;

    loop {
        loop(120) { yield; }
        moveToPlayer(rand(40, 80), rand(-40, 40), 60,
                     GetClipMinX + 32, GetClipMinY + 32,
                     GetClipMaxX - 32, GetClipMinY + 160);
    }
}

ここで、rand というのは、乱数を得るための関数です。rand(x, y) とすれば、x 以上 y 以下の値がランダムで得られます。

問題は、弾を撃つマイクロスレッドと敵を動かすマイクロスレッドを同時起動するとどうなるか、です。どうなるのだろうと思った時には、とりあえずやってみるというのも1つの手です。では、やってみましょう。

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

script_enemy_main {
    let imgBoss = "script\img\ExRumia.png";

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

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

        TNway;
        TMove; 
    }

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

        yield;
    }

    @DrawLoop {
        DrawGraphic(GetX, GetY);
    }

    @Finalize {
        DeleteGraphic(imgBoss);
    }

    task TNway { ... }
    task TMove { ... }
    function nway(dir, way, span, color) { ... }
    function offsetX(radius, angle) { ... }
    function offsetY(radius, angle) { ... }
    function moveToPlayer(xMove, yAdd, frame, left, top, right, bottom) { ... }
}

長くなるので随分省略していますが、... の部分から詳細ページへと飛べるようにしています。コメントを増やしている以外は今までと全く同じなので、特に読む必要はないと思います。

では、実際に実行してみて下さい。...うまくいきましたね。このように、マイクロスレッドは一度に何個も起動できるのです。ただ、どういう風な処理の流れになっているのでしょうか? 次は、それを見ていきましょう。

処理の流れ

先ず、上の処理の流れを見る前に、関数(サブルーチン)の処理の流れから復習してみましょう。関数では、関数を呼び出した時に関数の中へと処理が移ります。そして、関数が終了すると関数を呼び出した地点へと戻ります(図 1)。

図 1. 関数やサブルーチンの処理の流れ
図 1. 関数やサブルーチンの処理の流れ

関数が終了すると、関数の中で作られた変数は破棄されます。これの意味する所は、例えば

function hoge {
    let x;
    // ここ
    x = 100;
}

のようなことをした時を考えます。一度関数 hoge を呼んで、もう一度呼んだ際に、「// ここ」の地点での x の値が 100 になるわけではない、ということです。言うなれば、最初に呼んだときの x と二度目に呼んだときの x は別の x であり、最初に呼んだ時にいくら x を操作しても、二度目の x には何ら影響はないのです。

では、マイクロスレッドの処理の流れはどうなっているのでしょうか? 先ずは、1つのマイクロスレッドのみを起動した場合について復習しましょう。

マイクロスレッドを起動すると、先ずは関数と同様に、マイクロスレッドに処理が移動します。しかし、yield した地点で一旦処理を中断し、起動した地点に戻る事ができます。そして、戻った先でまた yield を行えば中断していた地点へ復帰できます。また yield をすれば中断でき、復帰のための yield を呼んだ地点に戻る事ができます(図 2)。

図 2. マイクロスレッドの処理の流れ
図 2. マイクロスレッドの処理の流れ

ここで、メインスレッドというのは、マイクロスレッドの外部の事です。但し、マイクロスレッドから呼ばれた関数の内部はマイクロスレッドの内部とみなされます。同じ関数を例えば @MainLoop から呼んだ場合、その内部はマイクロスレッドの外部とみなされます。

マイクロスレッドの中にある変数は、中断しても破棄されません。中断時と、復帰時とで、変数の中身は変わりません。マイクロスレッドの直下にある変数が破棄されるのは、マイクロスレッドが完全に終了した場合です。

では、マイクロスレッドが複数あった場合はどうなるのでしょうか? マイクロスレッドが複数ある場合でも、マイクロスレッド内で yield すると処理が中断される事は変わりません。では、復帰する時はどうなのでしょうか? マイクロスレッドの数だけ何度も yield しなければならないのでしょうか? いえ、そんなことはありません。実は、次の図のようになっています。

図 3. マイクロスレッドが複数ある場合の処理の流れ
図 3. マイクロスレッドが複数ある場合の処理の流れ

メインスレッドで一度 yield を呼べば、全てのマイクロスレッドが順番に復帰されるのです。

図はマイクロスレッドが3つ起動している場合の処理の流れを書いています。メインスレッドで yield すると、マイクロスレッド1が先ず復帰されます。そして、マイクロスレッド1が中断するか終了するかすれば、次はマイクロスレッド2が復帰されます。そして、マイクロスレッド2が中断するか終了するかすれば、最後にマイクロスレッド3が復帰され、マイクロスレッド3が中断するか終了するかすれば、ようやくメインスレッドに処理が戻ります。

マイクロスレッドの復帰順は、メインスレッドでマイクロスレッドを起動した順番と同じです。先に起動した方が、先に復帰されます。ただ、マイクロスレッド内でマイクロスレッドを起動するということもでき、この場合は少し話が変わってきます。これに関しては次回話そうと思います。

とはいえ、実行順が特に影響しない処理同士では、順番に実行されているということを意識する必要はありません。そういう場合は、並列的に実行されていると解釈してもらっても実用上大きな問題はないと思います。

要旨

次回は、マイクロスレッドの中からマイクロスレッドを起動する場合について話していきましょう。

第11回 | 第13回 | 目次へ戻る

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