Yes Second Life

セカンドライフ向けWebサービスを作ってました。このブログではVR・メタバースのことを書いていきます

セカンドライフの魅力

これはセカンドライフ非技術系アドベントカレンダー向けの記事です。

セカンドライフ非技術系 Advent Calendar 2014 - Adventar
ぼくにとっての、セカンドライフの魅力を語ります。

ルールがないこと

ふつうのMMOだと、クエストが用意されていたり、スキルシステムがあったりして、決められた手順を追ってゲームを進めます。しかし、セカンドライフには決められたことがなにもないので、自分でなにかを創りださないとなにも始まりません。それが逆に、なにか作れる人にとっては魅力になります。

まだ何もない世界で、自分が培ってきた技術力をつかって、何かを生み出していく、それがこの世界そのものを拡張していく、そういうエキサイティングな面白さがあります。

ルールを作り出せること

しかし、ルールがないと遊べないという人がいることも事実。というかそういう人のほうが多数派です。だったら何か自分たちでルールを作り出せばいい。セカンドライフには、ユーザ自身がルールを作っていけるほどの自由があります。
QuestMeister
これは、ぼくが作ったクエストマイスターというサイトです。このサイトで情報を入力して、セカンドライフ上でオブジェクトを配置すれば、誰でもクエストを作ることができます。自由すぎて何をしていいかわからない世界に、一定のルールを作りだせました。セカンドライフAPI、LSL言語がそれを可能にします。

仮想世界は今でこそ自由ですが、やがては色んなサービスがこうした枠組みを作っていき、最終的には手順を追って物事を進めていけるような世界になっていくとおもいます。まさになにもなかったWebで、掲示板、ブログ、動画サイトなんかが、フォーマットを与えていったように。

まだそうなっていない今、こうした枠組みをあーだこーだ考えて実装していくことが、高度な知的ゲームのようで自分には魅力的なのです。

子プリムに名前でアクセスする

2014年、セカンドライフ技術系アドベントカレンダーの1発めの記事です。

セカンドライフのオブジェクトは、いくつかのプリムをリンクさせて作成するわけですが、リンクされた子プリムには基本的に数値インデックスでしかアクセスできません。

llMessageLinked(2, 0, llGetScriptName(), "");    // 第1引数の「2」がインデックス

llSetLinkPrimitiveParams(3, [PRIM_FULLBRIGHT, ALL_SIDES, FALSE]);    // 第1引数の「3」がインデックス

しかし、子プリムの番号はリンクをやり直したりすると変わってしまいます。これではそのたびに、スクリプトの数値インデックスの変更が必要で不便。できれば子プリムの名前を指定してアクセスしたいところです。そこで、最初に名前とインデックスの索引をつくってしまう方法が楽です。

list linkPrims;    // 索引格納用リスト


// 索引を作成
createIndex()
{
    integer i;
    linkPrims = ["root"];    // インデックス0はルートプリムを表す
    for(i=1; i<=llGetNumberOfPrims(); i++)
    {
        // インデックス順に子プリムの名前をlinkPrimsへ格納
        string linkPrim = llGetLinkName(i);
        linkPrims += [linkPrim];
    }
}

// 名前をインデックスに変換
integer getIndex(string name)
{
    return llListFindList(linkPrims, [name]);   
}


default
{
    state_entry()
    {
        createIndex();
    }

    touch_start(integer num)
    {
    // 子プリムに名前でアクセス
        llSetLinkColor(getIndex("ResetButton"), <1.0, 0.0, 0.0>, ALL_SIDES);
    }

    // リンク変更時にリセット(いちごさんの指摘で追加)
    changed(integer change)
    { 
        if(change & CHANGED_LINK)
        {
            llResetScript(); 
        }
    }
}

この方法だと、機能追加でHUDにボタンを追加したりしても、子プリムの名前を変えないかぎりスクリプトのインデックスは変更不要でメンテも楽ですね( ̄∇  ̄ )

FacebookのOculus買収と、仮想世界への本気度

FacebookがOculusを買収して、けっこうなニュースになってるけど、「Facebookが資金をもてあまして流行りものに手を出したんじゃ」みたいな批判を目にしたので、FacebookのVRへの本気度について知ってる範囲で書いておきます。

