BlackSheep-LSL@Wiki

ノートを使おう

最終更新:

匿名ユーザー

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

はじめに


ベンダースクリプトの改良編です。
前回までで機能面は十分に満たすものを作りましたが、今回は使い勝手のよいものになるよう改造を施します。

改良点は商品データの管理の部分です。
前回までのスクリプトでは、商品データをリスト型変数に定義していましたが、この方法だと商品の追加や変更があったときにいちいちスクリプトを直さなければなりません。
そこで、商品データはSLのノートカードを使って管理するようにし、ノートカードを切り替えるだけで扱う商品を変えられるようなベンダーを考えます。

商品のデータに対して、そのデータを使ってアレコレ操作する仕組みを「ロジック」と言います。
ノートカードを使う方法は、言わば「データ」と「ロジック」を切り離して管理する手法になります。
これはシステム工学的に言っても基本的なところですね。

ノートカード読取の仕組み


SLのノートカードは、その名の通り、自由に文章を書いておけるテキストファイルです。
日本語を書いておくこともできますので、ちょっとしたメモなどに使えます。
私はお客さんからのクレームのIM(w)などを保管するのに使ったりしています。

スクリプトでは、このノートカードをデータの読み取り元として使うことができます。
あくまでも「読み取り元」です。「保存先」としては使えません。
ノートカードに書いたデータを変更する際は、直接ノートカードを編集することになります。

llGetNotecardLine関数


ノートカードを読み取る命令はllGetNotecardLine()といいます。
この関数は、指定されたノートカードの指定された行を読み込みます。

 key llGetNotecardLine(string name, integer line)

  • string name

ノートカード名です。
コンテンツ内にあるノートカードの名前でなければなりません。

  • integer line

何行目を読むかです。
一番最初の行は0行目になっています。

  • 戻り値

key型のデータが戻ってきます。
これはデータ読取のリクエスト番号です。
どういうことかというと、ズバリ、高速道路のサービスエリアとかのフードコートの番号札、アレです。

フードコートで食事を取る場合、我々はたいていの場合、食券を買い求めてカウンターに行きます。
カウンターで、我々は食券をチラつかせながら、押し殺した声で
「ラーメンと餃子を出せ。早くしろ」
こう要求するわけです。
するとカウンター内の店員は、
「ではこの53番の番号札を持ってお待ちください」
と対応してくるのです。
我々が番号札を握り締め、監視カメラに映らないよう隅っこの席で顔を隠しながら待っていると、
「53番でお待ちの方!」
と呼ばれます。
そこで我々は番号札を手にカウンターに戻り、まんまと食事を手に入れるのです。

llGetNotecardLine()を使ったときの戻り値は、まさにこの番号札です。
我々はllGetNotecardLine()を使って、
「ノートカード"hogehoge"の7行目のデータをよこせ」
このように要求します。
するとスクリプトは、
「では番号xxxxx-xxx...xxxxでお待ちください」
とkey型変数を返してくるのです。
そして我々は要求したデータが出てくるのを、周囲の目を気にしながら待つことになります。

dataserverイベント


「53番でお待ちの方!」に相当するイベントを、dataserver?イベントといいます。

 dataserver(key queryid, string data){
   // 処理
 }

引数にkey型のqueryidというのがありますね。
これが番号札です。
llGetNotecardLine()で要求したときに返されたkey型のデータが、ここに入ってきます。

もう一つのstring型の引数dataはラーメンや餃子・・・いや、ノートカードの中身です。
要求したノートカードの指定行に書かれているデータがここに入ってきます。

要求は一行ごとですので、ノートカードの全てのデータを読み取ろうと思ったら、行数文だけ繰り返し要求を出すことになります。
フードコートで繰り返し繰り返し食券を出すと迷惑がられますが、スクリプトは幸いなことに文句を言いません。
ありがたいことです。

典型的なパターン


ノートカードを全て読むときは、以下のような典型的なパターンになります。
これはもう丸暗記しちゃったほうが早いくらいに使いまわしがききます。

integer read_line;
String notecard_name = "hogehoge";
key query_id;

load_start(){
  llOwnerSay("Now Loading..");
  read_line = 0;
  if (llGetInventoryType(notecard_name) == INVENTORY_NOTECARD) {
    query_id = llGetNotecardLine(notecard_name, read_line); // request first line
  }else{
    llOwnerSay("Not found notecard " + notecard_name + ", Touch for retry.");
  }
}

default {
  state_entry(){
    state load;
  }
}

state main { // メイン処理を行うステート
  state_entry(){
    
  }
}

