第10回 自動計算装置

今回も引き続き関数に関して考えていきたいと思います。

要旨

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

戻り値

さて、前回関数を使ってみました。関数は特定の処理をまとめて、使い回せるようにしたものでした。

しかし、数学でいう所の「関数」の印象とは少し違うと思います。数学における「関数」は、特定の計算式をまとめて、その計算結果を返すものだからです。計算式が一般的な処理に変わったという点は理解できると思いますが、計算結果を返すという部分が無くなっている部分は少し気持ち悪いかもしれません。

例えば、次の様な数式を考えてみましょう。

f(x) = x3 + 2x2 + 3x - 1
g = f(2)

上で関数 f を定義しています。下では、その関数 f に 2 を渡した場合の値を g に代入しています。g に代入される値は、要するに f(2) = 23 + 2 × 22 + 3 × 2 - 1 = 21 になります。このように、引数に対してある値を返すのが数学における関数なわけです。

プログラムにおける関数では、果たしてこのように値を返す事はできないのでしょうか? 実はそのようなことはありません。実際に今までにもそのような関数を使っているのです! それは、GetX, GetY, GetCenterX, GetClipMinY です。それぞれ、敵の現在の x 座標、y 座標、画面中央の x 座標、画面上部の y 座標を返す関数です。これらは引数を持っていませんが、確かに値を返す関数なのです。

このような値を返す関数を作るのは非常に簡単です。単に return を返す値付きで実行すればいいだけです。

return <戻り値>;

このように関数から返される値の事を戻り値(返り値、返値)と言います。

では、何か実際に戻り値付きの関数を作ってみましょう。

今までは敵の位置から弾を出していましたが、敵の周りから弾を出すようにしてみましょう。そのために、半径と角度から x 座標や y 座標を計算する関数を作ってみましょう。

#東方弾幕風
#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, 5, YELLOW01);
            angleBase += 8;
        } else if(frame == 60) {
            nway(angleBase, 5, 3, WHITE01);
            angleBase += 8;
            frame = 0;
        }
    }

    @DrawLoop {
        DrawGraphic(GetX, GetY);
    }

    @Finalize {
        DeleteGraphic(imgBoss);
    }

    function nway(dir, way, span, color) {
        let radius = 32;
        let angle  = dir - (way - 1) / 2 * span;

        loop(way) {
            let x = GetX + offsetX(radius, angle);
            let y = GetY + offsetY(radius, angle);

            CreateShot01(x, y, 1, angle, color, 0);
            angle += span;
        }
    }

    function offsetX(radius, angle) {
        return radius * cos(angle);
    }

    function offsetY(radius, angle) {
        return radius * sin(angle);
    }
}

ここで作った「数学的な関数」は offsetXoffsetY です。半径 radius と角度 angle から、それぞれ円周上の x 座標と y 座標を返す関数です。

では、具体的にどういう処理が行われているかを見るために、関数を使わなかった場合の処理に置き換えてみましょう。

function nway(dir, way, span, color) {
    let radius = 32;
    let angle  = dir - (way - 1) / 2 * span;

    loop(way) {
        let radiusX = radius;
        let angleX  = angle;
        let x       = GetX + radiusX * cos(angleX);

        let radiusY = radius;
        let angleY  = angle;
        let y       = GetY + radiusY * sin(angleY);

        CreateShot01(x, y, 1, angle, color, 0);
        angle += span;
    }
}

見れば分かると思いますが、余計な処理が無いので、ほとんど関数の内容がそのまま埋め込まれているだけになっています。ただ、仮引数の初期化が行われている点に注意して下さい。他にも色々処理があれば、let x 等の前にその処理が行われることになります。

三角関数

さて、ここでいきなり sincos といったものが出てきました。これらは三角関数というものです。高校で習うはずですが、大人になって忘れ去られる数学の代表例としてよく挙げられます。

