Adsense

2008年8月12日火曜日

llDialog 再び

造形のセンスまったくなしなので、クールな HUD を作ってスクリプトを埋めこむ、、、なんてことができないので、どうしても llDialog を使うことになってしまいます。

llDialog で表示するダイアログには12個のボタンしか表示できない、ボタン内に表示できる文字数に制限がある、同じ場所から出てくるので「複数ダイアログ表示」できないことはないのですが、あまり使い勝手が良くないなど、制約が多いのですが、、、これを使うしかないわけで(笑

そこそこ多くのダイアログを作ってみて、気がついた点などを再度まとめてみようと思いました。

1) 複数の人がダイアログを押して、それを処理するような状況になるべくしないほうがいいかも

用途にもよりますが、複数の人がダイアログを押すような状況を仮定してスクリプトを組まないほうが懸命のようです。できないことはないのですが、if 文を駆使して、そのロジックをいろいろと悩むよりも、「前の人の処理が終わるまで待ってもらう」としたほうが当然のことながらスクリプトがシンプルになります。

[追記] ダンス玉(つまりアニメーション操作)など Permission を必要とするものなどは、touch されたら、アバターの Key を他のスクリプトに渡し、そちらで処理する方法があります。この場合は、複数の人が1つのプリムをタッチしても、個別に処理するスクリプトの数の最大に達するまで処理をさせることが可能になります。1つの Prim には複数のスクリプトを入れることが可能なので、touch を管理するスクリプトを親として、詳細の処理を子スクリプトに渡す、、、ということができます。

2) state を使ったほうがラクかも

これも人によりけりでしょうが、私の場合は、タッチされたら(もしくは、なんらかのイベントが発生してダイアログを呼ぶようなことになったら)、そのアバターの key を拾って state を変える方法を使うことが多くなりました。メリットは llListenRemove を使わなくても state が変わることによって listeners を溜めなくてすむこと(64 だったかな?、Remove せずにそのくらい溜まるとエラーになります)、他の人が touch したときの処理をシンプルにできること、だと思っています。また、state_entry, state_exit を使った初期化処理/終了処理が可能でスクリプトが見やすくなることもあげられます。デメリットは必ず llSetTimerEvent を使ってタイムアウトを検知させること。これをやらないでダイアログの [無視] ボタンを押されたら元の state に戻って来れません。
(以下は可読性を高めるためにグローバル変数をあまり使ってません)

key toucher = NULL_KEY;
list buttons = ["button1", "button2", "button3"];

default {
    state_entry() {
       toucher = NULL_KEY;
    }
    touch_start(integer detected_num) {
       toucher = llDetectedKey(0); //一番最初にタッチいた Avatar の Key を取得
       state dialog; //ダイアログ・ステートに移動
    }
}

state dialog {
    state_entry() {
       integer hDialog = llListen(-5555, "", toucher, ""); //-5555チャンネルで、タッチした Avatar の声だけ聞く設定
       llListenControl(hDialog, TRUE); //リスナーを追加(要は聞き始める、ということ)
       llDialog(toucher, "好きなボタンを押してください。", buttons, -5555); //ダイアログを表示。返信は -5555 チャンネル
       llSetTimerEvent(30.0); //30秒でタイムアウトにさせるため
    }
    touch_start(integer detected_num) { //ダイアログ処理中にタッチした Avatar への処理
       integer i = 0;
       for(; i<detected_num; i++) { //タッチした人分の処理をする for ループ
           llInstantMessage(llDetectedKey(i), "ダイアログ使用中です。しばらくまってください。");
       }
    }
    timer() {
        llInstantMessage(toucher, "タイムアウトになりました。");
        llSetTimerEvent(0.0);
        state default;
    }
   listen(integer channel, string name, key id, string message) {
        if (llListFindList(buttons, [message]) != -1) { //一応のチェック処理。この程度なら必要ないですが・・・
            llSetTimerEvent(0.0);
            if (message == "button1") {
                xxxxxxxx;
            } else if (message == "button2") {
                yyyyyyyy;
            } else if (message == "button3") {
                zzzzzzzz;
            }
        state default;
        }
   }
   state_exit() {
        toucher = NULL_KEY;
        llSetTimerEvent(0.0); //しつこいですが、、、過去に timer が戻らなかったことがあったので・・・
   }
}