state load{
  state_entry(){
    load_start();
  }

  touch_start(integer detected){
    if (llGetOwner() == llDetectedKey(0)){
      load_start();
    }
  }

  dataserver(key queryid, string data) {
    if (queryid == query_id) {
      if (data != EOF) {
        // ここに読み取ったデータの処理を追加する
        read_line ++; 
        query_id = llGetNotecardLine(notecard_name, read_line); 
      }else{
        llOwnerSay("Loading complete.");
        state main; // メイン処理を行うステートに遷移
      }
    }
  }
}

loadステートはデータロード中のステートです。
load_start()ユーザー関数はノートカードの読取リクエストを行います。

ノートカードを読み取りたいときは、変数notecard_nameに読み取りたいノートカード名をセットし、ステートをloadにするだけです。
notecard_nameにセットされているノートカードが見つからない場合は、
 "Not found notecard ノートカード名, Touch for retry."
(指定されたノートカードが無いよ。タッチするとリトライするよ)
とメッセージを表示します。
ノートカードを入れ忘れていたような場合はノートカードをコンテンツに入れ、タッチすると読取が開始されます。

ノートカードの読取が一番最後の行まで行くと、dataserverイベントの引数dataにEOFという値が入ってきます。
EOFは「End of File」の略です。最後まで読んだよという意味ですね。
dataがEOFだった場合はロード処理終了ですのでメインのステートに遷移させています。

ノートカード対応ベンダー


それでは、ベンダースクリプトにノートカード読取の機能を追加してみましょう。
defaultステートでパーミッションを取得した後、loadステートでノートカードを読み、activateステートで販売開始、のようにすると簡単でしょう。

list commodity = [];
integer read_line;
string notecard_name = "itemlist";
key query_id;

integer current_id = 0;
integer view_side = 1;

load_start(){
  llOwnerSay("Now Loading..");
  read_line = 0;
  if (llGetInventoryType(notecard_name) == INVENTORY_NOTECARD) {
    query_id = llGetNotecardLine(notecard_name, read_line); // request first line
  }else{
    llOwnerSay("Not found notecard " + notecard_name + ", Touch for retry.");
  }
}

set_commodity(){
  llSetTexture(llList2String(commodity, current_id * 4 + 1), view_side);
  llSetPayPrice(PAY_HIDE, [llList2Integer(commodity, current_id * 4 + 2),
    PAY_HIDE, PAY_HIDE, PAY_HIDE]);
}

default {
  state_entry(){
    llRequestPermissions(llGetOwner(), PERMISSION_DEBIT);
  }
  
  run_time_permissions(integer perm) {
    if (perm & PERMISSION_DEBIT){
      state load;
    } else {
      llRequestPermissions(llGetOwner(), PERMISSION_DEBIT);
    }
  }
}

state active {
  state_entry(){
    set_commodity();
  }
  
  touch_start(integer detected){
    integer i = llDetectedLinkNumber(0);
    if (i == 2) { // back button
      current_id --;
      if (current_id < 0){
        current_id = llGetListLength(commodity) / 4 - 1;
      }
      set_commodity();
    }else if (i == 3) { // next button
      current_id ++;
      if (current_id >= llGetListLength(commodity) / 4) {
        current_id = 0;
      }
      set_commodity();
    } else {
      if (llGetOwner() == llDetectedKey(0)){
        state load;
      }
    }
  }
  
  money(key id, integer amount){
    integer p = llList2Integer(commodity, current_id * 4 + 2);
    if (amount == p){
      list items = llParseString2List(
        llList2String(commodity, current_id * 4 + 3), ["|"],[]
      );
      integer i;
      for (i = 0; i < llGetListLength(items); i++){
        if (llGetInventoryType( llList2String(items, i)) == INVENTORY_NONE) {
          llSay(0, "I am sorry very much. This commodity is sold out now.");
          llGiveMoney(id, amount);
          return;
        }
      }
      llSay(0, "Thank you for purchasing! Please wait until getting the commodity...");
      llGiveInventoryList(id, llList2String(commodity, current_id * 4), items);
    }else{
      llSay(0, "You paid wrong amount. I repays to you " + (string)amount + "L$.");
      llGiveMoney(id, amount);
    }
  }
}

state load{
  state_entry(){
    load_start();
  }

  touch_start(integer detected){
    if (llGetOwner() == llDetectedKey(0)){
      load_start();
    }
  }

  dataserver(key queryid, string data) {
    if (queryid == query_id) {
      if (data != EOF) {
        list chk_l = llParseString2List(data, [","], []);
        commodity += chk_l;
        read_line ++; 
        query_id = llGetNotecardLine(notecard_name, read_line); 
      }else{
        llOwnerSay("Loading complete.");
        state active;
      }
    }
  }
}

llParseString2List関数


ついでに複数のアイテムを渡すのにも対応してみました。

