フロー制御(lsl基礎知識 第四回)

Miz

2007年05月31日 12:15


今回は「フロー制御」についてのお話です。
フロー制御というのは、条件によって実行するコードを変えたり、繰り返し処理を行ったりするものです。
また、lslにおいてはステート(状態)を変更することもフロー制御の一つに挙げられます。
フロー制御の種類

まず最初にフロー制御の種類を挙げておきます。

フロー制御説明if-else条件分岐。条件に応じて実行するコードを変える。whleループ処理。条件を満たす間延々と繰り返し処理を行う。do-whileループ処理。処理を行った後、条件を満たす場合は繰り返す。forループ処理。条件を変化させながら繰り返し処理を行う。return復帰(リターン)。ユーザー関数やイベントを終了する。jumpジャンプ。指定したコードの行に処理を飛ばす。stateステート。スクリプトのステートを変更する。

一番よく使うのはif文だと思いますが、「プログラムしてるぞ」感が強いのはforでしょうw
ちなみに、プログラミングに精通するほどif文の使用頻度は下がり、forを工夫して使うようになります。
どうしてもifが一番使いやすいので、最初の頃のコードはif文ばかりになったりします。
ifが多いか少ないかで、プログラミングのスキルはなんとなく判断がついたりします(^^;

if-else

というわけで基本のif文です。
構文から見てみましょう。

【基本形】
if (論理演算式) {
  論理演算式がTRUEであるとき、ここに書いたコードが実行される
}

前回やった「論理演算」がここで出てきます。
例えば、「name == "miz"」とか「money < 500」のような式ですね。
これがif文の条件になります。
条件が成り立つとき(=論理演算式の結果がTRUEのとき)に、if文の中身が実行されます。

if (ピアノが弾ける) {
  想いの全てを歌にする
}

上記の例では、もしも「ピアノが弾ける」なら、「想いの全てを歌にする」ことになります。
「ピアノが弾けない」場合は何も起こりません。
もちろん、lslでこんなコードがあるわけではありません、念のため(^^;

ところで・・・。
西田さんは本当はピアノを持ってないし、上手に弾けません。
「ピアノが弾けない」場合にも何か処理を書きたい場合はどうするのでしょうか。

【応用形1】
if (論理演算式) {
  論理演算式がTRUEであるとき、ここに書いたコードが実行される
} else {
  論理演算式がFALSEであるとき、ここに書いたコードが実行される
}

この形を使います。
elseは「そうじゃないとき」のような意味合いになります。

if (ピアノが弾ける) {
  想いの全てを歌にする
} else {
  伝える言葉が残される
}

このようにすると、もしも「ピアノが弾け」たなら「想いの全てを歌に」しますが、ピアノが無かったり、聞かせるほどの腕もない場合は「伝える言葉が残される」ことになります。
よくわからない人はタウンページのCMでも見てくださいw

さて、ピアノの弾き方にもいろいろあるわけです。
時と状況に応じていろいろに弾き分けたい場合があります。
条件がたくさんあるようなときですね。

そんなときは以下のようにします。

【応用形2】
if (論理演算式1) {
  論理演算式1がTRUEであるとき、ここに書いたコードが実行される
} else if (論理演算式2) {
  論理演算式2がTRUEであるとき、ここに書いたコードが実行される
} else {
  論理演算式1も2もFALSEであるとき、ここに書いたコードが実行される
}

こんなふうに、「else if」を使って条件式を次々に並べることができます。

if (雨が降る夜なら) {
  雨のよに弾く
} else if (風吹く夜なら) {
  風のように弾く
} else if (晴れた朝なら) {
  晴れやかに弾く
} else {
  きみと夢見ることもない
}

このように書くと、
「雨が降る夜」は「雨のよに」
「風吹く夜」には「風のように」
「晴れた朝」には「晴れやかに」
弾きますが、雨でも風でも晴れでもない場合は、
「きみと夢見ることも」なくなります。

真剣に何を喩えてるんだかサッパリわからないという方は、こちらでもご覧下さい。

アホな喩えばかりではアレですので、最後にlslの具体例を示しておきます。

if (color == "red") {
  llSetColor(<1.0, 0.0, 0.0>, ALL_SIDES);
} else if (color == "green") {
  llSetColor(<0.0, 1.0, 0.0>, ALL_SIDES);
} else if (color == "blue") {
  llSetColor(<0.0, 0.0, 1.0>, ALL_SIDES);
} else {
  llSetColor(<1.0, 1.0, 1.0>, ALL_SIDES);
}

変数colorの中身に応じて、primの色を変えるif文です。
llSetColorについては初級スクリプトの記事を参考にして下さい。

while/do-while

続いてループ処理を行うwhile文です。
do-whileというのもありますが、この二つは条件判定の位置が異なるだけです。

まずはwhile文から構文を見てみましょう。

【whileの構文】
while (論理演算式) {
  論理演算式がTRUEの間は延々とここに書いたコードを繰り返す
}

これは以下のような場合に使います。

while (俺の目が黒い) {
  ここは通さない
}

かの有名な「俺の目が黒いうちはここを通さない」がこれです。
whileは条件判定部分がTRUEのうちは、決してループを抜け出すことがありませんので、まさに「通れません」。
通るためにはヤツの目を黒以外の色にする必要があります。

もしも以下のようなwhile文を書いたらどうなってしまうでしょうか。

while (TRUE) {
  llSay(0, "俺を倒してから行け!");
}

このwhile文では条件判定が常にTRUEです。
ということは、永久にループし続けることになります。
これを「無限ループ」と言います。
無限にループして処理を続けるということは、SIMのCPUパワーを常に使いっぱなしということです。
こんなスクリプトを書いてしまったら、スクリプトをリセットする以外には止める方法がなくなりますので注意しなければなりません。
立ちはだかる敵を配置するのは結構ですが、必ず倒す手段を用意しておくべきでしょう。

さて、もう一方のdo-whileも見てみましょう。

【do-whileの構文】
do {
  ここに書いたコードを実行し、論理演算式がTRUEの場合はdoに戻って繰り返す
} while (論理演算式);

これは、以下のようなパターンのときに使います。

do {
  厳しい修行の日々
} while (卒業試練に不合格);

do-while文の場合、最低一回は中身が実行されます。
上の例ですと、まず最初に必ず「修行の日々」を過ごすことになります。

修行の日々が終わると、師匠が出てきて最後の卒業試練が行われます。
修行が足らず、この試練に失敗すると、
「修行が足らぬわ!」
とか言われて再び「激しい修行の日々」へとループします。

見事に師匠を倒すと、
「もはや教えることは何もない・・・ぐふっ」
とか言ってループが終了するわけです。

while文との違いは、do-while文の中身が最初に一回は必ず実行されるという点です。
いきなり「卒業の試練」を受けることは許されません。
お話が盛り上がりませんから。
あくまでも最初は「修行の日々」が無ければならないのです。

while文にしろ、do-while文にしろ、注意すべきなのは無限ループです。
立ちはだかる敵を倒す方法や、こにくたらしい師匠をギャフンと言わせる方法は必ず用意しておきましょう。

最後に具体例をば。

vector pos = llGetPos();
while(pos.z < 500.0){
  pos.z += 1.0;
  llSetPos(pos);
}

オブジェクトのZ位置が500.0以上になるまで、1mずつオブジェクトを動かす処理になります。

for

for文もwhile同様ループを行うフロー制御文なのですが、ループ時に変数の増加等の処理を行うことができる点が異なります。

構文から見てみます。

for(初期化; 論理演算式; 更新処理) {
  論理演算式がTRUEの間、ここに書いた処理が繰り返し実行される
}

「初期化」と「更新処理」の部分が目新しい要素ですね。

「初期化」の部分はループに入る直前に一度だけ実行されます。
「更新処理」のほうはループの最後、次のループに入るかどうか判定が行われる直前に実行されます。

言葉で書くとよくわからないので、for文をwhile文に書き換えてみましょう。

初期化処理;
while(論理演算式){
  論理演算式がTRUEの間、ここに書いた処理が繰り返し実行される
  更新処理;
}

このようになります。

このパターンのループがどういうときに使われるかと言うと、例えばリスト型の変数の全要素を表示したいような場合、

list colors = ["red","green","blue"];
integer i = 0; // 初期化処理
while(i < llGetListLength(colors)){
  llSay(0, llList2String(colors, i));
  i ++; // 更新処理
}

リスト型変数colorsには、3つの要素があります。
whileループに入る前に整数型の変数iを定義し、値を0にします。

そしてループに入りますが、「llGetListLength(colors)」というのはリストの項目数を返す関数ですので、3です。
iは0ですので、(0 < 3)はTRUEです。

「llList2String(colors, i)」はリストの中身を文字列型で取り出す関数です。
iが0なので、一番最初の要素"red"が取り出され、チャット欄に表示されます。

ループの最後の「i ++」は先日やった通り、「iの値を1増やす」計算です。
これでiが1になりました。

whileの条件判定に戻り、(1 < 3)はまだTRUEです。
再びループの中身が実行され、今度は"green"がチャット欄に表示されます。
i ++でiが2となり、再び条件判定・・・(2 < 3)なのでループ内が処理され、今度は"blue"が表示されます。
ループの最後でi ++されてiは3になります。

すると今度は(3 < 3)がFALSEとなるので、ループは終了します。

以上、"red","green","blue"のリストの中身が順番に処理されました。
このようなパターンのときに、forループは使われます。

上記のwhile文で書いたループをfor文に書き換えると以下のようになります。

list colors = ["red","green","blue"];
integer i;
for(i = 0; i < llGetListLength(colors); i++){
  llSay(0, llList2String(colors, i));
}

1行減ったので、ちょっとだけスッキリした気分になりますw(あくまで気分だけw)
while文で書いたときと、処理の内容に違いはありません。

for文の利点は、コードがスッキリすることと、無限ループの罠に陥りにくい点です。
先ほどのwhile文、
list colors = ["red","green","blue"];
integer i = 0; // 初期化処理
while(i < llGetListLength(colors)){
  llSay(0, llList2String(colors, i));
  i ++; // 更新処理
}

うっかり更新処理の「i ++」を書き忘れてしまうと、
list colors = ["red","green","blue"];
integer i = 0; // 初期化処理
while(i < llGetListLength(colors)){
  llSay(0, llList2String(colors, i));
}

iの値がいつまで経っても変化しないため、無限ループになります。

これがfor文だと、
list colors = ["red","green","blue"];
integer i;
for(i = 0; i < llGetListLength(colors); ){
  llSay(0, llList2String(colors, i));
}

更新処理の部分をうっかり書き忘れるというのは、あまり無さそうな気が・・・。
・・・・・。
・・・いざ書き忘れてみると、あとから間違いに気づきにくいですね(^^;;;
微妙なところですw

まぁ、SLに限って言えば、whileを使ってもforを使っても、どちらでも良いかも知れません(^^;

return

リターンはユーザー関数やイベントを中断するときに使います。
また、ユーザー関数で値を返すときにも使われます。

緊急脱出ポッドみたいなものだと思ってください。
スターデストロイヤーに拿捕されたレイア・オーガナのコレリアン・コルベットから、R2-D2とC3-POが脱出したときのアレです。
リターンを使うと、ダース・ベイダーの手を逃れてデス・スターの設計図を反乱同盟軍に届けることが可能です。
おまけに長年農夫として暮らしていた若造をフォースに目覚めさせ、果てはシスの暗黒卿を打ち倒すことさえできるのです。

・・・落としどころがわからないのでサラっと流して下さい(^^;

例えばこんな感じで使います。

touch start(integer total_number) {
  if ( llDetectedKey(0) != llGetOwner() ){
    return;
  }
  // オーナーだけに許される処理をここに書く
}

この例だと別にリターンを使わなくちゃいけないわけではないのですが・・・。
タッチした人のUUIDとオーナーのUUIDを比較し、一致しない場合(=オーナー以外がタッチした場合)はリターンでイベントを終了しています。
タッチしたのがオーナーだった場合はリターンせず、その先に書かれた処理が実行されます。

値を返すユーザー関数を作った場合は以下のようになります。

list keys = ["key1", "key2", "key3"];
list valuse = [ 100, 50, -25];

integer get_value(string keyword) {
  integer i = llListFindList(keys, [keyword]);
  if (i != -1 ) {
    return llList2Integer(values, i);
  }
  return 0;
}

このユーザー関数get_valueは、引数keywordで指定された文字列をリストkeysの中から探します。
そして見つかった場合はvaluesの同じ位置にある整数値を返します。
見つからなかった場合は0を返します。

この使い方については別途ユーザー関数の説明をするときにでも改めてまた書きたいと思います。

jump

ジャンプはコードの中の指定位置に飛ぶフロー制御です。
あんまり使いどころがないですが・・・。

while(orenome == "black"){
  if (llFloor(llFrand(10.0)) == 0){
    jump loopout;
  }
}
@loopout;

例えばwhileループからの脱出などに使います。
上記の場合、一見無限ループになっていますが、ランダムなタイミングで@loopoutの位置にジャンプするようになっています。

なお、ジャンプはどこにでも飛べるわけではありません。
ジャンプ先に選べるのは同じユーザー関数内か、同じイベント内だけです。

state

ステートはlslスクリプトのステートを切り替えます。

default{
  state_entry(){
    llSay(0, "default state.");
  }
  
  touch_start(integer detected){
    state next;
  }
}

state next{
  state_entry(){
    llSay(0, "next state.");
  }
  
  touch_start(integer detected){
    state default;
  }
}

タッチするたびに、defaultステートとnextステートを行ったり来たりする例です。

なお、ステート変更はイベント内でしか使えません。
実は裏技を使うとユーザー関数の中からも使えるのですが・・・どうも正式な仕様ではないらしく、後々修正される可能性があるので内緒にしておきます(^^;

※ユーザー関数の中からステートを変える方法:
SetStateDefault(){
  if(TRUE){
    state default;
  }
}


ではまた次回・・・。
基礎知識