2008年8月31日日曜日

新しい関数 llGetAgentLanguage

Second Life で外人の方とのコミュニケーションは珍しくありませんよね。
ダンスクラブに来られた外人の方にダンスの開始方法を教えるのも、、、まぁ、英語ならなんとかチャットで教えてあげていました。

サーバー 1.24 でいくつかの新しい関数が追加されていますが、llGetAgentLanguage という関数は、その Avatar の Agent が使っているビューワーの言語のバージョンを取得する関数です。これを使うと、スクリプトのメモリが許せば、日本語、英語、がんばれば他の外国語のメッセージをスクリプト内に保存しておいて、Agent が利用しているビューワーの言語情報によって切り替えることが可能ですね。

MLDU(ダンス玉) でいうと DDManager みたいなダイアログの操作でストリング(文字列)とリストを多用していると、メモリの問題から1つのスクリプト内で処理するのはつらいのですが、個々のアバターのパーミッションを管理して、ダンスの開始、停止を行うスクリプトなどは、複数の言語メッセージを保管しておいて、llGetAgentLanguage の情報からメッセージを切り替えることが可能になりますね。

こんな感じです。

string msg1 = "こんにちは!";
string msg1e = "Hello!!";
    if (llGetAgentLanguage(id)=="ja"){
            llInstantMessage(id, msg1);
    } else llInstantMessage(id, msg1e);
日本語以外だったら英語、という単純なものですが、ダンスの停止方法などをビューワーの言語設定にあわせて切り替えて表示させるには重宝しますね。
あたりまえですが、引数にはアバターの Key が必要です。ですから、Detected 系の関数をつかってあらかじめ対象となるアバターの Key を取得して llGetAgentLanguage に渡すことになりますね。戻り値は ja や en-us といった文字列になります。
list を使って利用する方法もありますね。
list msgList01 = ["ja","こんにちは!!","en-us","Hello!!","fr","Salut!!!"];
default
{
    state_entry()
    {
        llSay(0, "Hello, Avatar!");
    }
    touch_start(integer total_number)
    {
        integer i = 0;
        for(; i<total_number; ++i) {
            string lang = llGetAgentLanguage(llDetectedKey(i));
            integer x = llListFindList(msgList01,[lang]);
            if(x==-1) x=2; //言語対応してない場合のデフォルトを指定します。
            llInstantMessage(llDetectedKey(i), llList2String(msgList01,x+1));
        }
    }
}
ちょっとしたメッセージだと、この方が list に言語と対応するメッセージを追加するだけでよく、 if 文を変更せずに済むので楽なのかもしれませんね。
ちょっとした小ネタでした~。

2008年8月23日土曜日

Mono がやってきます

Linden の Official Blog に「Mono Launch」という記事がついに投稿されましたね。

Linden Official Blog - Mono Launch

20日から 1.24 サーバーの展開が始まって、その 1.24 サーバーには Mono が含まれている、ということらしいです。
サーバーが 1.24 になっただけでは Mono は使えず、スクリプト作成者はクライアントビューワーの 1.21 RC が必要になるということです。1.21 RC のビューワーではスクリプトエディタの下に [Mono] というチェックボックスが追加されるので、それをチェックして保存、コンパイルで Mono 対応に移行完了、ということらしいです。逆に [Mono] にチェックをいれなければ、従来のまま動くようです。
ちなみに サーバー 1.24 のリリースノートによれば、ビューワーの 1.21 RC は 8月25日の週にリリースする予定ですね。

Mono はスクリプトを動かすための新しいエンジンで、従来のものと並存して提供されるとのことです。すべてのスクリプトを変換する必要はなく、また、LSL の書き方が変わるわけではありません。ちょっと安心ですね。

この Mono ですが、この新しい実行環境はスクリプトの「実行スピード」と「メモリー管理」が現行のスクリプト エンジンよりも強化されている点が特徴です。

この 1ヶ月ほど、ダンス玉 (次の MLDU) に機能をどんどん追加していったらメモリーがかなり厳しくなって、スクリプトを分けないと 10人以上踊れない、、、という状況になっていてのこの記事だったので、早速ベータ グリッドにいって試してみました。メモリーに関していえば、これまで 1つのスクリプトは 16KB しかメモリーを使えませんでしたが、Mono では 64 KB まで拡張されていることは知っていました。

今の環境だと、一番メモリーがくるしい DDManager (Dance Slot と Dialog 管理スクリプト) は残りのメモリーが 1.1K しかありません。これ、リストの操作で空のリストを使う方法じゃないと、エラーでスクリプトが止まる状態だったりします、、、

mono003s


ベータ グリッドにアクセスするには Preview Grid Viewers でダウンロードできる 1.21 ベータをインストールして、1.24 サーバーになっている Sandbox のリージョンにいくと Mono のテストが可能です。
早速、スクリプトエディタを開いてみると、ありました、[Mono] のチェックボックスが。
mono002s
MLDU ダンス玉のスクリプトの [Mono] をすべてチェックして、メモリーの状況を確認してみました。
mono001s
1.1 KB しか余裕がなかった DDManager は 29.5KB(!!!) までフリーメモリーが増えました。(^^)v

リストやデータを保持するための単位必要メモリーの容量が大幅に変わっていなければ、これでかなり余裕 (というか、16KB より大きいしw) ができたことになりますね。
あと、やはり実行速度も速くなっています。メモリーの状況を表す上の画面の Text の表示や、MLDU 初期化時のアニメーションチェックなど、今のものよりも速くなっていますね。
ということで、スクリプト分割大改造せずに Mono を待つことにしました。(笑

2008年8月17日日曜日

YazMania Videos Clip

NMR10~、遅ればせながら参加できました。

nmr10d

ファンの人からの「この曲が好き、なぜなら、、、」みたいなやり方って、ほんと楽しかったし。
それぞれが、それぞれの思いあるから、「へぇ~、そうなんだー」みたいなのがあって、新鮮でした。
Yaz Yaz で素人なりにビデオつくってきて良かったなぁ~、と思いました。それぞれの思いがある曲もみんな違うし、それぞれの考え方や印象もみんな違う。それって、いろいろな思いをいだかせることができる才能のある人がなせる業w

すごいなぁ、、、Yaz さんって、いろいろな人にいろいろな感情・体験を持たせているんだぁ、、、と改めて実感しました。
で。。。。

こんなん、作ってみました~。
High Quality な、、、ですでに動画を見てる人は問題なさげ~、ですがw
[追記] サービス終了にて削除しました。
これからも多くのファンを楽しませてください~w

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 が返されます。
もちろん、これをマイナスの値にしてあげれば、さらに良くなりますね。