ノートカードには、以下の形式で商品データを書きます。

 ItemName1,ItemImage1,100,Item1-1|Item1-2|Item1-3
 ItemName2,ItemImage2,200,Item2-1
 ItemName3,ItemImage3,150,Item3-1|Item3-2

先頭から「商品名」「テクスチャ名」「価格」「オブジェクトリスト」です。
この4つのデータを「,」で区切って書きます。

一番最後のオブジェクトリストは、「|」で区切って書きます。
l(エル)ではありません。日本語のキーボードだと右上にある\キーをShift押しながら入力した「|」です。

llRequestPermissions()関数によって、このノートカードが一行ずつ読み込まれます。
dataserver?イベントの中で、
 list chk_l = llParseString2List(data, [","], []);
というのがありますね。
ここでは読み込んだデータを「,」を区切りにして分解しています。

 list llParseString2List(string src, list separators, list spacers)

「string src」で指定される文字列を、「list separators」を区切りとしてlist型に変換する関数です。
つまり、
 "ItemName1,ItemImage1,100,Item1-1|Item1-2|Item1-3"
というノートカードの一行を、
 ["ItemName1", "ItemImage1", 100, "Item1-1|Item1-2|Item1-3"]
こういうリストに変換します。
この形になってしまえば、前回作ったベンダースクリプトと同じ形です。

ただ、最後のオブジェクト名のところを「|」で区切ったリストにしていますので、実際に購入手続きの際、
 list items = llParseString2List(
   llList2String(commodity, current_id * 4 + 3), ["|"],[]
 );
今度は「|」を区切りとして分解しています。
この部分は、
 "Item1-1|Item1-2|Item1-3"
という文字列を、
 ["Item1-1", "Item1-2", "Item1-3"]
このようにリスト化することになります。

これで渡すべきオブジェクトのリストが出来ますので、あとはそれぞれのオブジェクトの存在確認を行い、お客さんに渡します。
オブジェクトの存在確認をするところで、初めて登場するfor文があります。

for文


 for (i = 0; i < llGetListLength(items); i++){
   // 繰り返し処理
 }

