ソラマメブログ

2008年02月19日

キャンプチェアーのスクリプト

「教えて!セカンドライフ!」の方に上がっていた質問に対するスクリプトをこちらに載せます。
http://oshiete.slmame.com/e142693.html

まずオブジェクトの作りからですが、ベンチ本体(ルートプリム)と座る部分で作ります。
スクリプトのテスト用ですので、ザックリ作ってこんな感じです。


(ザックリすぎる)

処理の大まかな流れですが、図にしてみました。


(これもザックリだなぁ)

スクリプトの中にコメントをいろいろ書いてますので、それを参照してください。
(実際に使う場合は、余計なコメントは消してもいいですよ)

まずルートプリム用のスクリプトです。
やってることはキャンプした人のキー、金額、時刻の管理です。キャンプできるか出来ないかの判定もこちらで行っています。

//==================================================
// sasa_camp_root
// 1.0.0
// 2008/02/15
// created by sasapy Beck

// このスクリプトはキャンプチェアーのスクリプト
// ルートプリムでキーと金額、時間の管理をし、各座るプリムで実際の支払いを行う
// 座るプリムは任意の数とする(何個でも可能)
// 基準時刻を設け、1日単位で累計する
// 1日の上限金額を設定し、それ以上は同じ日にキャンプできない
// 前回キャンプしてから一定時間経過しないと、同じ日にキャンプできない
// 前回座ったアバターキーを保持し、そこに登録されていれば同じ日にキャンプできない
// →その人以外座らなかったら1日経過しないとキャンプできない。いいのか?
// 基準時刻をまたいでしまった場合の処理はしていない
// →またいだ場合、前日分と当日分に分ける→面倒

// 課題:座る場所の数だけパーミッションの要求が出る(オーナーの手間なのでよしとする)

// 考察:メモリーが足りなくなるのは、どれぐらいか?
// 例えば20分で2L$、最高10L$とすると、1人の人が占有する時間は100分
// 1日で最大15人(24時間 * 60分 / 100分 = 14.4で切り上げ)
// 1人当たりの保持データは約60バイト(予想ですけど)
// 1日の座る場所1カ所分で、15人 * 60バイト = 900バイト
// 一つのスクリプトで16Kバイト(16,384バイト)が上限で、スクリプト自体や変数などの領域で2Kバイト使ったとすれば
// 14,336バイト / 900バイト = 15.92
// となるので、座る場所は最大15カ所
// 当然、時間、支払い金額、上限が変われば、可能なカ所も変わる

//--------------------------------------------------
// Define
    integer LINK_MSG_RESET = 11; // メッセージ番号
    integer LINK_MSG_REQUEST = 21;
    integer LINK_MSG_ANSWER = 31;
    integer LINK_MSG_LIMIT_NG = 32;
    integer LINK_MSG_TIME_NG = 33;
    integer LINK_MSG_CONTINUE_NG = 34;
    integer LINK_MSG_STAND = 41;

    integer JST = FALSE; // FALSE : GMTの0秒で一日の切り替わり
                                    // TRUE : JSTの0秒で一日の切り替わり
    float ONE_DAY_SEC = 86400.0; // 86400.0 sec = 24 hour
    float START_CLOCK = 0.0; // 切り替わり時間を調整したい場合設定する(負はダメ)

// 例えば切り替わり時間を日本時間の午前2時だったら、
// JST=TRUE、START_CLOCK=7200.0 (7200 sec = 2 hour)
// (JST=FALSE、START_CLOCK=39600.0 (39600 sec = 11 hour)でもいいが)

    float GMT_clock;

    integer MONEY_MAX = 10; // 最大金額10L$
    integer TIME_INTERVAL = 3600; // 次回までの時間 3600秒 = 1時間

    list avatar_data = []; // キー、金額、時間(GMT)
    integer find_list; // リスト中のインデックス
    integer last_money; // 前回の金額
    float last_time; // 前回の終了時刻(GMT)
    float pass_time; // 前回からの経過時間算出

    key avatar_key;
    list last_avatar = []; // 前回座っていたアバターキー

