6.関数の使い方(応用編)


6−1.ローカル変数とグローバル変数

関数はSphereScriptの核となる概念でちょっと複雑なので、もうすこし詳しく説明します。 以下のプログラムを実行すると、xが関数の中で2倍になるため、「10」と表示されるように思えます。

===============
function my_function(int a)
{
    a*=2;           //引数aを2倍にする
    return 0;       //特に戻り値を必要としないのでゼロを返す
}

int x=5;
my_function(x);     //引数を2倍にする関数を呼ぶ
print_string(x);    //xを表示する。
===============

ところが実際は「5」と表示されます。

これは関数の引数として指定された変数は、あくまで中身だけがコピーされて関数の中で処理されるという仕組みだからです。 一見するとまどろっこしい仕組みですが、関数の中で勝手に変数が書き換えられないということを保障できるため、バグの出にくいプログラムを書くことが出来るようになります。

ところで、上記の関数my_functionでは引数aを2倍にして代入していました。aの値を使う、こういうプログラムを書きたくなります。

===============
function my_function(int a)
{
    a*=2;           //引数aを2倍にする
    return 0;       //特に戻り値を必要としないのでゼロを返す
}
int x=5;
my_function(x);     //引数を2倍にする関数を呼ぶ
print_string(a);    //関数my_functionで2倍になった変数aを表示する。
===============

このプログラムは正しく動きません。これは「関数の中で作成された変数は、同じ関数の中でしか使えない」というルールがあるためです。 このルールにより、関数内部で宣言された変数(関数の引数として宣言されたものも含みます)は関数の外で読み書きすることは出来ません。 この変数を『ローカル変数』と呼びます(古いC言語の本だと『局地変数』なんて呼び方をしているものもあります)。 変数の型宣言を行っていなくても、関数の中で最初に使った変数は宣言したことと同じ扱いになるため、やはり以下のプログラムは正しく動きません。

===============
function my_function(int a)
{
    b=a*2;          //引数aを2倍にしてbへ代入(bはローカル変数になる)
    return 0;       //特に戻り値を必要としないのでゼロを返す
}
my_function(5);     //引数を2倍にする関数を呼ぶ
print_string(b);    //関数my_functionで2倍になった変数bを表示する。(←ここでランタイムエラーが発生)
===============

関数の中で宣言された値を外に取り出すにはreturnで返り値にするしかありません。以下のプログラムならば正しく動きます。

===============
function my_function(int a)
{
    b=a*2;          //引数aを2倍にしてbへ代入
    return b;       //bを返す
}
c=my_function(5);   //引数を2倍にする関数を呼ぶ
print_string(c);    //関数my_functionで2倍になった変数cを表示する。
===============

これを踏まえて、上で「関数の中で宣言された変数は、同じ関数の中でしか使えない」と書きましたが、一方で「関数の外で宣言された変数は、関数の中でも外でも自由に使える」というルールがあります。 この変数を『グローバル変数』と呼びます(古い言い方だと『大局変数』ですが、これを知っている人は相当年季が入ったエンジニアですね…)。 したがって、以下のプログラムは正しく動作し、「10」と表示されます。
(変数を宣言するときにexternという単語がついていることに注意してください。 詳細は「7.変数のスコープと寿命」で解説しますが、SphereEngineの仕様としてアンカーポイント内のスクリプトでグローバル変数を定義したい場合はexternを付けてください。 別のアンカーポイントからも読み書きできるようになります。)

===============
extern int b=0;     //グローバル変数bを宣言しておく
function my_function(int a)
{
    b=a*2;          //引数aを2倍にしてbへ代入(bはグローバル変数なのでどこでも使用可能)
}
my_function(5);     //引数を2倍にする関数を呼ぶ
print_string(b);    //関数my_functionで2倍になった変数bを表示する。
===============

例として示しましたが、変数をどこでも使いたいからといって、何でもかんでもグローバル変数にするのは良い方法ではありません。 グローバル変数はどこでもアクセスできるという便利な性質を持つため、言い換えれば意図しないところで勝手に変数の値が変わってしまうというようなバグを作ってしまいがちです。 これは大規模プログラムになると致命的問題となります。多人数で開発を行うと「その変数は一時計算用なんだから、そんなところで距離の判定に使わないでくれ!」というような問題が頻繁に起こってしまいます。


6−2.値渡しと参照渡し

関数の引数として変数が指定されると、その中身だけがコピーされると説明しました。この概念を『値渡し』と呼ぶことがあります。通常の関数を書くと、全ての引数は値渡しとなり、元の変数は一切無視されます。

===============
distance = vector_length(x1-x2, y1-y2, z1-z2);    //ここで呼んでいる関数vector_lengthは下で記述している。

function float vector_length(float x, float y, float z)
{
    float length=sqrt(x*x + y*y + z*z);    //ベクトル長を求める(sqrtはルートを計算する関数)
    return length;
}
===============

