ソラマメブログ

2007年05月31日

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

今回は「フロー制御」についてのお話です。
フロー制御というのは、条件によって実行するコードを変えたり、繰り返し処理を行ったりするものです。
また、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;
  }
}


ではまた次回・・・。


同じカテゴリー(基礎知識)の記事
 関数(lsl基礎知識 第五回) (2007-06-06 12:12)
 演算子(lsl基礎知識 第三回) (2007-05-29 12:15)
 変数(lsl基礎知識 第二回) (2007-05-25 12:15)
 スクリプト(lsl基礎知識 第一回) (2007-05-24 12:15)
この記事へのトラックバック
たいした話じゃないのですが、Makapuさんのブログを読んでいて思ったのですがLSL WikiにあるFlow Controlは日本語だと制御フローになるのではないかと思いました。英語だとControl Flowじゃないの...
Flow Control【keimar旅日記(仮)】at 2007年06月03日 20:36
この記事へのコメント
こんにちは。だんだん難しくなってきたので、脱落しそうです。
ところで、do whileの説明のところですが、whileの中味は卒業試練に不合格ということですよね?それとも、whileがdoの後ろに来ている場合は、whileの中味が真の場合は、doに戻らず、そこのループを抜けるということなのでしょうか?
Posted by sheila6225 Allen at 2007年06月01日 20:56
>sheila6225 Allenさん