2007年からのセカンドライファーならおそらく誰でも知っているCory Ondrjkaさんという開発者がいます。セカンドライフスクリプト言語、LSLなどを開発した方で、VR開発者の中でも非常にカリスマ性がある人です。Coryさんは、VRバブルにのって拡大するリンデンラボと反りが合わず、CEOのフィリップと口論してやめることになったのですが、2010年からFacebookで働いています。

そして、Facebookアプリでもある独自の仮想世界CloudPartyを、2012年6月に立ち上げました。OculusRiftの発表も2012年の6月でしたが、当然CloudPartyの開発自体はそれ以前から始められていたはずで、つまりFacebookはOculusがでてくる前から、VR技術には注目していたことになります。

なので、資金を持て余したとか、流行りモノだからという理由じゃなくて、いま自分たちの取り組んでいるVRに必要だと感じて、Oculusを買収したというのが自然な流れだとおもいます。

Facebookの本気度については、こういう理由で自分は疑ってないんですが、批判はもうひとつあります。いままでユーザが育ててきてオープンだったOculusが、Facebookによって不自由なものにされてしまうんじゃないかというものです。

こちらに関しては、正直よく分かりません。ぼく自身Facebookのクローズさがイヤで、アカウントだけ作ってあるものの全く使ってない状態です。ただ、FacebookのVR戦略にまず関わってるであろうCoryさんの性格上、あまりクローズにはならないのではないかと自分は楽観視しています。Coryさんはセカンドライフでユーザが作ったものがリンデンラボでなくユーザ自身に著作権が属するように積極的に働きかけました。そういったオープンな姿勢をみているので、変な囲い込みをするのが自分にはなかなか想像できません。

まとめると、FacebookのVRへの取り組みは本気であり、目的意識なくOculusを買収したわけではない。Facebookに買収されてクローズなかんじになるかは分からないが自分は楽観視しているというかんじです。

アニメーションオーバーライドできる新関数

セカンドライフアバターのデフォルトの歩いたり座ったりするアニメーションは、今までは基本的に上書きするのにハックが必要でした。タイマーをコンマ数秒単位で回して、アバターの状態を監視する方法です。しかしこれはシムへの負荷がかなり掛かるものでした。

そこで最近、負荷を抑えつつアニメーションを上書きできる新関数が追加されました。

llSetAnimationOverride

llSetAnimationOverride( string anim_state, string anim )

llSetAnimationOverrideは、アニメーションを上書きします。第1引数は「上書きする状態」、第2引数は上書きするアニメーションの名前です。例えば、歩く動作をしているとき座るアニメーションを再生したい場合は、次のようにします。

default
{
    touch_start(integer num)
    {
        // パーミッションを取得
        llRequestPermissions(llDetectedKey(0), PERMISSION_OVERRIDE_ANIMATIONS);
    }
    
    run_time_permissions(integer perm)
    {
        if(perm & PERMISSION_OVERRIDE_ANIMATIONS)
        {
            // 歩く動作のとき、座るアニメーションをさせる
            llSetAnimationOverride("Walking", "sit");
        }
    } 
}

上書きできる状態は、これだけあります。

  • Crouching
  • CrouchWalking
  • Falling Down
  • Flying
  • FlyingSlow
  • Hovering
  • Hovering Down
  • Hovering Up
  • Jumping
  • Landing
  • PreJumping
  • Running
  • Sitting
  • Sitting on Ground
  • Standing
  • Standing Up
  • Striding
  • Soft Landing
  • Taking Off
  • Turning Left
  • Turning Right
  • Walking

アニメーションは、基本的にはプリムに格納しておいて、その名前を第2引数で指定するのですが、組み込みアニメーションであれば、プリムになくても使用できます。上記のサンプルも組み込みアニメーションを使いました。

llGetAnimationOverride

llGetAnimationOverride( string anim_state )

llGetAnimationOverrideは、いま再生しているアニメーションの名前を取得できます。引数には、アニメーションの名前を取得したい状態を指定します。歩いているときは、普段は「walk」というアニメーションが再生されていますが、さっきのllSetAnimationOverrideのサンプルを実行したあとで以下のスクリプトを動かすと、「sit」と表示されます。

default
{
    touch_start(integer num)
    {
        // パーミッションを取得
        llRequestPermissions(llDetectedKey(0), PERMISSION_OVERRIDE_ANIMATIONS);
    }
    
    run_time_permissions(integer perm)
    {
        if(perm & PERMISSION_OVERRIDE_ANIMATIONS)
        {
            // 歩いているときに再生されているアニメーションの名前を取得
            llOwnerSay(llGetAnimationOverride("Walking"));
        }
    }
}