関数vector_lengthが呼ばれたとき、以下のような処理が行われると考えてください。

===============
1.関数vector_lengthで使えるローカル変数xを用意する。
2.x1-x2の計算結果をxに代入;
3.関数vector_lengthで使えるローカル変数yを用意する。
4.y1-y2の計算結果をyに代入;
5.関数vector_lengthで使えるローカル変数zを用意する。
6.z1-z2の計算結果をzに代入;
7.関数vector_lengthへジャンプする。
===============

この値渡しと言う概念は、関数の独立性を高めて、不具合の出にくいプログラムを書くのに役立つだけでなく。再帰を実現するために必要不可欠となっている仕組みですが、都合が悪いときもあります。 それは引数を変えて欲しいときです。具体的には「x,y,zの3次元座標を返したい」とか「ファイル読み込み時に、データ内容だけでなく、ファイルの状態も知りたい」とかいうような、複数の値を戻り値にしたいときです。 通常の関数では値を一つしかreturn出来ないため、このようなことは出来ませんが、以下のようにすると、関数の中で変更した値が関数呼び出し元の変数にも代入されます。

===============
function my_function(refer x)   //引数を2倍にする関数
{
    x*=2;
}

a=5;
my_function(a);    //aを2倍にする
print_string(a);
===============

この結果は「10」と表示されます。関数の引数の「refer」というキーワードがポイントです。この「refer」はintやstringなどと同じ型の一種で『参照型』と呼ばれます。 したがってここでの変数xは『参照型の変数』となります。 通常の関数では関数の中でどのような代入を行っても引数の値は変わらないため「5」と表示されますが、参照型の変数を使うことで、関数の中で行った代入が関数の引数に指定された変数にも適用されます。

この説明だけでは分かりにくいと思うので、もう一つ例を挙げます。

===============
function vector_normalize(refer x, refer y, refer z)    //3次元ベクトルを正規化(長さを1に)する関数
{
    float length=sqrt(x*x+y*y+z*z); //  ベクトルの長さを求める
    x=x/length;                     //  成分ごとに長さで割って正規化する
    y=y/length;
    z=z/length;
}

vec_x=2;
vec_y=3;
vec_z=5;
my_function(vec_x,vec_y,vec_z);     //  ベクトル[2,3,5]を正規化する
===============

参照型は便利である反面、グローバル変数同様に乱用するとプログラムが分かりにくくなる危険性があります。前述の配列やオブジェクトを使えば、参照渡しを使わずとも実現できる場面は多いため、上手に使ってください。 実際、上記のベクトル正規化の例は、ベクトルオブジェクト({x:0, y:0, z:0}というようなやつ)を使用すれば参照を使わずとも実現可能です。 このあたりはノウハウの積み重ねによるところが大きいため、試行錯誤しながら自分のスタイルを身に付けてください。


6−3.関数の再帰呼び出し

関数の中で自分自身を呼び出すことも可能です。階乗を求める関数です。

===============
function kaijo(int a)       //aの階乗を求める関数
{
    if (a==1) return 1;
    return a*kaijo(a-1);
}
===============

数学的には漸化式「f(x)=f(x-1)*x ※ただしf(1)は1である。」として定義されるものです。 このような関数の使い方を『再帰呼び出し』または単に『再帰』と呼びます。再帰を使ったプログラムを『再帰プログラミング』と呼ぶことがあります。 この再帰呼び出しですが、以下のようにすることで、for文でも全く同じ結果が得られます。

===============
result=1;
for (i=1;i<=a;i++) result*=i;
===============

これだけを見ると、上の再帰は全く無意味な書き方のように思えますが、ある程度を超えて高度な仕組みを作ろうとすると、for文では間に合わなくなってしまい、再帰が必要不可欠になります。例えば最大公約数を求める関数です。

===============
function int kouyaku(int a, int b)  //最大公約数を求める関数
{
    if (a<b) {
        return kouyaku(b, a);       //aよりbが小さい場合は入れ替える
    }
    if (b==0) return a;             //ゼロとの公約数はないので、aを最大公約数とする。
    return kouyaku(a%b, b);         //剰余とbの最大公約数がaとbの最大公約数である。
}
===============

漸化式だと「f(x,y)=f(max(x%y, y), min(x%y, y)) ※ただしf(x, 0)はxである。」として定義されます。これをループで書くことは可能ですが、非常に面倒くさいアルゴリズムを組み立てなければいけません。 再帰呼び出しは高度な(というかちょっと特殊な)スキルを要求する技法ですので、始めのうちは無理に使いこなそうと考えないほうが無難です。実際、普通にゲームのイベント処理などを作る限りでは、再帰が必要になることはまずありません。 複雑なエフェクト処理やゲームのシステムそのものを作ろうという場合に、改めて挑戦してみると良いでしょう。


最初のページへ戻る