>whileの中味は卒業試練に不合格ということですよね?
そのとおりです。
「不」の字が抜けておりました。
さっそく修正しておきます。
失礼いたしました。
Posted by Miz at 2007年06月02日 13:27
if が多いとスキルが低いってのは異論があるのですが、それはおいておいて、ひとつの関数/イベント内で、
if (..) { .. } else if (... ) { ...
のようにelse ifを繰り返していくと、20個目くらいでSyntax errorになるんですが、そういう仕様なんでしょうか。
switchみたいのがあればいいんですが、、
とりあえず、意味もなく関数にして分割して処理するようにしましたが。
Posted by yammy at 2007年10月10日 00:01
>yammyさん

ifとスキルの関係は聞き逃して下さい(^^;
単にifは「使いやすい」と言いたかっただけで、表現が不適切でしたね。
失礼いたしました。
もちろんifでしか書けないものもありますので、スキルが上がるとifが必要なくなるようなこともありません。

elseによる条件の追加は23個までが限度のようです。
一番最初のifと23個のelseで合計24個の条件判定で限界になります。

Syntax errorになるのは何だか奇妙な感じがしますがLSLの仕様みたいです。
Posted by MizMiz at 2007年10月10日 10:59
こんにちは、

いつも丁寧な解説すばらしいですね^^
本当にお世話になってます。

elseの制限が23個まで!!
ってちょっと驚きました。

LSL Wikiも確認したのですが、
(24個以上つかいたいときは)if elseを使わずに、ifブロックの最後で、returnとかjumpとかを使ったらいいんだよーみたいなことが書いてますね。

switchがないこととか・・・
LSLのフロー制御って、なかなか使いづらい感じです。
Posted by jinko at 2007年10月14日 09:35
>jinkoさん

あまり参考になる話ではないと思うので、単なる雑談としてコメントさせていただきます。

ifが24個以上の分岐を必要とするコードは、経験上あまり無いのですが、リストを使うとシンプルに書ける場合があります。

例えば文字列msgの中に色名が入っていて、色名に応じてvector値paramを求めたい場合、ifを使って書くと、

if (msg == "red"){
param = <1,0,0>;
}else if (msg == "blue"){
param = <0,1,0>;
}else if (msg == "yellow"){
param = <1,1,0>;
(以下略)

これを延々書くことになり、24色以上あった場合には判定不能になります。
同じことを実現するのに、リストを使うとシンプルに書けます。

list cmd = [
"red","blue","yellow"......."white" // 24個以上のコマンド
];

list color = [
<1,0,0>, <0,0,1>, <1,1,0>........<1,1,1> // コマンドに対応するパラメータ
];

vector param = <0,0,0>;
integer idx = llListFindList(cmd, [msg]);
if (idx != -1){
param = llList2Vector(color, idx);
}

以上、if条件は一つだけです。
どちらが処理効率が良いかはまた別問題ですが、コードのスマートさという観点では私は下の例が好みです。

この例では単純に色名の処理だけですが、もしもif文によって大きく処理を変えたりしたい場合は、私なら別関数にするか、別ステート、別スクリプトにします。
あらゆる条件判定をifでまかなうのではなく、
「関数を分けたらもっとシンプルにならないか?」
「全部が同じ状況で起こるわけじゃないからステートを分けたらどうか」
「そもそも違う機能が一つのスクリプトに詰め込まれてるから、二つに分けようか」
分岐のレベル感によって分けます。
ifで分けるのはifでしか分けようがない場合、ある意味最終手段です(^^;

そうすることにより、スクリプトの構造はとてもスッキリと体系立ったものになり、あとあとメンテナンスや改良が楽になるからです。
とは言うものの、慣れや考え方、コーディングセンスや美意識なども関わってくる部分ですので、何がベストかは一概には言えません。

if談義は結構突き詰めてくと面白いですねぇ~(^^
Posted by MizMiz at 2007年10月15日 13:56
こんにちは、
お忙しいMizさんにレスいただけるとは・・
しかも丁寧なサンプル付とは!!
感激です!

ワタシも、24個以上ネストされたif文の経験はないのですが、
(浅い経験ですが・・・)
コマンド処理のところでは、分岐が増えてしまいます。

listen(integer channel, string name, key id, string message){
if (message == "open") do_open();
else if (message == "close") do_close();
else if (message == "jump") do_jump();
}
のような場合。
コマンドが24個以上あるなんて考えにくいのですが、
てゆーか、そんなたくさんコマンドあったら覚えられへん!
でも、どうしてもそうなったら、中身を関数にしてreturnで抜ける感じかなー

do_command(string message){ //←処理用の関数
if (message == "open") { do_open(); return; } //←処理後ぬける
if (message == "close") { do_close(); return; }
if (message == "jump") { do_jump(); return; }
}
listen(integer channel, string name, key id, string message){
do_command( message );//←処理用の関数を呼出し
}

考えようによっては、こんな横着バージョンもありかなと
実行速度がどれだけ犠牲になるかわかりませんが、
コマンド分岐のような単純なケースだったらelse不要ですよねー

listen(integer channel, string name, key id, string message){
if (message == "open") do_open();//←処理後ぬけない
if (message == "close") do_close();
if (message == "jump") do_jump();
}

まー、最後の例は、使わないですけどね・・
わかりきったことを長々とすいません^^;
「雑談」というキーワードに甘えてしまいました。

んで、別関数というところまで、ギリギリできそうなのですが、
別ステート、別スクリプトに分割というのは、まだまだ修行が必要そうです。
LSLって奥が深いですね。
Posted by jinkojinko at 2007年10月16日 09:40
あ。なんか盛り上がってるw
私が作りたかったのは、
link_message( ... ) {
Posted by yammy at 2007年10月16日 14:20
(T_T)間違っておしちゃった

あ。なんか盛り上がってるw
私が作りたかったのは、
link_message( ... ) {
if (num ==1) {
a = (integer)str;
} else if (num == 2) {
b= (vector)str;
...
見たいな感じでまとめるのは難しいですねー。後の処理がなければ
returnしちゃってもいいですね。参考にします^^

#ってなに作ろうとしてるかわかるなー。。きっと^^;
Posted by yammy at 2007年10月16日 14:27
>jinkoさん

確かに後続に共通した処理がなければreturnで抜けるのもありですね。
コマンドで24個を越えるような場合は、かなり複雑なものを作っていると推察されるので、ifの限界より先に16kの壁に突き当たりそうな気もしますが(^^;

結構制限の多いLSLですが、いかにして制限を乗り越えるかを考えることに喜びを見出し始めるともう病気ですねw

>yammyさん

なるほど、その使い方だとif文のほうが素直かもしれません。
無理にif文を使わずに書けないこともないですが、
----------------------------------
list args = [ // 汎用変数格納用のリスト
 "",            // TYPE_INVALID 0
 0,             // TYPE_INTEGER 1
 0.0,            // TYPE_FLOAT 2
 "",             // TYPE_STRING 3
 NULL_KEY,       // TYPE_KEY 4
 ZERO_VECTOR,   // TYPE_VECTOR 5
 ZERO_ROTATION  // TYPE_ROTATION 6
]; // int,flo,str,ley,vec,rotの順で必ず格納する

integer ToInteger(){ // integer取り出し関数
 return (integer)llList2String(args,TYPE_INTEGER);
}

float ToFloat(){ // float取り出し関数
 return (float)llList2String(args,TYPE_FLOAT);
}

string ToString(){ // string取り出し関数
 return llList2String(args,TYPE_STRING);
}

// 以下各型ごとに関数を用意

default {
 link_message(....){
if (num <= TYPE_ROTATION && num > 0){
   // numで示されるリストargsの位置にstrを挿入
   args = llListReplaceList(args,[str],num,num);
  }

// 以降、必要に応じてToInteger関数などで取り出す
 }
}
----------------------------------
こんな感じでしょうかね。
ifを関数に置き換える例として提示しておきますね。
Posted by MizMiz at 2007年10月17日 10:40
ありがとうございますー
構造体なくて不便だと思ったら、list使うっていう手があるんですね。
(コストがどれくらいかかるかは疑問ですが)
っていうかlist使うなら送る側でlist丸ごとstringにして
おくっちゃえば受ける側は一行で済むし、そのほうがスマートなので
その作戦でいくことにします^^
Posted by yammy at 2007年10月17日 12:11
 
<ご注意>
書き込まれた内容は公開され、ブログの持ち主だけが削除できます。