//--------------------------------------------------
// Main
    default
    {
        state_entry()
        {
// 最初だけ時刻を取得して次のタイマーイベント発生までの時間を設定する
// タイマーイベントでは1日分の秒で設定し直している

            GMT_clock = llGetGMTclock(); // GMTの0:00:00からの秒数

            GMT_clock += START_CLOCK; // 開始時刻を0:00:00から調整
            if(JST) // 日本時間とする場合
            {
                GMT_clock += 32400.0; // + 9:00:00
            }
            if(GMT_clock > ONE_DAY_SEC) // 計算で1日以上になった場合
            {
                GMT_clock -= ONE_DAY_SEC; // 1日以内の秒数になるように補正
            }
            llSetTimerEvent(ONE_DAY_SEC - GMT_clock); // タイマー起動
        }

// Rez Event
        on_rez(integer start_param)
        {
// REZしたときにリセットすると、保持していたデータが無くなってしまう
// それでもよい、もしくはREZしたときは絶対リセットしたい場合はコメントをはずす
//          llResetScript();
        }

// Link Message Event
        link_message(integer sender_num, integer num, string str, key id)
        {

// 確認メッセージ受信
            if(num == LINK_MSG_REQUEST)
            {
                avatar_key = id;
                find_list = llListFindList(avatar_data, [avatar_key]);

// 保持データにキーが無い場合(新規)
                if(find_list == -1)
                {
                    last_money = 0;
                    llMessageLinked(sender_num, LINK_MSG_ANSWER, (string)last_money, avatar_key);
                }

// 保持データにキーがある場合
                else
                {
                    last_money = llList2Integer(avatar_data, find_list + 1); // 前回までの金額
                    last_time = llList2Float(avatar_data, find_list + 2); // 前回の終了時刻(GMT)

                    pass_time = llGetGMTclock() - last_time; // 前回からの経過時間算出
                    if(pass_time < 0) // GMTの0をまたいだ場合の補正
                    {
                        pass_time += ONE_DAY_SEC;
                    }

// 金額が上限の場合
                    if(last_money >= MONEY_MAX)
                    {
                        llMessageLinked(sender_num, LINK_MSG_LIMIT_NG, "", avatar_key);
                    }

// 前回終了から今回開始までの時間が少ない場合(前回から一定時間経過していない)
                    else if(pass_time < TIME_INTERVAL) // 「より小さい」にしているが「以下」なら<=
                    {
                        llMessageLinked(sender_num, LINK_MSG_TIME_NG, "", avatar_key);
                    }

// 前回座っていた場合
// 他に誰も同じ場所座らなかったら、この条件でいつまでもはじいてしまう。いいのか?

                    else if(llListFindList(last_avatar, [avatar_key]) != -1)
                    {
                        llMessageLinked(sender_num, LINK_MSG_CONTINUE_NG, "", avatar_key);
                    }

// キャンプ可能な場合
                    else
                    {
                        llMessageLinked(sender_num, LINK_MSG_ANSWER, (string)last_money, avatar_key);
                    }
                }
            }

// 立ち上がりメッセージ受信
            else if(num == LINK_MSG_STAND)
            {
                avatar_key = id; // メッセージのidが対象アバターキー
                last_money = (integer)str; // メッセージがこれまでの金額

                find_list = llListFindList(avatar_data, [avatar_key]);

// 保持データにアバターキーが無かった場合(新規追加)
                if(find_list == -1)
                {
                    avatar_data += [avatar_key, last_money, llGetGMTclock()];
                }

// 保持データにアバターキーがあった場合(上書き)
                else
                {
                    avatar_data = llListReplaceList(avatar_data, [avatar_key, last_money, llGetGMTclock()], find_list, find_list + 2);
                }

// 各場所の最後に座ったアバターキーの保持
                last_avatar = llListReplaceList(last_avatar, [avatar_key], sender_num - 2, sender_num -2);
            }
        }

// Timer Event
// 1日の切り替わりでイベント発生
// 保持しているデータを初期化する
        timer()
        {
            avatar_data = [];
            last_avatar = [];
            llSetTimerEvent(ONE_DAY_SEC);
        }
    }
//==================================================

次に子プリム用のスクリプトです。
座る箇所すべてに同じスクリプトを入れます。
やっていることは、実際のキャンプの時間と金額を更新と、支払い、キャンプできない場合のメッセージの表示などです。

//==================================================
// sasa_camp_child
// 1.0.0
// 2008/02/15
// created by sasapy Beck

//--------------------------------------------------
// Mode (スクリプトの動作に関わります。)

    integer GROUP_ONLY = FALSE; // FALSE:誰でも
                                                    // TRUE:同じグループのみ可
    float SECOND_MINI = 1200.0; // 最小時間1200.0秒 = 20分
    integer MONEY_MINI = 2; // 最小金額2L$
    integer MONEY_MAX = 10; // 最大金額10L$