llResetAnimationOverride

llResetAnimationOverride( string anim_state )

現在指定されているアニメーションを、デフォルトのものにリセットします。引数にはアニメーションをリセットしたい状態を指定します。llSetAnimationOverrideのサンプルを実行したあとで、次のスクリプトを動かすと、歩くときのアニメーションが「walk」にもどります。

default
{
    touch_start(integer num)
    {
        // パーミッションを取得
        llRequestPermissions(llDetectedKey(0), PERMISSION_OVERRIDE_ANIMATIONS);
    }
    
    run_time_permissions(integer perm)
    {
        if(perm & PERMISSION_OVERRIDE_ANIMATIONS)
        {
            // 歩いているときに再生されているアニメーションをデフォルトにもどす
            llResetAnimationOverride("Walking");
        }
    }
}

なお、llResetAnimationOverrideだけ引数に「ALL」が指定でき、すべてのアニメーションをリセットすることができます。

自分で何かつくるときはもちろん、既存のアニメーションオーバーライドツールを使う時も、できれば新しい関数を使って負荷をへらしているかチェックしてみるといいかもしれませんね( ̄∇  ̄ )

透視投影変換を使ってロックオンできるHUDをつくる

ロックオンできるHUDといっても、よくわからないと思うので、まずは動画をみてください。

赤い箱は土地にRezされたオブジェクトで、青い四角はHUDです。赤い箱にタッチすると箱が自分の現在の位置をシャウトするようになってて、それをHUD側がListenして箱が表示されている座標へ移動するようになってます。3Dシューティングなんかで敵の位置へカーソルが移動してロックオン状態になったりしますが、まさにそんな感じのものをつくりました。

使用している技術は透視投影変換で、これは3D上の世界を2次元の画面へマッピングするための手法です。ただ、これを説明するのは、ものすごく大変というか自分も完全には理解してないので、とりあえず赤い箱の位置を渡せばHUDをロックオンする位置に移動させる関数をいきなりお見せします。

// 引数にターゲットオブジェクトの位置を渡すと、そこにHUDをロックオンさせます
lockOn(vector pos)
{
    // Field of View レンズの画角を算出(SLのデフォルトの視野角は60度)
    float fov = 1.0 / llTan(60.0 * 0.5 * PI / 180.0);
    
    // カメラの情報を取得(要パーミッション)
    vector cameraPos = llGetCameraPos();
    rotation cameraRot = llGetCameraRot();
    
    // カメラとオブジェクトの距離を算出
    float dist = llVecDist(pos, cameraPos);
    
    // カメラとオブジェクト間をベクトルで表現
    vector objVec = pos - cameraPos;
    
    // カメラの回転を、X、Y、Zの各ベクトル表現に分解
    vector xVec = <1.0, 0.0, 0.0> * cameraRot;
    vector yVec = <0.0, 1.0, 0.0> * cameraRot;
    vector zVec = <0.0, 0.0, 1.0> * cameraRot;
    
    // オブジェクトへのベクトルとカメラの向いてる方向との角度差を得る
    rotation xRot = llRotBetween(objVec, xVec);
    rotation yRot = llRotBetween(objVec, yVec);
    rotation zRot = llRotBetween(objVec, zVec);
    
    // オブジェクトへの距離をカメラを軸としたX、Y、Z方向の距離に分解
    float xDist = dist * llCos(llRot2Angle(xRot));
    float yDist = dist * llCos(llRot2Angle(yRot));
    float zDist = dist * llCos(llRot2Angle(zRot));
    
    // 距離と視野角から、HUDの位置を算出する
    float posY = yDist / xDist * fov / 2.0;
    float posZ = zDist / xDist * fov / 2.0;
    vector pos = <0.0, posY, posZ>;
    
    // HUDを移動させる
    llSetPrimitiveParams([PRIM_POSITION, pos]);
}

ゲームプログラミングとかしてる人じゃないと分かりませんよね。3Dの世界を2次元にマッピングする場合、遠くにあるものほど真ん中へ寄せて表示しないとそれらしくならないので、そういう処理をしてると思ってください。

f:id:sabro:20131214012302p:plain
図にするとこんな感じかな。

この技術はカソウセカイカメラでも使われています。まあちょっとしたバグがあって、コメントは正確な位置から少しズレるんですが・・・。コメントを空間に表示するとき位置をどう決めればいいのかわからなくて、試行錯誤しててこの方法にたどりついた感じです。