しかし、それは三角関数が「実用品」であると意識したことがないからだと断言します。三角関数はゲームプログラムにおいては必要不可欠な非常に実用性の高い数学です。「えー、三角関数覚えんとあかんのん?」と思われるかもしれませんが、普通はごくごく基本的な事しか使いませんし、テストがあるわけでもないので公式が必要であったとしても調べれば良いだけなので安心して下さい。これを機に三角関数の基本を身につけてしまいましょう。

さて、三角関数は「三角関数」という名前がついていますが、一番最初の定義から拡張された結果、実際には「円関数」と呼んだ方がいい代物になってしまっています。そこで、今回のみに限って、以降「円関数」と呼ぶことにします。

円関数は3種類存在します。それぞれ、単位円(半径 1 の円)を使って、図 1 のように定義されます。

図 1. 円関数
図 1. 円関数

先ずは cos(コサイン)と sin(サイン)です。これらは、それぞれ単位円の円周上の x, y 座標を表します。cos と sin は x 軸からの角度を引数にとって、円周上の x, y 座標を返します。

そして、もう1つは tan(タンジェント)です。これは、sin/cos に等しいです。要するに、円の中心と (cos, sin) を結ぶ直線の傾きになります。言い換えれば、x 軸からある角度傾いた直線の傾きを求める関数になります。直線の傾きというのは、x 方向に進む距離と y 方向に進む距離の比率です。例えば傾きが a の場合、x 方向に c だけ進むと、y 方向には ac だけ進むことになります。逆に考えると、x 方向に c 進んだ時に y 方向に s 進む場合の傾きは s/c になります。tan = sin/cos は、直線と x 軸のなす角度から傾きを求める関数なのです。

円関数は、具体的には上記の offsetX, offsetY のような形で使われます。つまり、半径と角度から、cos と sin を使って円周上の座標を取得する、という形で使われる場合が殆どです。従って、実用的にはこのように使えるということだけ覚えてもらえれば、当面はそれで十分です。

また、円関数の逆関数である逆円関数も存在します。逆関数というのは、関数 y = f(x) に対して、x = F(y) となる関数 F(y) のことです。例えば、cos の逆関数は acos(アークコサイン)と呼ばれますが、acos は cos の値から逆に角度を求める関数になります。asin(アークサイン)は sin の値から逆に角度を求める関数で、atan(アークタンジェント)もやはり tan の値から角度を求める関数になります。

逆円関数の中で、実際によく使うのは atan です。atan は tan の値から角度を求める関数ですが、tan は線の傾きを表す関数でした。つまり、atan は線の傾きから角度を求める関数なわけです。

ただ、線が真上を向いている状態では傾きが無限大になってしまいます。その問題を回避するため、東方弾幕風(や、その他の多くの言語)には atan2 という関数が用意されています。atan2 では線の傾きを y/x の計算済みの値ではなく、y と x をバラで指定します。つまり、atan2(y, x) という風にすれば、傾き y/x の直線と x 軸とのなす角度が得られるわけです。

図 2. atan2
図 2. atan2

具体的には、atan2 はある点 (x1, y1) から別の点 (x2, y2) を見た時の角度を取得する場合によく使われます。先ず、(x1, y1) を原点と考えると、もう1つの点の座標は (x2 - x1, y2 - y1) となります。原点と (x2 - x1, y2 - y1) を結んだ線分の傾きは (y2 - y1)/(x2 - x1) なので、点 (x1, y1) から点 (x2, y2) を見た時の角度は atan2(y2 - y1, x2 - x1) になります。実用上は、この式をそのまま使ったのでも、当面は問題ないと思います。

サブルーチン

引数も戻り値も必要がない処理を作る場合もあると思います。確かに関数にしても構わないのですが、サブルーチンと呼ばれるものにした方が微妙に速いです。サブルーチンの定義は

sub <サブルーチン名> {
    <処理>
}

呼び出しは

<サブルーチン名>;

となります。ほとんど関数と変わりませんが、function の代わりに sub を使います。

要旨

次回は、東方弾幕風の中でも非常に重要な「マイクロスレッド」というものについて話しましょう。

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

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