LSLを使ってセカンドライフで湯婆婆 その１

### はじめに
こちらは、[Qiita湯婆婆アドベントカレンダー](https://qiita.com/advent-calendar/2020/yubaba)および[セカンドライフ技術系アドベントカレンダー](https://qiita.com/advent-calendar/2020/yubaba)の個別記事の１つめになります。仮想空間SecondLifeでは空間内のオブジェクト内部にスクリプトを仕込むことでオブジェクトを動かすことができるので、それを使って湯婆婆を実装しようという試みです。

SLユーザー向け前書きはこちら
[電脳九龍飯店 SL支店:セカンドライフ技術系Advent Calendar 2020](http://kowloon.tec29.com/preview)

Qiitaユーザー向け前書きはこちら
[LSLを使ってセカンドライフで湯婆婆 \- Qiita](https://qiita.com/regicat/items/ab89ebed688704e3f712)

動作としてはこんなイメージです。

1. 湯婆婆をクリックすると「契約書だよ。そこに名前を書きな。」としゃべる
2. チャット欄に名前を入力すると、名前を奪い、そこから一文字だけ返してくる

### (1) 湯婆婆オブジェクトの作成
セカンドライフのスクリプトは単体では動作せず、必ず仮想空間内のオブジェクト内に格納する必要がありますので、まずはオブジェクトを作成します。なんか探せばメッシュでモデリングした湯婆婆とかどこかで配布されてそうな気もするのですが、著作権的にアレがナニしてる可能性もあるので、ここは順当に、板にジブリ提供の公式画像を貼って作りました。オブジェクトそのものの制作手順は本筋ではないので割愛。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57680/28f4a31a-f633-0086-f08d-1894ea689c31.png)

オブジェクトを編集状態にして「内容」タブを選択し、「新規スクリプト」ボタンを押すと、スクリプトファイルがオブジェクト内に生成されるので、そこにLSL言語でスクリプトを書いていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57680/4485a4e3-8784-8951-f469-87604463a88c.png)

生成されたスクリプトは最初からサンプルコードを含んでいます。その内容がこちら。

```c++:default.lsl
default
{
    state_entry()
    {
        llSay(0, "Hello, Avatar!");
    }

    touch_start(integer total_number)
    {
        llSay(0, "Touched.");
    }
}
```
スクリプトはイベント駆動型で、スクリプト起動時、クライアントからクリックされた時、チャットに発言があった時などさまざまなタイミングでイベントが走ります。このコードのstate_entryやtouch_startなどがそのイベントに紐づけられたイベントプロシージャです。今回は湯婆婆の顔のパネルをクリックすると処理が始まるようにしましょう。touch_startがクリック時のプロシージャなのでこちらを残し、state_entry（起動時に走ります）のほうは消しておきましょう。
一番外側の default{} は、スクリプトのステートを定義しています。ステートを切り替えると同じイベントでもステートごとに異なった動作をさせることができますが、今回はdefaultステートだけ使いますので、イベントプロシージャの外側を大きく囲っています。
### (2) 台詞を喋らせる：llSay
サンプル内にも書かれている「llSay」がオブジェクトに発言させるためのビルトイン関数になります。LSLのビルトイン関数はll（小文字のL2つ）から始まる決まりになっています。
[llSay \- Second Life Wiki](http://wiki.secondlife.com/wiki/LlSay/ja)
リンク先にある通り、引数の1つ目の整数が発言チャンネル、2つ目の文字列が喋らせる内容です。発言チャンネルはここではとりあえず、0を指定しておけばオープンチャットに発言が流れると理解しておけばOKです。（チャンネルを0以外にするとユーザーには見えなくなるので、オブジェクト同士の通信に使ったりします）

```C++
llSay(0, "契約書だよ。そこに名前を書きな。");
```
### (3) クリックしたユーザーのIDを取得する：llDetectedKey
touch_startプロシージャの中で、llDetected～から始まる関数を実行すると、クリックイベントに関する各種情報を取得することができます。llDetectedKeyを実行すると、クリックしたユーザのID（ログインIDではなく、全ユーザに割り振られたユニークなUUID）を取得できます。クリックした人だけが契約できるように、このIDを覚えておきましょう。
[llDetectedKey \- Second Life Wiki](http://wiki.secondlife.com/wiki/LlDetectedKey/ja)

```C++
key user;
user = llDetectedKey(0);
```
llDetectedKeyの帰り値はkey型です。key型はUUID形式の値を格納することに特化した文字列型で、UUIDを格納することを前提としつつ、文字列型と同様の扱いをすることができます。
llDetectedKeyの引数は、イベントで検出した対象のインデックスです。llDetected関数はクリック以外にも様々なイベントプロシージャで使われ、その場合は複数のユーザーやオブジェクトがDetectedの対象になったりするため、引き渡したインデックスで「何人目の対象」を指定することができます。実はクリックイベントでも複数人が同時クリックした場合はtouch_startの引数にその人数が入ってきているので、人数分ループさせて同時クリックした人全員にDetectedを実行することもできるのですが、それはかなりのレアケースとなるため、普通は人数の引数は無視して、llDetectedKeyの引数も0固定とすることが多いです。
### (4) ユーザーの発言を聞く：llListen
ユーザーが入力した名前を取得するには、少し準備が必要です。具体的には、発言を待ち受ける「リスナー」を生成し、リスナーが発言を聞き取るとまたイベントが発生する、という流れになります。
リスナーを生成するのはllListen関数です。
[llListen \- Second Life Wiki](http://wiki.secondlife.com/wiki/LlListen/ja)

```C++
integer lh;
lh = llListen(PUBLIC_CHANNEL, "", user, "");
```
llListenは実行すると、ListenerHandleと呼ばれる整数値を返してきます。リスナーごとに独自の数値が割り振られるので、これを変数に格納して、後で操作に使えるようにしておきます。
llListenの引数はすべて、イベントが発動する条件をフィルタするための指定です。以下で１つづつ説明していきます。
PUBLIC_CHANNELは定義済み定数で、0を表しています。0チャンネルに発言があった場合のみイベントが発動します。
2番目は発言者の名前です。ここで指定した名前のユーザやオブジェクトが発言した場合のみイベントが発動します。空文字列を指定すると、名前によるフィルタは行いません。
3番目は発言者のIDです。(3)でllDetectedKeyによって取得したクリック者のIDを格納した変数（user）を指定しているので、クリックした人が発言した場合のみイベント発動します。ここも空にするとIDによるフィルタは行いません。
4番目は発言の内容です。指定すると、ここで指定した内容と同じ内容が発言された場合のみイベントが発動します。空文字列を指定すると、発言内容によるフィルタは行いません。
### (5)listenイベントプロシージャ
リスナーが発言を検知した際に起動するイベントプロシージャはサンプルに含まれていないので、追加で書く必要があります。発言検知時に発動するイベントプロシージャはlistenです。
[listen \- Second Life Wiki](http://wiki.secondlife.com/wiki/Listen/ja)

```C++
listen(integer channel, string name, key id, string message)
{
    //イベント発生時の処理
}
```
listenの引数には、発言に関する様々な情報が格納されて引き渡されます。channelは発言先のチャンネル、nameは発言者の名前、idは発言者のUUID、messageは発言内容です。
……名前？
そうなのです。実は湯婆婆はこの時点で、相手の名前などお見通しなのです。でもせっかく契約書を出しましたからね。ユーザーが名乗った名前を奪うことにしましょう。名乗った名前＝発言内容なので、ここではmessageに格納されています。

### (6)文字列操作
文字列操作の考え方は、他の一般的な言語と大きく変わることはありません。まず文字列全体の長さを求め、その範囲内でランダムにインデックスを指定して1文字取り出します。文字列の長さはllStringLength、文字列取り出しはllGetSubString、乱数はllFrandです。
[llStringLength \- Second Life Wiki](http://wiki.secondlife.com/wiki/LlStringLength/ja)
[llGetSubString \- Second Life Wiki](http://wiki.secondlife.com/wiki/LlGetSubString/ja)
[llFrand \- Second Life Wiki](http://wiki.secondlife.com/wiki/LlFrand/ja)

```C++
integer len = llStringLength(name);
integer idx = (integer)llFrand((float)len);
string newName  = llGetSubString(name, idx, idx);
```
llFrandは0から引数で指定したfloat値までの乱数をfloatで返してくるので、文字列長をfloatにキャストして渡し、結果をintegerにキャストして（小数点以下は切り捨てられます）取り出しのインデックス値とします。llGetSubStringはstartとendを指定する形式なので、両方に同じ値を設定すれば1文字取り出せます。

これで必要な要素は出そろいました。コードの全体は下のようになります。

```C++:lslYubaba.lsl
integer lh;
key user;
default
{
    touch_start(integer total_number)
    {
        llSay(0, "契約書だよ。そこに名前を書きな。");
        
        user = llDetectedKey(0);
        
        lh = llListen(PUBLIC_CHANNEL, "", user, "");
    }
    listen(integer channel, string avatarName, key id, string name)
    {
        llSay(PUBLIC_CHANNEL, "フン。" + name + "というのかい。贅沢な名だねぇ。");
        integer len = llStringLength(name);
        integer idx = (integer)llFrand((float)len);
        string newName  = llGetSubString(name, idx, idx);
        llSay(PUBLIC_CHANNEL, "今からお前の名前は" 
                + newName + "だ。いいかい、" + newName + 
                + "だよ。分かったら返事をするんだ、" + newName + "!!");
        llListenRemove(lh);
    }
}
```
説明ではリスナーハンドルやユーザIDの変数は使う行の直前に書きましたが、実際にはプロシージャ間で取り廻す必要があるため、ステート定義の外で定義してグローバル変数としています。listenの引数の名前は、説明中の書き方だとnameに発言した名前が入らずmessageに入るなど紛らわしいため、実際のアバター名が自動で入る変数はavatarNameとし、ユーザーの名乗った名前が入る変数をnameに変更しました。
最後の行のllListenRemoveは、リスナーの削除です。リスナーを放置しておくと、クリックした人がしゃべるたびに反応してしまうので、引数で指定したリスナーハンドルを持つリスナーを削除して、それ以上反応しないようにします。

動作例動画アップしました。

[LSLを使ってセカンドライフで湯婆婆 \- YouTube](https://www.youtube.com/watch?v=3sipeQkCURY&feature=youtu.be)

### クラッシュ湯婆婆
これでセカンドライフに湯婆婆が実装できました。しかしこの実装では一番大事な仕様が抜けています。そう、名前が空だとクラッシュする、アレです。listenイベントは発言があるまで発動しないので、「名前を空で入力」することができないのです！これはいけません。
ということで、その対応は次回に。