BlackSheep-LSL@Wiki

ステートのこと

最終更新:

mizcremorne

- view
メンバー限定 登録/ログイン

はじめに


今回はスクリプトの構成について少し突っ込んだ話をします。
具体的に実用性のあるスクリプトは出てきませんが、自分で1からスクリプトを書こうというとき、構成をどうするか考える際の参考にしていただければと思います。

このwikiの記事では、最初の頃にスクリプトの構成について触れました。
「ステート」「イベント」「処理」の3つの要素で成り立っているという話です。

私はこの3つがスクリプトの基本要素であると説明し、スクリプトを書く際には作ろうとしている機能がどんな「ステート」「イベント」「処理」になるのか意識すると良いと書いてきましたが、実際には「ステート」がどんな時に登場するのか、いまひとつハッキリしないという方もいらっしゃるのではないでしょうか。
というのも、今まで挙げてきたスクリプトの例の中には、defaultステートしか存在しないようなものが多々あるからです。

ステートって、どんなときに使うのでしょうか?

ステートの復習


ステートというのはスクリプトの「状態」であると説明してきました。
確かに、この説明は間違ってはいないのですが、かなり曖昧ではあります。

例えば照明スクリプトについて思い出してみて下さい。
照明の状態は、「点灯」「消灯」の二通りだと考えることができます。
ですが、私はこの二つの状態をステートとして採用していません。
その一方で、ベンダースクリプトのときには、defaultステートに加えて、
「ロード中」「販売中」の二つのステートを追加してスクリプトを書いています。