応用範囲は広くて、3Dシューティングゲームを作ったりできますし、なにかモノを指し示すガイドとしても使えるとおもいます。特に後者は、情報が整理されておらずカオスな仮想世界で何か探してもらうときに、かなり役に立つんじゃないでしょうか。

最後に実際に動作確認したスクリプトを載せておきます( ̄∇  ̄ )

赤い箱のスクリプト

// チャット用のチャンネル(適当)
integer CHANNEL = -84527;

default
{
    touch_start(integer total_number)
    {
        // 今の位置をシャウト
        llShout(CHANNEL, (string)llGetPos());
    }
}

青い四角のスクリプト

// Secondlifeのデフォルトの視野角
float ANGLE = 60.0;

// チャット用のチャンネル(適当)
integer CHANNEL = -84527;

// 引数にターゲットオブジェクトの位置を渡すと、そこにHUDをロックオンさせます
lockOn(vector pos)
{
    // Field of View レンズの画角を算出
    float fov = 1.0 / llTan(ANGLE * 0.5 * PI / 180.0);
    
    // カメラの情報を取得
    vector cameraPos = llGetCameraPos();
    rotation cameraRot = llGetCameraRot();
    
    // カメラのとオブジェクトの距離を算出
    float dist = llVecDist(pos, cameraPos);
    
    // カメラとオブジェクト間をベクトルで表現
    vector objVec = pos - cameraPos;
    
    // カメラの回転を、X、Y、Zの各ベクトル表現に分解
    vector xVec = <1.0, 0.0, 0.0> * cameraRot;
    vector yVec = <0.0, 1.0, 0.0> * cameraRot;
    vector zVec = <0.0, 0.0, 1.0> * cameraRot;
    
    // オブジェクトへのベクトルとカメラの向いてる方向との角度差を得る
    rotation xRot = llRotBetween(objVec, xVec);
    rotation yRot = llRotBetween(objVec, yVec);
    rotation zRot = llRotBetween(objVec, zVec);
    
    // オブジェクトへの距離をカメラを軸としたX、Y、Z方向の距離に分解
    float xDist = dist * llCos(llRot2Angle(xRot));
    float yDist = dist * llCos(llRot2Angle(yRot));
    float zDist = dist * llCos(llRot2Angle(zRot));
    
    // 距離と視野角から、HUDの位置を算出する
    float posY = yDist / xDist * fov / 2.0;
    float posZ = zDist / xDist * fov / 2.0;
    vector pos = <0.0, posY, posZ>;
    
    // HUDを移動させる
    llSetPrimitiveParams([PRIM_POSITION, pos]);
}


default
{
    state_entry()
    {
        // ターゲットオブジェクトの発信する位置情報をListen
        llListen(CHANNEL, "", NULL_KEY, "");
        
        // HUDとしてアタッチされてる場合のみ、カメラのパーミッションを取得
        if(llGetAttached() >= ATTACH_HUD_CENTER_2 && llGetAttached() <= ATTACH_HUD_BOTTOM_RIGHT) 
        {
            llRequestPermissions(llGetOwner(), PERMISSION_TRACK_CAMERA);
        }
    }
    
    on_rez(integer start_param) 
    {
        llResetScript();
    }
    
    listen(integer channel, string name, key id, string message)
    {
        // サンプルなのでチェックはなしです

        // Lock on 
        lockOn((vector)message);
    }
    
    run_time_permissions(integer perm) {
        if(perm & PERMISSION_TRACK_CAMERA) {
            // do nothing
        }
    }
    
}

LSLスクリプトでJSONを扱う(一括変換編)

この記事は、セカンドライフ 技術系 Advent Calendar 2013向けに書かれた記事です。今回は、JSONとリストを一括変換するLSL関数を紹介します。

llJson2List

list llJson2List(string src)

JSON文字列を、LSLのリスト変数に変換します。引数に対象のJSONを指定し、返り値が変換したリストになります。

// リストの表示をわかりやすくするため、文字列への変換関数を用意しておきます
string dump(list l)
{
    return "[ " + llDumpList2String(l, ",") + " ]";
}


// JSONオブジェクトを変換してみます
list value1 = llJson2List("{ \"a\" : 1, \"b\" : true }");

// [ a,1,b,﷖ ]と表示されます。strided list(ひとつ飛ばしのリスト)になります
// リストの最後の項は、JSON_TRUEです
llOwnerSay(dump(value1));

