buyとpay
buildツールをいじっていて、オブジェクトをクリックしたときの動作に、「buy」と「pay」があるのを見て、
「どう違うんじゃい?」
と思った方もいらっしゃるのではないでしょうか。
ごく簡単に言ってしまうと、「buy」はスクリプトを使わずにアイテムを売る際に使います。
「pay」はスクリプトでお金を受け取る際に使います。
「buy」は、buildツール上で販売価格を設定し、コンテンツの中身を売ったり、コピーを売ったりします。
普通に商品を売るときにはこの方法が一番手軽で便利です。
一方「pay」はスクリプトで制御することが出来ますので、商品販売以外にも応用が利きます。
例えば、10L$払うと上空まで運んでくれる乗り物とか。
お金を払うと一定時間動く、遊園地のアトラクションなども出来ます。
カジノマシーンなどもそうですね。
今回はスクリプトによるベンダーを作っているわけですから、当然「pay」を使います。
buildしたベンダーをクリックしたときの動作は、「pay」にセットしておきましょう。
誰かが「pay」をしようとすると、まずその人のクライアント画面にpayダイアログが表示されます。
このダイアログには金額ボタンや金額の入力欄があります。
いくら支払うのかを確認・入力し、payを実行すると、オブジェクトの中のスクリプトが動き出します。
「payされた」というイベントが発生するわけです。
「pay」されたときに発生するイベントは、前回名前だけちょろっと書きましたが、moneyというイベントになります。
money(key id, integer amount){
// 処理
}
引数「key id」には支払った人のUUIDが入ってきます。
「integer amount」は支払われた金額です。
これで「誰が」「いくら」支払ったのかがわかりますので、正しい金額が支払われたかどうかをチェックし、支払った人にアイテムを渡すことができます。
なお、スクリプトにmoneyイベントが書かれていないと、「pay」は動作しません。
「pay」できるようにしたいときは、必ずmoneyイベントが必要になります。
難しい引数があるわけでもないので、簡単に出来てしまいそうに思えますが、まだまだこの先があります(^^;
金返せ!
「pay」は基本的にはスクリプト側で受け取った金額を判断し、商品を渡す処理を実装しなければなりません。
設定にもよりますが、「pay」による支払いは支払う側が自由に金額を入力することもできますので、何も考えずに実装すると、100L$の商品に対して、1000L$の支払いや10L$の支払いをすることができてしまう可能性があります。
きちんと金額の判定を行わなければ、トラブルになるのは目に見えています。
非常にシビアですので、お金のやり取り部分が正しく動作するよう、二重三重の対策を施しておくべきでしょう。
まず第一に、「pay」で表示される支払いダイアログをコントロールし、決まった金額しか支払えないようにしておきます。
通常payダイアログには、1L$、5L$、10L$、20L$のボタンと、自由に金額を入力できる欄があります。
このままですと、操作する人がいくらでも好きな金額を支払えます。
Tipjarなどの場合はそれでもいいのですが、今回は決まった商品の販売ですから、商品の値段のみを支払えるように制限をかけます。
それにはllSetPayPrice()という関数を使用します。
llSetPayPrice(integer price, list quick_pay_buttons);
integer price
金額入力欄に表示される額を設定します。
例えばここに100を設定しておくと、payダイアログの金額入力欄には100という数字が表示されます。
ですが、入力欄は操作する人が自由に変更できてしまいますので、初期値を設定するだけでは十分ではありません。
今回はこの入力欄を一切表示しないようにしてしまいましょう。
入力欄を消すには、ここにPAY_HIDEをいう値をセットします。
list quick_pay_buttons
金額ボタンを設定します。
デフォルトでは1L$、5L$、10L$、20L$の4つのボタンがあります。
これを例えば、10L$、100L$、1000L$にしたい場合は、
[10, 100, 1000, PAY_HIDE]
と指定します。
PAY_HIDEを指定するとそのボタンは表示されなくなります。
今回は商品の値段に固定しますので、ボタンを一つだけにします。
例えば50L$の商品であれば、
[10, PAY_HIDE, PAY_HIDE, PAY_HIDE]
のようにセットします。
このllSetPayPrice()を使うことで、商品の金額以外のお金は支払えなくなります。
これで対策は十分でしょうか?
次のような場合を考えてみましょう。
(1)100L$の商品を選択する
(2)payを選ぶと100L$のボタンが表示される
(3)
実際に100L$を支払う前に次の商品(50L$)を表示する
(4)100L$支払いのボタンを押す
このように、お客さんが想定外の操作をする可能性があります(^^;
上記のように操作されてしまうと、表示している商品は50L$にも関わらず、100L$の支払いが出来てしまいます。
ですので、moneyイベントの中で、支払われた金額と表示している商品の金額を判定し、一致していない場合は返金するような仕組みを作っておかなければなりません。
例えば商品の値段がpriceという変数に入っているとして、
money(key id, integer amount){
if (amount == price) {
// 金額が一致するので商品を渡す処理
} else {
// 金額が一致しないので返金する
}
}
このような判定が必須です。
返金する際にはllGiveMoney()関数を使います。
この関数は指定した相手に指定した金額を支払う関数です。
llGiveMoney(key destination, integer amount)
key destination
支払う相手のUUIDを指定します。
今回は返金ですので、moneyイベントで受け取った支払者のUUIDをセットしてやればOKです。
integer amount
いくら支払うかです。
差額返金などを考えてもいいのですが、ややこしくなるので全部返してしまったほうが楽でしょう。
moneyイベントで受け取った、入金額をそのまま指定してやれば良いですね。
まだ考えるべきところはありますが、これでずいぶんと安全なスクリプトになるでしょう。
しかしながら、llGiveMoney()関数を使うときには、少々考えなければならないことがあります。
llGiveMoney()関数は実は、いきなり使うことのできない関数なのです。
パーミッション
パーミッションという考え方があります。
スクリプトの実行許可のことです。
lslでは様々な処理が可能ですが、全ての処理を無条件に行えるわけではありません。
例えば、さきほど出てきたllGiveMoney()について考えてみてください。
llGiveMoney()を使って、自分に対してL$を支払うスクリプトを書き、それを他人に渡したとします。
もしもllGiveMoney()が無条件に実行できてしまったら、他人からお金をふんだくるスクリプトが簡単に書けてしまうわけです。
アバターのアニメーションなどもそうです。
無条件に実行できるのなら、恥ずかしい格好をするアニメーションを作り、視界に入る人を誰彼かまわずアニメーションさせるようなスクリプトを組むことが可能です(^^;
そんな事態にならないよう、お金を扱う関数を始めとして、アバターのアニメーション、オブジェクトのリンクの変更、カメラ(視点)の制御、キー入力の判定などの処理については、使う前にパーミッション(許可)を取らなければなりません。
少々面倒な手続きになりますが、重要な処理ですのでしっかり押さえておきましょう。
まずはパーミッションを取得するための関数ですが、llRequestPermissions()といいます。
llRequestPermissions(key agent, integer perm)
key agent
誰に対して許可を取るかの指定です。
UUIDをセットします。
今回のllGiveMoney()の実行許可を取る際には、ここにはオブジェクトオーナーのUUIDしか使えません。
integer perm
何の許可を取るかです。
いくつか値がありますが、今回使う値はPERMISSION_DEBITという値です。
アニメーションの許可やリンク変更の許可を取る場合には別の値が用意されています(別の機会に説明します)。
このllRequestPermissions()関数を使うと、許可を求める人のクライアント画面にダイアログが表示されます。
アニメーションの許可確認のダイアログなど見たことがある人もいるでしょう。
このダイアログに対して「OK」を出さない限り、パーミッションは取得されません。
もしも予期せぬところで「支払い許可のダイアログ」などが出てきた場合は、安易にOKしてはいけません!
パーミッションさえ与えなければ、勝手に支払いが行われることはありませんので、何かダイアログが出てきた場合はよく注意してから操作しましょう。
なお、このパーミッションというのはスクリプトごとに個別のものです。
一つのスクリプトに対して許可を与えたからといって、他のスクリプトも許可されるわけではありません。
スクリプトを作る側からすると、いちいち毎回パーミッションを取らなければいけないのはわずらわしいことではありますが、全てはSLの健全な秩序維持のためですので、文句を言わずに実装しましょう(^^;
パーミッションが取れると(あるいは不許可の場合も)スクリプトではrun_time_permissionsイベントが発生します。
run_time_permissions(integer perm) {
// 必要なパーミッションが取れたかどうかの確認
}
integer perm
パーミッションのデータが入ってきます。
PERMISSION_DEBITパーミッション以外の値もまとまって入ってきますので、判定する際には以下のような小細工が必要です。
run_time_permissions(integer perm) {
if (perm & PERMISSION_DEBIT){
// PERMISSION_DEBITパーミッションが許可された
} else {
// PERMISSION_DEBITパーミッションが許可されなかった
}
}
この書き方はパーミッションを扱うときのお約束パターンです。
「perm & PERMISSION_DEBIT」と書いてやると、PERMISSION_DEBITパーミッションが許可されているかどうかが判定できます。
今回のベンダースクリプトでは、PERMISSION_DEBITパーミッションが取得できないと返金処理ができませんので、スクリプトの最初でパーミッション取得の処理を行い、run_time_permissions()で許可が取れたことを確認した後、実際のベンダー機能が使えるようにすべきでしょう。
パーミッションについては、少々わかりにくいかと思いますので、今回全てを理解できなくても構いません。
いずれアニメーションのスクリプト等でも再び出てくることになりますから、少しずつ理解を深めていくことにしましょう。
今回知っておいて欲しいことは、lslには勝手に実行できない命令がいくつかあり、実行する際にはパーミッションを取得しなければならないのだ、という概要のみです。
アイテムの存在確認
安全なベンダーにするために、最後にもう一つ工夫しましょう。
お金の受け取りについては必要な対策を考えましたが、今度はアイテムを渡す際の間違いを防ぐ方法です。
スクリプトからアイテムを渡す際には、llGiveInventoryList()関数を使えば出来ることをすでに学びました。
ですが、この関数が正しく動作するためには、オブジェクトのコンテンツの中にアイテムが入っていなければなりません。
うっかりアイテムを入れ忘れたままベンダーを設置し、お客さんがお金を払ったとき、スクリプトがエラー終了してしまったらオオゴトです(^^;
アイテムがきちんとコンテンツの中にあるかどうか確認し、もし無いようならお客さんにお金を返すような仕組みにしておくべきでしょう。
アイテムの存在確認のためには、llGetInventoryType()関数を使います。
integer llGetInventoryType(string name)
この関数は、本来は指定された名前のアイテムの種別(サウンドとか、アニメーションとか、スクリプトとか)を調べるための関数です。
ですが、もしも指定された名前のアイテムがコンテンツに見つからないときは、INVENTORY_NONEという値を返してくれます。
これを利用し、もしINVENTORY_NONE以外の値が返ってきたら、アイテムはちゃんとインベントリの中にあるということがわかります。
例えば、"Violin"というアイテムがコンテンツの中にあるかどうかを調べるには、
if (llGetInventoryType("Violin") != INVENTORY_NONE) {
// INVENTORY_NONE以外なので"Violin"はある
} else {
// "Violin"は無い
}
このようにします。
厳密に言うと、さらに、アイテムがトランスファー可能かどうかを調べるべきでしょう。
コンテンツの中にあっても、トランスファー不可能な状態だったら、アイテムを渡すことができません。
ですが、自分で作成したアイテムに関しては、トランスファーが不可になることは通常ありませんので、ここではトランスファー可能かどうかを調べる処理は省略します。
ベンダースクリプト
以上の安全対策を踏まえたスクリプトが以下です。
list commodity = [
// name, texture, object, price
"Guiter", "GuiterImage", "GuiterBox", 500,
"Piano", "PianoImage", "PianoBox", 600,
"Trumpet", "TrumpetImage", "TrumpetBox", 500,
"Violin", "ViolinImage", "ViolinBox", 550
];
integer current_id = 0;
integer view_side = 1;
set_commodity(){
llSetTexture(llList2String(commodity, current_id * 4 + 1), view_side);
llSetPayPrice(PAY_HIDE, [llList2Integer(commodity, current_id * 4 +3), PAY_HIDE, PAY_HIDE, PAY_HIDE]);
}
default {
state_entry(){
llRequestPermissions(llGetOwner(), PERMISSION_DEBIT);
}
run_time_permissions(integer perm) {
if (perm & PERMISSION_DEBIT){
state active;
} else {
llRequestPermissions(llGetOwner(), PERMISSION_DEBIT);
}
}
}
state active {
state_entry(){
set_commodity();
}
touch_start(integer detected){
integer i = llDetectedLinkNumber(0);
if (i == 2) { // back button
current_id --;
if (current_id < 0){
current_id = llGetListLength(commodity) / 4 - 1;
}
set_commodity();
}else if (i == 3) { // next button
current_id ++;
if (current_id >= llGetListLength(commodity) / 4) {
current_id = 0;
}
set_commodity();
}
}
money(key id, integer amount){
integer p = llList2Integer(commodity, current_id * 4 +3);
if (amount == p){ // payの金額と商品の価格が一致
if (llGetInventoryType( llList2String(commodity, current_id * 4 +2)) != INVENTORY_NONE) { // アイテム存在確認
llSay(0, "Thank you for purchasing! Please wait until getting the commodity...");
llGiveInventoryList(id, llList2String(commodity, current_id * 4), [llList2String(commodity, current_id * 4 +2)]);
}else{
llSay(0, "I am sorry very much. This commodity is sold out now.");
llGiveMoney(id, amount);
}
}else{
llSay(0, "You paid wrong amount. I repays to you " + (string)amount + "L$.");
llGiveMoney(id, amount);
}
}
}
最初の頃に比べると、ずいぶんとスクリプトっぽくなってきました(^^;
まず、商品のデータの管理ですが、commodityというlist型の変数で行っています。
このリストには商品名、テクスチャ名、実際のオブジェクト名、価格の順番でデータを並べてあります。
データが4つで1商品ですね。
このようなデータの場合、特定の商品の情報を取り出すにはどのようにすればよいでしょうか。
例えば3番目の商品のデータを取り出す場合には、
llList2String(commodity,3 * 4) // 商品名
llList2String(commodity,3 * 4 + 1) // テクスチャ名
llList2String(commodity,3 * 4 + 2) // オブジェクト名
llList2Integer(commodity,3 * 4 + 3) // 価格
このようになります。
商品データは4つで1商品ですので、3番目の商品のデータはリストの3 * 4番目から始まっています。
それに+1すると3番目の商品のテクスチャ名ですし、+2ならオブジェクト名です。
+3した価格はinteger型なので、llList2Integer()関数で取り出します。
今回のスクリプトでは、商品の番号をcurrent_idという変数で管理していますので、
llList2String(commodity,current_id * 4 + n)
のような表現がたくさん出てきます。
リストの使い方としては基本形の一つです。
set_commodityユーザー関数には、llSetPayPrice()を使った支払額の設定処理を追加しています。
表示中の商品の金額のみがpayダイアログに表示されるようにしています。
defaultステートは、パーミッション取得のみを行うステートにしました。
パーミッションが取得できて初めて、activateステートに移ります。
こうすることで確実に返金処理が実行可能なスクリプトにしています。
activeステートは前回のスクリプトにmoneyイベントを追加したものです。
moneyイベントの中では、まず支払われた金額と商品の価格とが一緒かどうかを判定しています。
価格が異なる場合は
"You paid wrong amount. I repays to you " + (string)amount + "L$."
とメッセージを表示して、返金を行います。
「(string)amount」の部分はまだ説明していませんでした。
amountはinteger型の変数ですが、メッセージはstring型です。
integer型のものをメッセージに含ませる場合は、string型に変換してやらないと表示できません。
「(string)amount」というのは、「amountをstring型に変更する」という意味です。
これで、例えばamountが100だった場合は、
"You paid wrong amount. I repays to you 100L$."(金額が違うよ。100L$返すよ)
と表示されるようになります。
支払われた金額と商品の価格とが一致していた場合、今度はllGetInventoryType()関数を使ってアイテムの存在確認をします。
アイテムが無かった場合、
"I am sorry very much. This commodity is sold out now."(ごめんね、その商品は今売り切れなんだわ)
苦し紛れの言い訳メッセージを表示するようにしました。
そして受け取ったL$を返金しています。
アイテムがちゃんと存在していた場合は、llGiveInventoryList()関数を使ってアイテムを渡しています。
これでようやく、一通り動作するベンダーが完成しました。
今回のポイント
payされたときのイベント:
money(key id, integer amount){
// 処理
}
payダイアログの設定:
llSetPayPrice(integer price, list quick_pay_buttons);
L$を支払う関数:
llGiveMoney(key destination, integer amount)
パーミッションの取得:
llRequestPermissions(key agent, integer perm)
パーミッションが変わったときのイベント:
run_time_permissions(integer perm) {
if (perm & PERMISSION_DEBIT){
// PERMISSION_DEBITパーミッションが許可された
} else {
// PERMISSION_DEBITパーミッションが許可されなかった
}
}
この例は「支払いパーミッション」の確認時。
アイテムの存在確認:
if (llGetInventoryType("アイテム名") != INVENTORY_NONE) {
// INVENTORY_NONE以外なので"アイテム名"はある
} else {
// "アイテム名"は無い
}
n個ずつのデータが管理されているリスト型変数listにおいて、m番目のデータセットのl個目のデータを取得する方法:
llList2String(list, m * n + l)
この例はデータがstring型の場合。
初級スクリプトと言いつつ、中身は結構濃くなってきています(^^;
今回特に重要なのはパーミッションの考え方です。
また他のスクリプトでも出てきますので、パーミッションについては覚えておいて下さい。
さて、ベンダースクリプトは機能的には完成しましたが、まだ改良の余地があります。
例えば、商品データの管理をリストで行っている部分。
商品が増えるたびにここを修正しなければいけないのは少々手間かもしれません。
販売可能なアイテムが一度に一つずつなのも不便です。
箱詰めにして売るならばこのスクリプトでも良いのですが、何か工夫の余地はないものでしょうか。
次回はその辺りについて考えてみたいと思います。