ソラマメブログ

2007年04月18日

ノートを使おう(初級スクリプト第十二回)

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

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

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

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

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

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

  key llGetNotecardLine(string name, integer line)

string name

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

integer line

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

戻り値

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

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

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

「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;
      }
    }
  }
}


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

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

  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 (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リストを作っておいて選べるようにするとか・・・。
特にあとからデータを増やせるようなスクリプトにするには、ノートカードを使うのがベストでしょう。

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


同じカテゴリー(初級スクリプト)の記事画像
衝突判定(スクリプト初級第二十三回)
カメラ制御(スクリプト初級第二十二回)
センサーを使おう(スクリプト初級第二十回)
HUDを作ろう(スクリプト初級第十六回)
prim間通信(スクリプト初級第十五回)
アニメさせよう(スクリプト初級第十三回)
同じカテゴリー(初級スクリプト)の記事
 衝突判定(スクリプト初級第二十三回) (2007-05-08 12:15)
 カメラ制御(スクリプト初級第二十二回) (2007-05-07 14:36)
 デモ商品を作ろう(スクリプト初級第二十一回) (2007-05-02 12:15)
 センサーを使おう(スクリプト初級第二十回) (2007-05-01 12:15)
 ステートのこと(スクリプト初級第十九回) (2007-04-27 12:15)
 rez!(スクリプト初級第十八回) (2007-04-26 12:15)
この記事へのコメント
Notecardの読み取りは未知の分野だっただけに(ってか未知の分野のほうが圧倒的に多いわけですが(笑)非常に助かります。またMakapuに遊びにいきますですよ!
Posted by のてす at 2007年04月18日 14:34
>のてすさん

使わなくてもなんとかなっちゃうノートカードですが、使えると便利なものであるのは間違いないですね(^^
私ものてすさんにはいろいろと教えて欲しいことが(ぇ
ぜひまた遊びにいらして下さい~。
Posted by Miz at 2007年04月18日 14:54
こんにちは、TJ teatime こと Jitohです。表題の内容とは直接関係ないのですが、ここにつなげさせて頂くことをお許しください。

 えと、以下の件、私の blog への書き込みありがとうこざしました。

http://jitoh.seesaa.net/article/38412546.html#comment

 あわてていないので、ゆっくりで結構ですのでよろしくお願いしますっ。

 それと一つ質問が... 自分で作った音源を10秒で分割してULし、ギターに吸い込ませたんですが、どうも別々の曲として扱われてしまって、連続再生できません。
 ファイル名の付け方に、なにかコツがあるのでしょうか? 教えて頂けたら助かります(__)
Posted by Jitoh at 2007年04月18日 15:48
>Jitohさん

こんにちは(^^
実はテレキャスターのほうは完成しています。
Tipjarも一応作ってありますが、ちょっと完成度がイタタタ・・・なのでどうしようかと悩んでいるところです。
帽子には見えますが、カッコいいテンガロンハットかと言われるとちょっと・・・(^^;

サウンドの連続再生に関しては
http://miz.slmame.com/e1401.html
このエントリに書いてありますので参考にしてみて下さい。
Posted by Miz at 2007年04月18日 16:04
ノートカードの読み取りって本当に色々応用がききそうですねー。
データベースとして使う訳ですね。
あ、ビジュアルノベルっぽいのとかも出来そうダw

今回も勉強になりましたっ。
ありがとうございます。
Posted by Nitaro at 2007年04月18日 16:06
>Nitaroさん

>データベースとして使う訳ですね。
まさにそうです。
ただし読取専用ではありますが(^^;

>あ、ビジュアルノベルっぽいのとかも出来そうダw

本文には書きませんでしたが、ノートカードには少々制約があります。
まず一行の読み取れる最大文字数は255文字まで。

それから、日本語に関しては読み取った時点で化けてくれます。
http://miz.slmame.com/e1486.html
↑ここに書いた方法を使えばノートカードから日本語(URLエンコードされた文字)を読み取ることは可能ですが、URLエンコードした時点で文字数は膨れ上がりますので、255文字の制限を簡単に越えてくれます。

かゆいところにまだまだ手の届かないlslではあります・・・(^^;
Posted by Miz at 2007年04月18日 16:24
Mizさん!

 素早いご返答ありがとうです! ええ、テンガロンハットの件は無視してください!

 ファイル名についてはOKです! ありがとうございました!!!
Posted by Jitoh at 2007年04月18日 17:36
>Jitohさん

週末にはテレキャスター・ストラトキャスター販売予定です。
よろしくどうぞ(^^
Posted by Miz at 2007年04月19日 09:22
毎回楽しみに、ちょっとずつ消化しています。
今回もとても参考になりました、llSetPayPrice のところを前回のまま、current_id * 4 + 3 にしていたので、最初は価格が出ませんでしたが(^^;
ところで、dataserverイベント内の commodity += [chk_l] ですが、[] をはずして、chk_l としないと、Lists may not contain lists というエラーが出ました。
Posted by Munemitsu Nishi at 2007年06月21日 05:03
>Munemitsu Nishiさん

ご指摘ありがとうございます。
修正いたしました。

もともとchk_lはリスト型の変数なので、[]はいらなかったですね・・・。
Posted by Miz at 2007年06月21日 09:19
質問なんですが・・・

アイテム購入時にオブジェクト名が表示されないんですが
どこを触ればいいんでしょうか?
Posted by カバチ at 2007年12月17日 18:10
自己解決しましたw

set_commodity(){
 llSetObjectName(llList2String(commodity, current_id * 4));
  :
  :
 (以下略)
}

ですねw
Posted by カバチ at 2007年12月17日 18:44
 
<ご注意>
書き込まれた内容は公開され、ブログの持ち主だけが削除できます。