3) ダイナミックにボタンを変えてみましょう

「前ページ」「次ページ」ボタンで複数ダイアログを使いこなすのもおもしろいですが、操作する側にとっては望まない複数ダイアログは結構迷惑、、、だったりします。
たとえば、ダンスを踊る、止める、みたいな場合、最初のころ [踊る] [止める] のボタンを用意していましたが、よくよく考えると踊っている時は [踊る] ボタンを押すわけもなく、だったら [踊る] のボタンを [止める] にすればいいわけです。このため、状態を保持するフラグを使用しなくてはなりませんが、ボタン使用の節約には効果大です。
この場合は、フラグを元にボタンのリストに対して llListFindList を使い、該当する List 要素の順番をとり、llListReplaceList で置き換える、ということをやります。
意外に、、、この手の「対称な意味を持つ」ボタンって、、、多かったりします。
以下の複数ページダイアログの例の「ノートカードが変更されたら Close ボタンを RESET ボタンに変更する」などもこの考えでボタンを変更しています。

4) 複数ページダイアログの例

以前にご紹介している ネトラジ・チェンジャーは複数ページダイアログでネトラジ局をボタンにしています。その方法をご紹介します。
以下のスクリプトの前段階処理で、ノートカードからネトラジ局の情報を読み込んで contentList という List に入れています。
ダイアログの一番下の段は [< Prev] [Close] [Next>] という3つのボタンがくるので、1つのダイアログには9つのラジオ局のボタンが表示できます。その9つの部分を変えていくわけですね。
現在のページ数を currentPage として、0ページ目(そう、この世界ってはじめはゼロのほうが都合がいいです)の場合は 0 番から8番まで、1ページ目の場合は 9番から17番、、、、としていきます。
最後のページは List の項目数を 9 で割った商になります。たとえば0ページから「前ページ」を押されたら、最大ページに移る、、、みたいなこともあらかじめ最後のページ数をとっておいて、もし、「前ページ」がマイナスになるのであれば、最大ページをいれる、次ページが最大ページを超えるようであれば、0ページを入れる、とすれば、複数ページのラウンドロビンが可能ですよね。
以下は NRC のソースの default ステートを除いたそのものです。オレンジにした部分がページとボタン操作になります。
state wait {
    state_entry()
    {
        llMessageLinked(LINK_THIS,myNumber,"wait",gToucher);
        currentPage = 0;
        buttonList = [];
        gToucher = "";
        integer handle = llListen(gChan,"","","");
        llListenControl(handle,TRUE);
    }
    link_message(integer sender_number, integer number, string message, key id)
    {
        if (message == "start") {
            if (number == myNumber) {
                gToucher = id;
                currentPage = 0;
                state active;
            }
        }
       
        if (message == "contentChanged")
        { // ノートカードが変更されたら Close ボタンを RESET ボタンに変更する
            fixedList = llListReplaceList(fixedList,["RESET"],llListFindList(fixedList,["Close"]),llListFindList(fixedList,["Close"]));
        }
    }
    listen(integer channel, string name, key id, string message)
    {
        if (llStringTrim(llToUpper(message),STRING_TRIM)=="HIDE TEXT") {
            llMessageLinked(LINK_THIS,myNumber,"HIDE TEXT","");
        } else if (llStringTrim(llToUpper(message),STRING_TRIM)=="SHOW TEXT") {
            llMessageLinked(LINK_THIS,myNumber,"SHOW TEXT","");
        }
    }      
}
state active {
    state_entry()
    {
        integer handle = llListen(gChan,"",gToucher,"");
        llListenControl(handle,TRUE);
        buttonList = fixedList + llList2List(contentList,currentPage*9,(currentPage+1)*9-1);
        gDialogMsg2 = "登録数 "+(string)gCount+", "+"ダイアログチャンネル: "+(string)gChan;
        gDialogMsg3 = "オブジェクト上の文字を消したい場合は"+"\n"+"/"+(string)gChan+" hide text をダイアログを閉じてから行ってください。";
        dMsg = gDialogMsg+"("+(string)(currentPage+1)+"/"+(string)(maxPage+1)+")"+"\n"+gDialogMsg2+"\n"+gDialogMsg3;
        llDialog(gToucher,dMsg,buttonList,gChan);
        llSetTimerEvent(0.0);
        llSetTimerEvent(gInterval);
    }
    state_exit()
    {
        llSetTimerEvent(0.);
    }
    timer()
    {
        if (llKey2Name(gToucher) != "") {
            llInstantMessage(gToucher,gMsgTimeout);
        }
        state wait;
    }
    listen(integer channel, string name, key id, string message)
    {
        if (llListFindList(fixedList,[message])!=-1) {
            buttonList = [];
            if (message == "Next >"){
                if (currentPage+1 > maxPage) currentPage=0;
                else currentPage = currentPage+1;
            } else if (message == "< Prev") {
                    if (currentPage-1 < 0) currentPage=maxPage;
                else currentPage = currentPage-1;
            } else if (message == "Close") {
                state wait;
            } else if (message = "RESET") {
                llResetOtherScript("dialogMgr");
                state wait;
            }
           
            buttonList = fixedList + llList2List(contentList,currentPage*9,(currentPage+1)*9-1);
            gDialogMsg2 = "登録曲数 "+(string)gCount+", "+"ダイアログチャンネル: "+(string)gChan;
            dMsg = gDialogMsg+"("+(string)(currentPage+1)+"/"+(string)(maxPage+1)+")"+"\n"+gDialogMsg2+"\n"+gDialogMsg3;
            llDialog(gToucher,dMsg,buttonList,gChan);
            llSetTimerEvent(0.0);
            llSetTimerEvent(gInterval);
        } else if (llListFindList(contentList,[message])!=-1) {
            llSetParcelMusicURL(llList2String(urlList,llListFindList(contentList,[message])));
            llSetText(message,<1,1,1>,0.8);
            state wait;
        }
    }
単純といえば単純ですが、、、なんらかのご参考になれば幸いです。

[追記]
最初にものすごく悩んで、今は普通にやっていることを書き忘れました。
llDialog が使用するチャンネルについてです。

5) llDialog が利用するチャンネルは決めうちにしないほうが良い場合が多い