今まで出てこなかったのが不思議なほど基本的な文です・・・(^^;
このfor文というのは「繰り返し処理」をする際に使われます。

 for (最初の状態; 繰り返す条件; 更新の式){
   // 繰り返し処理
 }

意味は上記のようになります。
この文がどういう動きをするかというと、
(1)まず「最初の状態」の部分が実行される
(2)「繰り返す条件」が正しいかどうかチェックし、正しければ(3)、正しくなければfor文を抜ける
(3)「繰り返し処理」(for文の{}の中身)が実行される
(4)「更新の式」が実行され、(2)に戻る。
文章で書くと、少々ややこしそうな雰囲気になりますね(^^;

今回のスクリプトを例として具体的に見てみましょう。

 for (i = 0; i < llGetListLength(items); i++){
   if (llGetInventoryType( llList2String(items, i)) == INVENTORY_NONE) {
     llSay(0, "I am sorry very much. This commodity is sold out now.");
     llGiveMoney(id, amount);
     return;
   }
 }

(1)まず「最初の状態」の部分が実行される

「最初の状態」の部分は「i = 0」と書いてあります。
ですので、まずはiが0になります。

(2)「繰り返す条件」が正しいかどうかチェックし、正しければ(3)、正しくなければfor文を抜ける

「繰り返す条件」は「i < llGetListLength(items)」です。
llGetListLength(items)はlist型変数itemsの項目数を取得する関数でした。
仮にitemsが、
 ["Item1-1", "Item1-2", "Item1-3"]
だとすると、項目数は3ですね。
iは(1)で0になっていますので、
 「0 < 3」(0は3より小さいか?)
小さいですね。
条件が正しいので、(3)に進みます。

(3)「繰り返し処理」(for文の{}の中身)が実行される
for文の{}の中身は以下のようになっています。

   if (llGetInventoryType( llList2String(items, i)) == INVENTORY_NONE) {
     llSay(0, "I am sorry very much. This commodity is sold out now.");
     llGiveMoney(id, amount);
     return;
   }

アイテムの存在確認をして、もしも無ければ返金する処理ですね。
returnというのはイベントを終了する命令です。
返金したらそれ以上の存在確認は必要ありませんので、returnでイベントを抜けています。

さて。
iは0ですのでllList2String(items, i)で取得されるのは"Item1-1"ですね。
もしもコンテンツの中に"Item1-1"が無ければ、返金して終了、コンテンツの中にあれば(4)に進みます。

(4)「更新の式」が実行され、(2)に戻る。

「更新の式」は「i++」です。
これはiを1増やす計算でしたね。
iは0なので、+1されて1になり、(2)に戻ります。

(2)「繰り返す条件」が正しいかどうかチェックし、正しければ(3)、正しくなければfor文を抜ける

「繰り返す条件」は「i < llGetListLength(items)」です。
itemsの項目数は3のままです。
iは1になっていますが、
 「1 < 3」(1は3より小さいか?)
条件は正しいので、(3)に進みます。

(3)「繰り返し処理」(for文の{}の中身)が実行される

今度はiが1になっていますので、llList2String(items, i)で取得されるのは"Item1-2"ですね。
もしもコンテンツの中に"Item1-2"が無ければ、返金して終了、コンテンツの中にあれば(4)に進みます。

(4)「更新の式」が実行され、(2)に戻る。

iがさらに1増えます。
1から1増えて、2になります。

(2)「繰り返す条件」が正しいかどうかチェックし、正しければ(3)、正しくなければfor文を抜ける

「繰り返す条件」は「i < llGetListLength(items)」です。
itemsの項目数は3のまま、iは2になりました。
 「2 < 3」(2は3より小さいか?)
まだ条件は正しいので、(3)に進みます。

(3)「繰り返し処理」(for文の{}の中身)が実行される

iが2になっていますので、llList2String(items, i)で取得されるのは"Item1-3"です。
もしもコンテンツの中に"Item1-3"が無ければ、返金して終了、コンテンツの中にあれば(4)に進みます。

(4)「更新の式」が実行され、(2)に戻る。

iがさらに1増えて、3になります。

(2)「繰り返す条件」が正しいかどうかチェックし、正しければ(3)、正しくなければfor文を抜ける

「繰り返す条件」は「i < llGetListLength(items)」です。
itemsの項目数は3のまま、iは3ですので、
 「3 < 3」(3は3より小さいか?)
条件が正しくなくなりました。
よってfor文はここで終了し、スクリプトは先に進みます。

長々と書きましたが、これでitemsリストの中のオブジェクトが全て存在確認されたのがわかりましたでしょうか。
繰り返し処理(for文)を使うことにより、このダラダラをコンパクトに書くことができます。

これで商品の追加に関してはスクリプトを手直しすることなく、ノートカードの修正のみでOKになりました。
ノートカードの再読み込みについては、タッチイベントに処理を追加しています。
オーナーが商品画像にタッチしたときにステートをloadに変更し、再読み込みするようにしました。

今回の歩イント


  • ノートカードの読み取り:

 key llGetNotecardLine(string name, integer line)

nameで指定されたノートカードのline行目の読み込みを要求する。
番号札(key型)が返る。

  • 要求したデータの受信イベント:

 dataserver(key queryid, string data){
   // 処理
 }

番号札queryidでお待ちの方にデータdataが届く。

  • 文字列からリストへの変換:

 list llParseString2List(string src, list separators, list spacers)

「string src」で指定される文字列を、「list separators」を区切りとしてlist型に変換する。
なお、三番目の引数spacersも区切り文字として機能するが、結果のリストに含まれるかどうかの違いがある。

例:
 llParseString2List("A-B-C", ["-"], [])・・・["A", "B", "C"]を返す(区切り文字"-"は含まれない)
 llParseString2List("A-B-C", [], ["-"])・・・["A", "-", "B", "-", "C"]を返す(区切り文字"-"を含む)

  • for文:

 for (最初の状態; 繰り返す条件; 更新の式){
   // 繰り返し処理
 }

(1)「最初の状態」の部分が実行される
(2)「繰り返す条件」が正しいかどうかチェックし、正しければ(3)、正しくなければfor文を抜ける
(3)「繰り返し処理」(for文の{}の中身)が実行される
(4)「更新の式」が実行され、(2)に戻る。

  • retunr:

イベントやユーザー関数を抜ける

……ということでベンダーは今回で完成としたいと思います。
ベンダー作成を通じて知っていただきたかったことは、まず第一にパーミッションの考え方です。
lslのパーミッションは、個人的には非常に使いにくいと思っていますが、思ったようにスクリプトを動作させるためにはクリアしなければならない壁の一つです。
特にアニメーションを扱う際には・・・宿敵みたいな存在です(^^;

ノートカードの使い方も、覚えておくといろいろと応用が利く部分です。
テクスチャとサウンドをノートカードに書いて順番に表示・演奏するとか、ムービーのURLリストを作っておいて選べるようにするとか・・・。
特にあとからデータを増やせるようなスクリプトにするには、ノートカードを使うのがベストでしょう。

少々お堅いスクリプトを見てきましたので、次回は遊べるものが良いかもしれません。
アニメーションあたりでしょうかね(^^

  • 誤表記 > retunr: -- 読者P (2009-02-07 16:04:52)
  • ご表記 > 歩イント -- 読者Q (2009-03-04 15:30:16)
  • ↑ ○誤 ×ご -- 読者R (2009-03-04 16:44:17)
名前:
コメント:
記事メニュー
目安箱バナー