2009年7月1日水曜日

回転ドアの研究

話が前後します。わたしが参加していたスクリプトの勉強会の第一回のテーマがドアでした。第2回目がやじろべいで、そちらを先に書きましたが、今回は回転ドアの研究です。

研究すると言いましたが、それには、まずこのドアの仕様が分からなければならないのですが、残念ながらそのことに関する説明もない(開発の現場ではありがちなことですが、学生にはそれが普通だとは、口が裂けても言えないのが私の立場です)ので、以下に聞いた範囲で、わたしの想像も交えて仕様を箇条書きで書いてみました。

・90度回転する回転ドア
・タッチで開く
・開くときにはゆっくり開く
・自動的に閉まる
・タッチした人とは反対側に開く

という感じになるのでしょうか。しかし明文化せずともドアには必要な機能があります。それはどの向きに、ドアが置かれても正しく動作しなければならないということです。これを暗黙の仕様として、ソフトウエアの機能テストとして基礎中の基礎になりますが、仕様からブラック・ボックステストの手法でテストケースを作成し(厳密にはやっていません)、正しく機能を満足しているかを実際に動作させて調べてみました。
テストするために、このスクリプトをオブジェクトに入れて見ましたが、実行時にサウンドがないという意味のエラーメッセージが表示されることに加え、板全体が回転してしまい見たように動作しません。そこで、ここを調べたところ、

回転の軸をドアの端っこにするために、パスカットを使って板を半分にしています。
よくわからない方は、ひとまずパスカットの値を、0.375-0.875に設定してみて下さい。
立方体が半分になって、結果中心座標が端っこになります。


とのことで、まず回転軸をドアの端にするために、パスカットをしなければならないということが分かりました。また実行時に表示されたエラーメッセージは、ドアの開閉音らしいのでそれはコメントアウトしました。私が聞き漏らしたのかも思いますが、そのことに関する説明は勉強会当日にはありませんでした。

作成した状態から回転させないで動作させた場合
・90度回転する回転ドア
おおよそ90度回転して開きました
・タッチで開く
一回開いたて閉じた後、再度タッチしても開きません。もう一回タッチすると開きました。これは何度やっても同じで、一回開いて閉じたドアを再度開こうとして1回タッチしても、1回目ではドアは反応せず、2回目のタッチに反応して開くようです。
・開くときにはゆっくり開く
滑らかな動きではなく、間歇的な動きでしたが、ゆっくり開きました
・自動的に閉まる
仕様通り一定時間後に閉まるようです
・タッチした人とは反対側に開く
とりあえず動作しているようです

作成した状態からドアを回転させて動作させた場合
・90度回転する回転ドア
おおよそ90度回転して開きました
・タッチで開く
(作成した状態から回転させないで動作させた場合と同じ不具合がありました)
・開くときにはゆっくり開く
特に問題なし
・自動的に閉まる
ドアが閉じるときに、回転させる前の閉じ位置にドアが戻ってしまいました
・タッチした人とは反対側に開く
回転させた角度によって、様相がかわります。ある角度では正しく動作するのですが、別な角度では、ドアの手前にアバターがいるか/向こう側にいるかではなく、アバターがドアの右側にいるか/左側にいるかで開く向きが変わりました。)と同じような不具合が出る場合もあり、また、手前に開く場合もありました

きちんとしたテスト計画を作っていれば、確実に「テスト中止、開発側にリジェクト」のケースです。

そこで問題を検討するために、以下にそのときにもらったスクリプトを示します。

rotation rot;
integer counter = 0;
vector door;
 
default
{
state_entry()
{
rot = llGetRot();
door = llGetPos();
}
 
touch_start(integer num)
{
llPlaySound("door04", 1.0);
vector av = llDetectedPos(0);
if(door.y > av.y){ state door_out; }
else{ state door_in; }
}
 
on_rez(integer num)
  {
llResetScript();
}
}
 
state door_out
{
state_entry()
{
llSetTimerEvent(0.1);
}
 
timer()
{
counter++;
if( counter == 10 ){ counter = 0; state out_next; }
llSetRot( llGetRot()*llEuler2Rot(<0.0,0.0,10.0> * DEG_TO_RAD ));
}
}
 