なぜ一方ではステートを使わず、一方ではステートを使っているのでしょうか。
決して気まぐれでそうしているわけではありません(^^;
ステートを使うべきか、使わずに済ませるべきか、その判断基準があります。

例えば照明スクリプトはステートを使うと実現不可能かというと、決してそんなことはありません。
実際、ステートredやステートblueなどを使った照明スクリプトも紹介しました。
ですが、「似たような繰り返しになるのでステートを使わず、ユーザー関数を使うべきだ」ということで、ユーザー関数の作り方を説明しましたね。

これが一つのポイントになります。
「似たような繰り返しかどうか」
もっと突き詰めて言うと、
「ステートごとに扱うべきイベントが同じかどうか」
これが大きな判断基準です。

ステートの使いどころ


ステートを複数用意したときの最大のメリットというのは、ステートごとに扱うべきイベントをそれぞれに定義できる点です。
つまり、
あるときには「タッチ」イベントを扱いたいが、
別のあるときには「タッチ」イベントを扱いたくない。
そのような状況が考えれるとき、ステートの使用を考えるべきです。

照明スクリプトを例にとって考えてみましょう。
考えられる「状態」に対して、扱うイベントを並べてみます。

状態 扱うイベント
消灯 タッチ
赤で点灯 タッチ
緑で点灯 タッチ
青で点灯 タッチ

全ての「状態」に関して「扱うイベント」が一緒です。
このような場合にはステートに分けるメリットがありません。
ですから、ステートを使わず、ユーザー関数を使って処理を統一したわけです。

一方、ベンダースクリプトはどうでしょうか。

状態 扱うイベント
ロード中 データ取得
商品Aを表示 タッチ
商品Bを表示 タッチ
商品Cを表示 タッチ


敢えて商品の表示を「状態」として分けてみました。
ロード中のみ扱うイベントが異なるのがわかります。
ですので、ロード中は別ステートとし、商品の表示は1つのステートにまとめるのが最もスマートな構成だと判断したのです。

ステートとグローバル変数


さらに突っ込んで考えてみましょう。
「状態」によって「イベント」が異なるときには、必ずステートを使わなければならないのでしょうか?

アニメーションのスクリプトを思い出してみて下さい。
タッチするとパーミッションダイアログが開き、アニメーション許可が出たらアニメーションさせるというものでした。

状態 扱うイベント
初期状態 タッチ
パーミッション申請中 パーミッションイベント/タッチ不可
パーミッション取得後 タッチ

厳密に「状態」を分けるとこのようになります。
扱うべきイベントが異なっていますが、このスクリプトではステートを分けてはいません。

ではどのようにして扱うべきイベントを判断しているのかというと、変数の値によって、イベントの中で処理したりしなかったりしているのです。
アニメーションのスクリプトをもう一度引っ張り出してみます。

string animation_name="boogie";
key agent = NULL_KEY;

default {
  touch_start(integer detected){
    if (agent == NULL_KEY) {
      agent = llDetectedKey(0);
      llRequestPermissions(agent, PERMISSION_TRIGGER_ANIMATION);
    }
  }

  run_time_permissions(integer perm) {
    key perm_key = llGetPermissionsKey();
    if (perm_key == agent) {
      if (perm & PERMISSION_TRIGGER_ANIMATION){
        key chk = llGetInventoryKey(animation_name);
        list anms = llGetAnimationList(agent);
        integer i;
        for (i = 0; i < llGetListLength(anms); i++){
          if (chk == llList2Key(anms, i)) {
            llStopAnimation(animation_name);
            agent = NULL_KEY;
            return;
          }
        }
        llStartAnimation(animation_name);
      }
      agent = NULL_KEY;
    }
  }
}

タッチイベントのところを見て下さい。
最初にkey型変数agentがNULL_KEYかどうかを判断し、NULL_KEYの場合のみ処理をしています。
「パーミッション申請中」にはこのagentには何らかのUUIDが入ります。
ですので、「パーミッション申請中」に「タッチ」イベントが起きても、何も処理がされないようになっているのです。
このように、「状態」に応じて扱うべきイベントが異なる場合でも、ステートを使わずに済ませる方法はあります。

グローバル変数


ちょっと脱線になりますが、今まで説明していなかったことを補足します。
key型変数agentのように、スクリプトの先頭で宣言されている変数のことを「グローバル変数」と言います。
「グローバル変数」はスクリプトのあらゆるステート、イベント、ユーザー関数の中からでも使うことができます。

「グローバル変数」以外の変数は、その変数が宣言された場所でしか使えません。
例えば、上記スクリプトの中のrun_time_permissions?イベントの最初にkey型変数perm_keyが宣言されていますが、このperm_keyはrun_time_permissions?イベントの中でしか使うことができません。
さらにその続きで、if文の中でkey型変数chkやlist型変数anms、integer型変数iが出てきますが、これらはif文の中でしか使えません。
このように使用できる場所が限られている変数のことを「ローカル変数」と言います。
イベントの引数もローカル変数です。
従って、例えばタッチイベントの引数permは、このタッチイベントの中でしか使うことができません。

ステートの話に戻りますが、この例のように、グローバル変数を使うとステートを使わずとも扱うべきイベントのコントロールが可能です。
となると、
「じゃあ一体、ステートを使うべきなのはどんな場合なんだ?」
と悩んでしまいそうですね(^^;

基本的には「状態」によって「イベント」が異なるとき、です。
その場合でも、グローバル変数を使ってイベントの処理を分けることは可能です。
あとはもう、各人のスクリプトのスタイルだと言ってもいいでしょう。
グローバル変数を使ったほうがわかりやすいと思えば、それでも構いませんし、ステートのほうがいいと思えばそれでOKです。

ステートの注意点


最後に、ステートを使う際の注意点です。

特にはまりがちなのは、listenを使うスクリプトでステートを変更したときの動きです。
実はlistenはステートを変更すると自動的にOFFになります。
defaultステートのstate_entry?イベントでlistenをONにし、「これで安心」と思っていると、ステート変更をしたときに自動的にOFFになり、一生懸命チャットでコマンドを発言しているのにスクリプトが反応しない、と悩みかねません。
listenはステートをまたがって使えないということを覚えておいて下さい。

それからタイマーイベントにも注意しましょう。
タイマーはlistenとは逆に、他のステートに変わった後も動作し続けます。

以下のような場合に予期せぬタイミングでタイマーが発動したりするので注意です。

default {
  touch_start(integer detected){
    llSetTimerEvent(60.0);
    state active;
  }
  
  timer(){
    llSay(0, "timer!");
  }
}

state active{
  touch_start(integer detected){
    state default;
  }
}

defaultステートのタッチイベントが発生した時点で、タイマーは60秒にセットされます。
そのあとactiveステートに変更されても、タイマーはチクタクと動き続け、60秒経つとタイマーイベントを起こそうとします。
ですが、activeステートにはタイマーイベントがないため、イベントは処理されずに待ち続けることになります。
activeステートでタッチするとdefaultステートに戻りますが、その瞬間に待ち続けていたタイマーイベントが発生し、「timer!」とわめき出します。

ステートをまたがってタイマーを使用するのでなければ、ステートチェンジの前にタイマーをOFFにするか、新しいステートのstate_entry?イベントでタイマーをOFFにするようにしましょう。

今回のポイント


  • ステートの使いどころ
「状態」によって扱う「イベント」が異なる場合にはステートの使用を検討する

・・・・・・これだけですね(^^;
すでにスクリプトに慣れている人や、プログラミングの経験が豊富な方であれば、このことは感覚的に理解できているかと思います。
ですが初めてlslに触れるという方にとっては、使いどころがイマイチわかりにくいのがステートではないでしょうか。
そういうわけでステートの使い方については、どこかで一度詳しく説明しておこうと思っていましたが、なかなか機会がなかったので今回書かせてもらいました。

・・・・・・とか言ってますが、実は昨日風邪引いて寝ていたため、今日の分の記事を書いていなかったのが根本原因ですw
いつも一日先の記事を用意しているので、昨日の分は自動配信で救われたのが幸いでしたが(^^;

ではまた来週~。
記事メニュー
目安箱バナー