// JSON配列を変換してみます
list value2 = llJson2List("[ 1, \"two\", \"3\" ]");

// [ 1,two,3 ]と表示されます。普通にリストに変換されます
// ちなみにリストの要素は全て文字列型になります
llOwnerSay(dump(value2));

// 単一の値を変換してみます
list value3 = llJson2List("1");

// [ 1 ]と表示されます。ひとつだけ要素を含んだリストになります
llOwnerSay(dump(value3));

// keyをnullにした不正なJSONを変換してみます
list value4 = llJson2List("{ \"a\" : 1, null : true }");

// [ ﷐ ]と表示されます。JSON_INVALIDだけ含んだリストです
llOwnerSay(dump(value4));

JSONオブジェクトは、strided list(ひとつ飛ばしのリスト)に変換されます。LSLにはstrided listを扱える関数がいくつかあるので活用できそうです。また、JSONをリストに変換した時、リストの要素は全て文字列型になります。これは例えば数値をfloatに勝手に変換して桁落ちしたりしないような配慮だとおもいます。

不正なJSONが入力された場合は、JSON_INVALIDのみ含んだリストが返されるので、入力値のJSONが信頼できない場合は、条件分岐でJSON_INVALIDと比較するのが良さそうです。

if(llList2String(value4, 0) != JSON_INVALID)
{
    // 本来の処理
    // ...
}

では、ネストしたJSONは正しく変換できるのでしょうか。

string json = "
{
    \"a\" : 1,
    \"b\" : [ 1, \"two\", \"3\" ],
    \"c\" : { \"d\" : 2, \"e\" : false }
}";

// ネストしたJSONを変換してみます
list value1 = llJson2List(json);

// [ a,1,b,[ 1, "two", "3" ],c,{ "d" : 2, "e" : false } ]と表示されます。
llOwnerSay(dump(value1));

JSONの内部にあるJSONは再帰的に変換されず、文字列としてリスト内に格納されます。なので、深いところにある値を取得するには、自力で何回か変換してやる必要があります。

// [ 1, \"two\", \"3\" ]の部分を取得し、変換します
list value2 = llJson2List(llList2String(value1, 3));

// [ 1,two,3 ]と表示されます。
llOwnerSay(dump(value2));

// { \"d\" : 2, \"e\" : false }の部分を取得し、変換します
list value3 = llJson2List(llList2String(value1, 5));

// [ d,2,e,﷗ ]と表示されます。最後の項はJSON_FALSEです
llOwnerSay(dump(value3));

llList2Json

string llList2Json(string type, list values)

LSLのリストを、JSON文字列に変換します。引数の1つめはJSONの種類(JSON_OBJECTか、JSON_ARRAY)、2つめは変換するリストです。返り値は変換したJSON文字列になります。

// JSONオブジェクトに変換してみます
string value1 = llList2Json(JSON_OBJECT, ["a", 1, "b", JSON_TRUE]);

// {"a":1,"b":true}と表示されます。JSON_TRUEはtrueに変換されます
llOwnerSay(value1);

// JSON配列に変換してみます
string value2 = llList2Json(JSON_ARRAY, [1, "two", "3"]);

// [1,"two","3"]と表示されます。ListからJSONへの変換では数値と文字列は区別されます
llOwnerSay(value2);

ちなみに、keyに数値を指定するなど、おかしな操作をするとJSON_INVALIDが返ります

// Keyに1を指定
string value3 = llList2Json(JSON_OBJECT, [1, 2]);

// JSONのKeyは文字列でないといけません
if(value3 == JSON_INVALID)
{
    llOwnerSay("不正な操作です");
}

では、ネストしたJSONを作ってみましょう。

/*
こんなJsonを作ってみます
{
    "a" : 1,
    "b" : [ 1, "two", "3" ],
    "c" : { "d" : 2, "e" : false }
}
*/

// 内部のJSON(配列)を、先に作っておきます
string subjson1 = llList2Json(JSON_ARRAY, [1, "two", "3"]);

// 内部のJSON(オブジェクト)を、先に作っておきます
string subjson2 = llList2Json(JSON_OBJECT, ["d", 2, "e", JSON_FALSE]);

// 最後にそれらを合成します
string json = llList2Json(JSON_OBJECT, ["a", 1, "b", subjson1, "c", subjson2]);