//--------------------------------------------------
// Define
    integer LINK_MSG_RESET = 11; // メッセージ番号
    integer LINK_MSG_REQUEST = 21;
    integer LINK_MSG_ANSWER = 31;
    integer LINK_MSG_LIMIT_NG = 32;
    integer LINK_MSG_TIME_NG = 33;
    integer LINK_MSG_CONTINUE_NG = 34;
    integer LINK_MSG_STAND = 41;

    float now_time; // 経過時間
    integer last_money; // 前回までの金額(1日単位)
    integer now_money; // 現在の金額

    vector SIT_OFFSET = <0.3, 0.0, 0.5>; // 座る位置
    vector SIT_ROTATION = <0.0, 0.0, 0.0>; // 座る回転

    integer same_group; // アクティブグループが同一か
    string GROUP_NAME = "group name"; // グループのみキャンプ可のグループ名

    key owner_key; // オーナーキー
    key avatar_key; // アバターキー
    key camper_key; // キャンプ対象アバターキー
// 座ったときはavatar_key、キャンプ開始したらcamper_key
// プリムをリンクしているので座ったとき/立ち上がったときのイベントが、座っていない場所でも発生する
// 立ち上がったときのイベントでcamper_keyを判定し、自プリムかどうか判定する

    vector COLOR_OFF = <1.0, 1.0, 1.0>; // 白
    vector COLOR_ON = <1.0, 0.0, 0.0>; // 赤

//--------------------------------------------------
// Main
    default
    {
        state_entry()
        {
            owner_key = llGetOwner(); // オーナーキー取得

            llSitTarget(SIT_OFFSET, llEuler2Rot(SIT_ROTATION * DEG_TO_RAD)); // 座る位置指定

// 保持パーミッションがオーナーの場合
// 保持パーミッションが支払いの場合
// REZしたらllResetScriptしているので、パーミションは持っていない。条件判断しないでllRequestPermissionsするほうがよい。こういうパターンということでそのまま記述
            if((llGetPermissionsKey() == owner_key) && (llGetPermissions() & PERMISSION_DEBIT))
            {
                state operation;
            }
            else
            {
                llRequestPermissions(owner_key, PERMISSION_DEBIT);
            }
        }

// Rez Event
        on_rez(integer start_param)
        {
            llResetScript();
        }

// Permission Event
        run_time_permissions(integer perm)
        {
            if(perm & PERMISSION_DEBIT)
            {
                state operation;
            }
        }
    }

//--------------------------------------------------
state operation
    {
        state_entry()
        {
            llOwnerSay("Camp start");
        }

// Rez Event
        on_rez(integer start_param)
        {
            llResetScript();
        }

// Change Event
        changed(integer change)
        {
            if(change & CHANGED_LINK)
            {
                avatar_key = llAvatarOnSitTarget();

// 座った場合
                if(avatar_key != NULL_KEY)
                {
                    same_group = llSameGroup(avatar_key); // アクティブグループが同じ場合TRUE

// グループのみ可で、アクティブグループが違う場合
                    if(GROUP_ONLY && !same_group)
                    {
                        llUnSit(avatar_key); // 立たせる
                        llSay(PUBLIC_CHANNEL, "This camp is Group Only. Please active group : " + GROUP_NAME);
                    }

// グループ不問、またはグループのみ可の時のグループメンバーの場合
                    else
                    {
// ルートにリクエストを送信
                        llMessageLinked(LINK_ROOT, LINK_MSG_REQUEST, "", avatar_key);
                    }
                }

// 途中で立ち上がった場合
                else
                {
                    if(camper_key != NULL_KEY) // 自プリムでキャンプしていた
                    {
                        llSay(PUBLIC_CHANNEL, "You stopped camping.");

// 今回の金額が発生する場合
                        if(now_money > 0)
                        {
                            llSay(PUBLIC_CHANNEL, "I pay " + (string)now_money + " L$.");
                            llGiveMoney(camper_key, now_money); // 支払い
                            llMessageLinked(LINK_ROOT, LINK_MSG_STAND, (string)(last_money + now_money), camper_key); // ルートに情報送信
                        }
                        camper_key = NULL_KEY; // キーのクリア
                        llSetTimerEvent(0.0); // タイマー停止
                        llSetText("Camp chair\n" + (string)MONEY_MINI + " L$ / " + (string)llFloor(SECOND_MINI / 60.0) + " min", COLOR_OFF, 1.0); // テキスト表示
                    }
                }
            }
        }

// Link Message Event
        link_message(integer sender_num, integer num, string str, key id)
        {
// 許可メッセージの場合
            if(num == LINK_MSG_ANSWER)
            {
                last_money = (integer)str; // 前回までの金額
                camper_key = id; // キー取得

                now_time = 0.0; // キャンプ時間の初期化
                now_money = 0; // 今回金額の初期化

                llSetText("Your camp\n" + (string)now_money + " L$ / " + (string)llFloor(now_time / 60.0) + " min", COLOR_ON, 1.0); // テキスト表示
                llSetTimerEvent(SECOND_MINI); // 最小時間でタイマー起動
            }

// 金額上限の場合
            else if(num == LINK_MSG_LIMIT_NG)
            {
                llUnSit(avatar_key); // 立たせる
                llSay(PUBLIC_CHANNEL, "You reached the limit of 1 day. Please camp tomorrow.");
            }

// 前回キャンプから経過時間が少ない場合
            else if(num == LINK_MSG_TIME_NG)
            {
                llUnSit(avatar_key); // 立たせる
                llSay(PUBLIC_CHANNEL, "Your time does not pass from the last camping. Please camp after doing it for a while.");
            }

// 連続の場合
            else if(num == LINK_MSG_CONTINUE_NG)
            {
                llUnSit(avatar_key); // 立たせる
                llSay(PUBLIC_CHANNEL, "You cannot camp in succession. Please camp after doing it for a while.");
            }
        }

// Timer Event
// 最小時間でタイマーイベント発生
        timer()
        {
            now_time += SECOND_MINI; // キャンプ時間の更新
            now_money += MONEY_MINI; // 今回金額の更新

            llSetText("Your camp\n" + (string)now_money + " L$ / " + (string)llFloor(now_time / 60.0) + " min", COLOR_ON, 1.0); // テキスト表示

// 前回までの金額と今回の金額で上限となった場合
            if((last_money + now_money) >= MONEY_MAX)
            {
                llUnSit(camper_key); // 立たせる
                llSay(PUBLIC_CHANNEL, "You reached the limit of 1 day. Please camp tomorrow.");
                llSay(PUBLIC_CHANNEL, "I pay " + (string)now_money + " L$.");
                llGiveMoney(camper_key, now_money); // 支払い

                llMessageLinked(LINK_ROOT, LINK_MSG_STAND, (string)(last_money + now_money), camper_key); // ルートに情報送信
                camper_key = NULL_KEY; // キークリア
                llSetTimerEvent(0.0); // タイマー停止
                llSetText("Camp chair\n" + (string)MONEY_MINI + " L$ / " + (string)llFloor(SECOND_MINI / 60.0) + " min", COLOR_OFF, 1.0); // テキスト表示
            }
        }
    }