state out_next
{
state_entry()
{
llSleep(10);
llSetTimerEvent(0.1);
}
 
timer()
{
counter++;
if( counter == 10 ){ counter = 0; llSetTimerEvent(0.0);
llPlaySound("door04", 1.0);
llSetRot( rot ); state default; }
llSetRot( llGetRot()*llEuler2Rot(<0.0,0.0,-10.0>*DEG_TO_RAD ));
}
}
 
state door_in
{
state_entry()
{
llSetTimerEvent(0.1);
}
 
timer()
{
counter++;
if( counter == 10 ){ counter = 0; state in_next; }
llSetRot( llGetRot()*llEuler2Rot(<0.0,0.0,-10.0> * DEG_TO_RAD ));
}
}
 
state in_next
{
state_entry()
{
llSleep(10);
llSetTimerEvent(0.1);
}
 
timer()
{
counter++;
if( counter == 10 ){ counter = 0; llSetTimerEvent(0.0);
llPlaySound("door04", 1.0);
llSetRot( rot ); state default; }
llSetRot( llGetRot()*llEuler2Rot(<0.0,0.0,10.0>*DEG_TO_RAD ));
}
}


中身を検討するに当たって、いただいたソースではとても気持ち悪いので、インデントを書き換えたのが以下になります。

   1: rotation rot;
2: integer counter = 0;
3: vector door;
4:
5: default
6: {
7: state_entry() {
8: rot = llGetRot();
9: door = llGetPos();
10: }
11: touch_start(integer num) {
12: //llPlaySound("door04", 1.0);
13: vector av = llDetectedPos(0);
14: if (door.y > av.y) {
15: state door_out;
16: } else {
17: state door_in;
18: }
19: }
20: on_rez(integer num) {
21: llResetScript();
22: }
23: }
24:
25: state door_out
26: {
27: state_entry() {
28: llSetTimerEvent(0.1);
29: }
30: timer() {
31: counter++;
32: if (counter == 10) {
33: counter = 0;
34: state out_next;
35: }
36: llSetRot(llGetRot()*llEuler2Rot(<0.0,0.0,10.0>*DEG_TO_RAD));
37: }
38: }
39:
40: state out_next
41: {
42: state_entry() {
43: llSleep(10);
44: llSetTimerEvent(0.1);
45: }
46: timer() {
47: counter++;
48: if (counter == 10) {
49: counter = 0;
50: llSetTimerEvent(0.0);
51: //llPlaySound("door04", 1.0);
52: llSetRot(rot);
53: state default;
54: }
55: llSetRot(llGetRot()*llEuler2Rot(<0.0,0.0,-10.0>*DEG_TO_RAD));
56: }
57: }
58:
59: state door_in
60: {
61: state_entry() {
62: llSetTimerEvent(0.1);
63: }
64: timer() {
65: counter++;
66: if (counter == 10) {
67: counter = 0;
68: state in_next;
69: }
70: llSetRot(llGetRot()*llEuler2Rot(<0.0,0.0,-10.0>*DEG_TO_RAD));
71: }
72: }
73:
74: state in_next
75: {
76: state_entry() {
77: llSleep(10);
78: llSetTimerEvent(0.1);
79: }
80: timer() {
81: counter++;
82: if (counter == 10) {
83: counter = 0;
84: llSetTimerEvent(0.0);
85: //llPlaySound("door04", 1.0);
86: llSetRot(rot);
87: state default;
88: }
89: llSetRot( llGetRot()*llEuler2Rot(<0.0,0.0,10.0>*DEG_TO_RAD));
90: }
91: }


正直に言って、ずっこけてしまいました。不具合の80%(この数値はいい加減です)原因は、14行目にあります。正の回転でドアを開けるのか、それとも負の回転でドアを開けるのかの判断をしているらしいのが14行目ですが、その判断に、ドアとタッチしたアバターのy座標(南北方向)の位置関係だけで判断しているようです。「あのう、すみません。脳みそ溶けているだけじゃなくて、耳から流れ出してますけど・・・」
そして閉じた後に、1回のタッチでドアが反応せず、2回目で反応する件は、どうもリンデン側のバグのようです。これも、google検索で「jira state transfer touch」というキーワード一発でみつかる有名なバグのようです。これの回避は簡単で、ステートの遷移をしないようにしなければ良いだけです。