// {"a":1,"b":[1,"two","3"],"c":{"d":2,"e":false}}と表示されます
llOwnerSay(json);

小さい部品から先に作っていけばいいわけですね。ちなみにこういう書き方もできます。

// 一気に組みあげます
string json = llList2Json(JSON_OBJECT,
    [
     "a", 1,
     "b", llList2Json(JSON_ARRAY, [1, "two", "3"]),
     "c", llList2Json(JSON_OBJECT, ["d", 2, "e", JSON_FALSE])
    ]
);

// {"a":1,"b":[1,"two","3"],"c":{"d":2,"e":false}}と表示されます
llOwnerSay(json);

以上で、一括変換の説明も終わりです。

2回に渡ってLSLでJSONを扱う方法を書いてきました。JSONを扱えるようになることで、Webサービスとの連携がやりやすくなるはずです。例えばTwitterJSONを返すAPIがありますね(OpenAuthの認証が必要ですが)。しかし一方でなんでもJSONオブジェクトにしちゃったりすると、Keyの分だけ少ないLSLメモリを消費してしまうという罠もあります。本番運用では、そのへんも見極めて最適なデータ形式を選択していけるといいんじゃないかなとおもいます( ̄∇  ̄ )

LSLスクリプトでJSONを扱う(単要素処理編)

これは、セカンドライフ 技術系 Advent Calendar 2013向けに書かれた記事です。LSLにJSONを扱う関数が追加されたので、今回はそれを紹介します。

JSONとは

JSONはJavascriptの記法で書かれたデータなのですが、いろんなプログラミング言語にJSONを扱うためのライブラリがあるので、さまざまなプログラム間でデータをやりとりするときに使われています。

こんなかんじでオブジェクトと配列の組み合わせでJSONは構成されます。

// JSONオブジェクト
{ "a" : 1, "b" : true } 

// JSON配列
[ "c", 3, false ]

// 組み合わせ
[ "c", 3, false, { "a" : 1, "b" : true } ] 

LSLには「値を1つずつ処理する方法」「JSONをリストに一括で変換する方法」の、2通りのやり方が用意されています。一気に紹介しようと思ったのですが、意外と長くなってしまったので、今回は値を1つずつ処理する方法を紹介します。

llJsonGetValue

string llJsonGetValue(string json, list specifiers)

JSONから指定したパスにある値を文字列として取得します。第一引数に対象のJSONを指定して、第二引数のListでパスを指定します。

// JSONをリテラルで書く場合、ダブルクォーテーションはエスケープが必要です
string value1 = llJsonGetValue("{ \"a\" : 1, \"b\" : true }", ["a"]);

// 1と表示されます
llOwnerSay(value1);

// ネストした例、配列の4番目(0から始まるので)のオブジェクトの、bをKeyにもつ値を取得
string value2 = llJsonGetValue("[ \"c\", 3, false, { \"a\" : 1, \"b\" : true } ]", [3, "b"]);

// TRUEと表示してほしいところですが、﷖と表示されます
llOwnerSay(value2);

2つめの例をみるとわかるように、リストにパスとなる値を書いていくことで深い位置にある値もとってくることができます。しかし、うまくTRUEと表示されてくれません。LSLには定数として「TRUE」が用意されてるんですが、これって単に数値の1の置き換えでしかないので、実は真の意味で真偽値を表す定数ってないんですね。そこで、JSONの真偽値を表すために「JSON_TRUE」「JSON_FALSE」が追加されています。

// trueを取得します
string value1 = llJsonGetValue("{ \"a\" : 1, \"b\" : true }", ["b"]);

// このように、条件分岐を使うと人にもわかるように表示できます
if(value1 == JSON_TRUE)
{
    llOwnerSay("TRUEを取得しました");
}

定数は他にもあります。

// nullを取得します
string value1 = llJsonGetValue("{ \"a\" : 1, \"b\" : null }", ["b"]);

// nullは、JSON_NULLであらわします
if(value1 == JSON_NULL)
{
    llOwnerSay("NULLを取得しました");
}

// 存在しないKeyを指定します
string value2 = llJsonGetValue("{ \"a\" : 1, \"b\" : null }", ["c"]);

// JSON_INVALIDが返ります
if(value2 == JSON_INVALID)
{
    llOwnerSay("不正な操作です");
}

llJsonSetValue

string llJsonSetValue(string json, list specifiers, string value)