ダンスボールや、音楽玉、ビデオ玉を作り始めのころに、ダイアログの混線の問題がありました。
たとえば、自分の作ったダンスボールを複数 Rez したとき、llDialog が利用するチャンネルを決めうちにしている場合は、それら Rez したダンス玉を交互に操作するとダイアログ・チャンネルの混線が発生します。
llDialog って、結局は llSay でいうことを代わりにやってくれているようなものなので、それぞれのダイアログの返答がどのダイアログからきているかを判定するもの(値)がないんです。
よって、それぞれのダイアログを判定する方法は、llDialog で使用するチャンネルを変える事になります。
私は llFrand を使って、ランダムにダイアログのためのチャンネルを Rez や初期化時に生成して利用するようになりました。
絶対同じものにならない、、、というわけではありませんが、同じオブジェクトを複数 Rez しても、それぞれのダイアログが利用するチャンネルが違う確率が高いので、この方法をいつも無意識に使うようになりました。
以下のようなユーザー関数を使って、ランダムに生成されたチャンネルを使います。
これは 1000 から 9999 までのランダムな数字を作る例です。

integer randChan() {
    return 1000 + (integer)llFrand(8999);
}

llFrand(8999) によって、0 から 8999 までの間の数字がつくられます。 0 が作られたら、それに 1000 を足したもの、つまり 1000 が返され、8999 だったらそれ1000を足した 9999 が返されます。
もちろん、これをマイナスの値にしてあげれば、さらに良くなりますね。