//==================================================

実際に自分で試してみましたが、意外と気がつかないところがあって、机上で作ったスクリプトにいろいろと手を加えました。

コメントでも書いてあるのですが、立ち上がったときの処理で最初に
avatar_key = llAvatarOnSitTarget();
としていたので、立ち上がるとNULL_KEYになってしまい、支払いする相手を見失ってしまいました。そこでcamper_keyという変数を持ち、ルートからキャンプ可のメッセージを受けたら、この変数にキーを保持するようにしました。

次に、changeイベントの発生ですが、座る部分が複数ある時に、座った場所以外でもイベントも発生してしまいます。
llAvatarOnSitTargetでキーを取得し、キーがあれば自分のところに座ったことになります。
キーがなかったらcamper_keyを参照し、それがNULL_KEYで無い場合は、自分のところで立ち上がったことになります。
camper_keyがNULL_KEYだったら、元々自分のところには座っていなかったということになります。
(説明が分かりにくい・・・)

 


この記事へのトラックバックURL

http://sasapy.slmame.com/t147429
この記事へのコメント
はじめまして!
スクリプトの作成時によく参考にさせてもらってます!
途中に書かれてるコメントのおかげで処理がよく分かり、
とても勉強になります!すごすぎです(≧ロ≦)
今、アイテムキャンプを作成したいと思って、
こちらのキャンプチェアーのスクリプトを
llGiveInventoryを使用するかたちにして、
色々触ってみたのですが、なかなか上手くいきません…。
できましたらサンプルコードを教えて頂けないでしょうか?
お願いします。
Posted by ソンジェソンジェ at 2008年03月21日 17:58
ソンジェさん、こんにちは。

ソンジェさんのブログに紹介していただき、ありがとうございます。

アイテムキャンプということで、基本的には一定時間座っているとアイテムがもらえるというものだと思いますが、そうすると通常のキャンプのように小刻みに時間で金額計算とか必要になくなります。
座ったらタイマー起動して時間まで座ってればインベントリーからギヴすればOK

コメントで書くとインデントできないので、あとで別に書きますね。
Posted by ささぴ at 2008年03月22日 20:33
ソンジェさん、新しい記事に書いたので、そちらを参照してください。
Posted by ささぴささぴ at 2008年03月22日 22:46