やはり、このソースも修正するのは難しいので、スクラッチで書き直してみました。

   1: vector      GOrigPos;
2: vector GOrigRot;
3: integer GStateFlag = 0;
4: float GOpenPeriod = 5.0;
5: integer GCommChannel = -15643;
6: integer GCommHandle;
7:
8: doorOpen(vector p)
9: {
10: GOrigPos = llGetPos();
11: GOrigRot = llRot2Euler(llGetRot());
12:
13: vector targetPos = p;
14: float targetAngle = llAtan2(targetPos.y - GOrigPos.y,
15: targetPos.x - GOrigPos.x);
16: float myAngle = GOrigRot.z;
17: float direction = targetAngle - myAngle;
18:
19: while (direction <= -PI || PI < direction) {
20: if (direction < 0.0) {
21: direction += TWO_PI;
22: } else {
23: direction -= TWO_PI;
24: }
25: }
26:
27: if (0.0 <= direction) {
28: GStateFlag = -1;
29: } else {
30: GStateFlag = 1;
31: }
32: llSetRot(llEuler2Rot(GOrigRot +
33: GStateFlag*<0.0, 0.0, PI_BY_TWO>));
34: }
35:
36: doorClose()
37: {
38: llSetRot(llEuler2Rot(GOrigRot));
39: GStateFlag = 0;
40: }
41:
42: default
43: {
44: state_entry() {
45: GStateFlag = 0;
46: GCommHandle = llListen(GCommChannel, llGetObjectName(), NULL_KEY, "");
47: }
48: touch_start(integer total_number) {
49: if (GStateFlag == 0) {
50: vector p = llDetectedPos(0);
51: llWhisper(GCommChannel, llList2CSV(["open", (string) p]));
52: doorOpen(p);
53: llSetTimerEvent(GOpenPeriod);
54: } else {
55: llWhisper(GCommChannel, "close");
56: doorClose();
57: llSetTimerEvent(0.0);
58: }
59: }
60: timer() {
61: llSetTimerEvent(0.0);
62: llWhisper(GCommChannel, "close");
63: doorClose();
64: }
65: listen(integer channel, string name, key id, string message){
66: if (channel == GCommChannel && name == llGetObjectName()) {
67: list m = llCSV2List(message);
68: string m1 = llList2String(m, 0);
69: if (m1 == "open") {
70: doorOpen((vector) llList2String(m, 1));
71: } else if (m1 == "close") {
72: doorClose();
73: }
74: }
75: }
76: }


あ、仕様も勝手に変えてしまいました。

・開いている状態の時にタッチしても閉まる
・同じ名前のドアが10m以内にあった場合、タッチしたドアと同期する
・ゆっくり閉まらない

それに今見直してみると、デバッグ時に使用した冗長な部分(doorOpen(vector p)関数内)がかなりありますね。直すのがかったるいので、ごめんなさい。

1 件のコメント:

  1. というわけで?私のバージョンとNullpoさんのバージョンのドアのスクリプトをブログに載せてみました。
    http://blog.innx.co.jp/vw/secondlife/lsl/2009-07-03-door-script-samples

    ---
    ところで、元々配布されたスクリプトにはいろいろツッコミ所がありますが、ほぼ同じ内容の処理を4つのstateにわざわざ分けているのは、いくらなんでも可読性以前に無駄が多すぎますよね。
    インワールドでもお話したのですが、stateはいろいろ便利なのでタイマー処理の切り替えなどのためだけに安易に使ってしまうというのをよく見かけるのですが、個人的にはそのような使い方はあまり好きではありません。
    (という部分を話すと、宗教論争に発展してしまいそうですが(笑))



    いずれにしましても、こうやってスクリプトにツッコミを入れたり、自分たちで書き直してみるというのは、とても楽しいです。

    返信削除