当然値をセットする関数もあります。第一引数に対象のJSONを指定して、第二引数のListでパスを指定、第三引数でセットする値を指定します。返り値は値が更新されたJSONです。

// aをKeyとする値に数値の2をセットしています
string value1 = llJsonSetValue("{ \"a\" : 1, \"b\" : true }", ["a"], "2");

// {"a":2,"b":true}と表示されます
llOwnerSay(value1);

// ネストした例、今度は文字列の2をセットしています
string value2 = llJsonSetValue("[ \"c\", 3, false, { \"a\" : 1, \"b\" : true } ]", [3, "a"], "\"2\"");

// ["c",3,false,{"a":"2","b":true}]と表示されます。「数値」と「数字の文字列」は区別されています
llOwnerSay(value2);

// aをKeyとする値にオブジェクトをセットしています
string value3 = llJsonSetValue("{ \"a\" : 1, \"b\" : true }", ["a"], "{\"c\":1,\"d\":2}");

// {"a":{"c":1,"d":2},"b":true}と表示されます
llOwnerSay(value3);

// 存在しないKeyを指定すると、キーと値のペアを追加できます
string value4 = llJsonSetValue("{ \"a\" : 1, \"b\" : true }", ["c"], "3");

// {"a":1,"b":true,"c":3}と表示されます
llOwnerSay(value4);

数値と数字の文字列が区別されたり、オブジェクトをセットできたり、けっこう自由度が高いです。また特殊な定数を使った便利な操作も用意されています。

// JSON_NULLを使って、nullをセットしています
string value1 = llJsonSetValue("{ \"a\" : 1, \"b\" : true }", ["a"], JSON_NULL);

// {"a":null,"b":true}と表示されます
llOwnerSay(value1);

// JSON_APPEND定数を使って、配列の最後に値を追加しています。要素数がわからないとき便利です
string value2 = llJsonSetValue("[ \"c\", 3, false, { \"a\" : 1, \"b\" : true } ]", [JSON_APPEND], "5");

// ["c",3,false,{ "a" : 1, "b" : true },5]と表示されます
llOwnerSay(value2);

// ネストした配列でも可能です
string value3 = llJsonSetValue("[ \"c\", 3, false, [ \"a\", \"b\"] ]", [3, JSON_APPEND], "6");

// ["c",3,false,["a","b",6]]と表示されます
llOwnerSay(value3);

// JSON_DELETE定数を使って、値を削除しています
string value4 = llJsonSetValue("{ \"a\" : 1, \"b\" : true }", ["a"], JSON_DELETE);

// {"b":true}と表示されます
llOwnerSay(value4);

// 配列でも削除できます
string value5 = llJsonSetValue("[ \"a\", \"b\"}", [0], JSON_DELETE);

// ["b"]と表示されます
llOwnerSay(value5);

llJsonValueType

string llJsonValueType(string json, list specifiers)

指定したパスにある値のタイプを取得します。第一引数に対象のJSONを指定して、第二引数のListでパスを指定します。返り値は、以下の定数のどれかです。

  • JSON_INVALID
  • JSON_OBJECT
  • JSON_ARRAY
  • JSON_NUMBER
  • JSON_STRING
  • JSON_NULL
  • JSON_TRUE
  • JSON_FALSE
string json = "[ \"c\", 3, false, { \"a\" : null, \"b\" : true } ]";

// JSON_STRING
string type1 = llJsonValueType(json, [0]);
if(type1 == JSON_STRING) llOwnerSay("string");

// JSON_NUMBER
string type2 = llJsonValueType(json, [1]);
if(type2 == JSON_NUMBER) llOwnerSay("number");

// JSON_TRUE
string type3 = llJsonValueType(json, [3, "b"]);
if(type3 == JSON_TRUE) llOwnerSay("true");

// JSON_NULL
string type4 = llJsonValueType(json, [3, "a"]);
if(type4 == JSON_NULL) llOwnerSay("null");

// JSON_OBJECT
string type5 = llJsonValueType(json, [3]);
if(type5 == JSON_OBJECT) llOwnerSay("object");

// JSON_INVALID
string type6 = llJsonValueType(json, [9]);
if(type6 == JSON_INVALID) llOwnerSay("invalid");

以上で、単要素処理編はおしまいです。しかしJSONのパスをリストで表すというのが、なんか斬新でした。次回、一括変換編は万一アドベントカレンダーが完走しなかった場合に書こうとおもいます。ま、まあきっと完走するよね・・・。