2009年8月23日日曜日

オープンソース・カンファレンス

学会で名古屋に行ったついでに、オープンソース・カンファレンス2009名古屋に参加してきました。学会はみどりさんと一緒だったので、オープンソース・カンファレンスにも誘ったのですが、学会が終了するとさっさと戻っていってしまいました(笑。



こんな写真しかないんかい、と言われそうですが、ないんですw。

思えばオープンソースカンファレンス、2004年の秋にうちの学校で始めたんですね。いやー、もう5年前になるんですね。そんときの記録がないか調べてみたんですけど、手元にはないのでWeb上で調べたところ、ありました。
一部の人には、有名ですが「がんばれ!!ゲイツ君」のこのエントリに、ちっちゃいながら記述が残っていました。いやー懐かしいw。

地域系勉強会パネルディスカッション【2コマ連続】に興味があったのですが、これって難しいんですよね。自分にも地域の勉強会に参加した経験があるのですが、これまで長続きした経験がない。そこで、長続きさせるためにどんなことをやっているのか、とっても興味がありました。まあ結論としては「緩く」というのが、最大の秘訣みたいに聞こえましたが、それだけじゃないんだろうと思いました。

セカンドライフ内部には、キラカフェみたいな現実社会のことについて、セカンドライフ内部で談話会をやっているところもありますが、もうちょっと現実社会で役に立つというか、セカンドライフ内部にはとどまらない勉強会みたいなことを、機会があればやってみようかな(ってあるじゃんw)。

あー、それから、会場で、GOTTiさんにお会いしました。ランチでもと思ったのですが、場所が場所(大学)で、時期が時期(夏休み中)だったので、ろくなところがなく、コンビニでおにぎりを買って会場内で食べましたw。

2009年7月27日月曜日

セカンドライフ内の最高速度

腰痛で死んでます。

昨日の夕方から、腰を動かすと猛烈に痛くなり、今日は休んで整体に行ってきました。整体の先生曰く「来てますねぇ」と(笑。という訳で今日と明日は休んで、腰の治療に励みますといっても、温和しくしているしているだけなのですが。

最近セカンドライフ内の物理エンジンについて、ちょっと疑問なところがあって調べていたのですが、セカンドライフ内では、物理オブジェクトには、ちゃんと重力加速度が働いている様です。

上空4,000mから、以下のスクリプトを入れたオブジェクトを落として、その速度を1秒ごとに測定してみました。

   1: default
2: {
3: touch_start(integer total_number) {
4: llSetPrimitiveParams([
5: PRIM_PHYSICS, TRUE,
6: PRIM_TEMP_ON_REZ, TRUE,
7: PRIM_PHANTOM, TRUE
8: ]);
9: llSetTimerEvent(1.0);
10: }
11: timer() {
12: llOwnerSay((string) llGetVel());
13: }
14: }


典型的なデータを以下に示します

[21:25] FreeFall: <0.00000, 0.00000, -11.29792>
[21:26] FreeFall: <0.00000, 0.00000, -21.53347>
[21:26] FreeFall: <0.00000, 0.00000, -31.33345>
[21:26] FreeFall: <0.00000, 0.00000, -40.91565>
[21:26] FreeFall: <0.00000, 0.00000, -50.71563>
[21:26] FreeFall: <0.00000, 0.00000, -60.29782>
[21:26] FreeFall: <0.00000, 0.00000, -70.53347>
[21:26] FreeFall: <0.00000, 0.00000, -80.33360>
[21:26] FreeFall: <0.00000, 0.00000, -90.35152>
[21:26] FreeFall: <0.00000, 0.00000, -99.49833>
[21:26] FreeFall: <0.00000, 0.00000, -109.29850>
[21:26] FreeFall: <0.00000, 0.00000, -119.53420>
[21:26] FreeFall: <0.00000, 0.00000, -129.11650>
[21:26] FreeFall: <0.00000, 0.00000, -138.91630>
[21:26] FreeFall: <0.00000, 0.00000, -148.49840>
[21:26] FreeFall: <0.00000, 0.00000, -158.08040>
[21:26] FreeFall: <0.00000, 0.00000, -168.31570>
[21:26] FreeFall: <0.00000, 0.00000, -178.11550>
[21:26] FreeFall: <0.00000, 0.00000, -187.69760>
[21:26] FreeFall: <0.00000, 0.00000, -197.49740>
[21:26] FreeFall: <0.00000, 0.00000, -200.00000> ← ここで速度がサチってます
[21:26] FreeFall: <0.00000, 0.00000, -200.00000>
[21:26] FreeFall: <0.00000, 0.00000, -200.00000>
[21:26] FreeFall: <0.00000, 0.00000, -200.00000>
[21:26] FreeFall: <0.00000, 0.00000, -200.00000>
[21:26] FreeFall: <0.00000, 0.00000, -200.00000>
[21:26] FreeFall: <0.00000, 0.00000, -200.00000>
[21:26] FreeFall: <0.00000, 0.00000, -200.00000>
[21:26] FreeFall: <0.00000, 0.00000, -200.00000>
[21:26] FreeFall: <0.00000, 0.00000, 14.96331> ← ここで地面と衝突した?
[21:26] FreeFall: <0.00000, 0.00000, 4.72773>
[21:26] FreeFall: <0.00000, 0.00000, -5.07227>
[21:26] FreeFall: <0.00000, 0.00000, 1.47556>
[21:26] FreeFall: <0.00000, 0.00000, 0.00452>
[21:26] FreeFall: <0.00000, 0.00000, 0.00000>
[21:26] FreeFall: <0.00000, 0.00000, 0.00000>

何度か試してみましたが、このデータから重力加速度を計算すると、9.8になります。
ふむ、割とまじめに作ってあるんですね。これならいろいろ物理シミュレーションを視覚的に作れそうな感じです。夏休みにはがんばってみるか。

2009年7月26日日曜日

35cmの謎 - 2つの椅子のスクリプト

2つの椅子をスクリプトを作ってみました。

特に変わった椅子ではありません。ただ、New Script で時々お会いするペンギンさん(ってたくさんいらっしゃるので)のこのエントリが気になったので、2つの椅子のスクリプトを作ってみました。

ひとつは

   1: vector  GSitPos = <0.3, 0.0, 0.56>;
2: vector GSitRot = <0.0, 0.0, 0.0>;
3:
4: default
5: {
6: state_entry() {
7: llSitTarget(GSitPos, llEuler2Rot(DEG_TO_RAD*GSitRot));
8: llSetLinkPrimitiveParams(2, [PRIM_POSITION, GSitPos]); //***
9: }
10: }

LSLのテキストの定番、llSetTargetを使って書いた例です。//***というコメントがついた行(8行目)は、llSetTargetで設定した位置を示すための球状のプリムの位置を示すためのコードです。
そして、もう一つが、

   1: vector  GSitPos = <0.3, 0.0, 0.91>;
2: vector GSitRot = <0.0, 0.0, 0.0>;
3: integer GMyPrimCount;
4:
5: default
6: {
7: state_entry() {
8: GMyPrimCount = llGetNumberOfPrims();
9: llSetLinkPrimitiveParams(GMyPrimCount, [PRIM_POSITION, GSitPos]); //***
10: }
11: changed(integer change){
12: if (change & CHANGED_LINK) {
13: integer nLinkCount = llGetNumberOfPrims();
14: if (GMyPrimCount + 1 == nLinkCount) {
15: llSetLinkPrimitiveParams(nLinkCount, [
16: PRIM_POSITION, GSitPos,
17: PRIM_ROTATION, llEuler2Rot(DEG_TO_RAD*GSitRot)/llGetRot()
18: ]);
19: } else if (GMyPrimCount + 1 < nLinkCount) {
20: llUnSit(llGetLinkKey(nLinkCount));
21: }
22: }
23: }
24: }

こちらになります。このスクリプト中の//***というコメントがついた行(9行目)は、やはりllSetLinkPrimitiveParamsで、座る位置を指定する場所を示すために、球状のプリムをそこに移動させるものなので、動作自体には関係ありません。


この写真が、前者のllSetTargetで座る位置を指定した椅子に座ったときの物で、


こちらが、後者のllSetLinkPrimitiveParamsで、座る位置を指定した時の写真になります。
すぐに分かるのですが、前者と後者で、座る位置の指定のZ座標が、およそ 0.35m異なっているのです。

座るアバターの大きさが、この差に関係しているのかと思ったので、自分のアバターを最大身長と、最小身長にして(うむ、元に戻すのが大変でしたw)確かめてみました。
その結果、大きくなったときと、小さくなったときで、座る位置に非調整が必要(大きくなったときは設定位置を上へ、小さくなったときは設定位置を下へ)という事が分かりましたが、2つのスクリプトの間にずれの差は認められませんでした。

もしかすると、座るアニメーションに関係するのかなと思って、上のスクリプトを以下のように改造して試してみましたが

   1: vector  GSitPos = <0.3, 0.0, 0.56>;
2: vector GSitRot = <0.0, 0.0, 0.0>;
3: key GSitted;
4: string GAnimationName = "stand";
5:
6: stopAllAnimation(key id)
7: {
8: list anms = llGetAnimationList(id);
9: integer i;
10: for (i = 0; i < llGetListLength(anms); i++){
11: llStopAnimation(llList2Key(anms, i));
12: }
13: }
14:
15: default
16: {
17: state_entry() {
18: GSitted = NULL_KEY;
19: llSitTarget(GSitPos, llEuler2Rot(DEG_TO_RAD*GSitRot));
20: llSetLinkPrimitiveParams(2, [PRIM_POSITION, GSitPos]);
21: }
22: changed(integer change){
23: if (change & CHANGED_LINK) {
24: key nowSitted = llAvatarOnSitTarget();
25: if (GSitted != NULL_KEY) {
26: if (nowSitted == NULL_KEY) {
27: llStopAnimation(GAnimationName);
28: GSitted = NULL_KEY;
29: }
30: } else {
31: if (nowSitted != NULL_KEY) {
32: GSitted = nowSitted;
33: llRequestPermissions(GSitted, PERMISSION_TRIGGER_ANIMATION);
34: }
35: }
36: }
37: }
38: run_time_permissions(integer perm) {
39: if (perm & PERMISSION_TRIGGER_ANIMATION) {
40: stopAllAnimation(GSitted);
41: llStartAnimation(GAnimationName);
42: }
43: }
44: }

(もちろん、後者のスクリプトも、同様に改造して)2つを試してみましたが、差はありません。





この差が、何に起因するのかよく分かりませんが、仕様という奴なんでしょうか。

あと1つ不審点がありました。
1つは、2番目のllSetLinkPrimitiveParamsで座る位置を指定するスクリプトの場合、llSitTargetを入れないと、座れないことがあります。これが怪しいところなのですが、椅子を移動したり回転させると座れなくなるような気がしますが、再現性がない。実装がわからないと、これ以上突っ込みようがないので、こんなもんだと理解するしか方法はないようです。

2009年7月21日火曜日

思惑

この10日は結構忙しかったので、ここの更新が出来ませんでした。
ある人から、以下の記事を教えてもらって、今日学生に実習をさせながら眺めていたのですが、とてもおもしろい。

あのセカンドライフは今どうなってる? 潜入取材で実際に見てきました

この記事なのですが、発言が95もあるので、ある程度統計情報になるかな、と思ってちょっと分析をしました。もちろん、同じ人間が何度も書き込んでいるでしょうから、世論?をこのデータから推し量ることはまったく出来ませんが、とてもおもしろい。



以下はその分析の結果なのですが、セカンドライフを擁護する発言に+1、逆にセカンドライフなんてだめじゃーんって発言を-1、どちらでもない発言を0として、発言順(横軸)に累計を取った物です。もちろんここの発言が、セカンドライフを擁護しているものなのかどうかは、わたしの個人的な判断なので、的外れな物もあると思います。

このグラフで、はじめはセカンドライフ擁護派の発言が続き、1/3あたりから「セカンドライフってだめじゃーん」って発言が増えてきて、2/3あたりからまたセカンドライフ擁護派の発言が増えている。

これが全体的な傾向なのですが、おもしろいなーと思ったのが、セカンドライフってだめじゃーんって発言が出始めている時期に、

「セカンドライフユーザのアホどもが必死だなw」

という発言があることです。それに続いて

「長文書いている人は某広告代理店の方々ですか?」

という発言があり、時折根拠のない「セカンドライフってだめじゃーん」って言う発言が出てくることです。もちろん説得力を持った「セカンドライフってだめじゃーん」って発言が多く、議論が成立している場面もありましたが、根拠なく「セカンドライフってだめじゃーん」も時折、見受けられるのです。

この議論をする訳でもなく、「セカンドライフってだめじゃーん」というステロタイプな発言をしている人間の意図というか、何を考えているのかを想像すると、とてもおもしろい。

最近のネットを見ていて感じることに、他人を貶めその反応を見て楽しむという文化があることに気づいていましたが、なるほどなという感じです。この発言をした人間は、一方的に「セカンドライフってだめじゃーん」と決めつけ、それに反駁する人の発言を誘い出し、それをせせら笑うことを目的とした発言なのだと思うのです。ふむ。
人が必死になっている事を見るのが、滑稽とは私は思わないのですが、似たようなことは高校時代にあったなぁと、思い出したことがありました。それは、自分のうちに帰ると、まあそこそこ勉強しているのですが、友人の前では、まったく勉強していないように装うのが伊達と思っていたことです。
このような風潮は、文化の爛熟期には必ずと言って良いほど発生すると、私は根拠なく思っているのですが、なるほどと思いました。そしておもしろいw。

このような、人を貶め、その人が右往左往するのを見て楽しむというのを、粋だと思うことは、決して異常なことではない。むしろ、そのステロタイプさに、ほほえましくもあります。
きっとそのような人が、これを読むと「上から目線が気に入らん」という反応がでることも当然なのですが、それはしょうがない。私の方が齢を重ねているのだからw。

まあ、それは置いておいて、このような発言があることに、おもしろさを感じるのは歳のせいだろうかと、帰りの電車の中で真剣に考えてしまった自分が情けない。

次回は、まともな記事を書こうと思います。

2009年7月13日月曜日

プロジェクター

昨夜はうちの学生を全員招いてミーティングをしました。

そのための準備がとても忙しく、遊んでいる暇はなかったのですが、どうにかホールも完成しました。ホールには、うちのサーバにアップした画像を連続して映写できる、プロジェクター(というより、スクリーンです)も備え付けました。

以下がサーバ側のCGIプログラムのソースになります。
/*
** projector.c
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/types.h>
#include <dirent.h>
#include <time.h>
 
int main(int argc, char* argv[])
{
char filename[BUFSIZ];
int imgNum = 0;
 
memset(filename, (char) 0x00, sizeof(filename));
 
printf("Content-type: text/html\n\n");
printf("<!DOCTYPE html PUBLIC \"-//w3c//dtd html 4.0 transitional//en\">\n");
printf("<html>\n");
printf("<head>\n");
printf("<title>secondlife view test</title>\n");
printf("</head>\n");
printf("<body bgcolor=\"#ffffcc\">\n");
printf("<center>\n");
if (2 < argc) {
strncpy(filename, argv[1], BUFSIZ - 1);
imgNum = atoi(argv[2]);
printf("<img alt=\"testImage\" src=\"/projecter/%s/img%03d.png\" "
"height=\"100%%\" width=\"100%%\">\n", filename, imgNum);
} else {
printf("ERROR: %s, %d\n", filename, imgNum);
}
printf("</center>\n");
printf("</body>\n");
printf("</html>\n");
 
return 0;
}

すみません。ソースの色付けにLSL用の物を使ってしまいました
それから、これ見て試そうという人はそれなりのプロでしょうからあんまり人に見せられた品質のプログラムじゃないです。
そして、目一杯のセキュリティー・ホールがあります



このCGIプログラムを、セカンドライフ側から叩けば、していたディレクトリに格納されているimgxxx.png(xxxは数字)を表示するWebページを戻します。それをセカンドライフ側から表示してやれば、うちのサーバにある画像をセカンドライフ内で表示することができます。

試して見たところ結果は比較的良好で、問題はセカンドライフ側のスクリーンに仕込むスクリプトに焦点が絞られそうです。

2009年7月7日火曜日

なぜタッチ?

素朴な疑問

椅子に座りタッチでアニメーションが切り替わる物が多いようですが、なぜ、タッチなんでしょ?
椅子にすわっているときってチャットしている事が多いので、手はキーボード上にあるのが普通です。ですので、いちいちマウスに手を伸ばしてタッチするより、キーボードから切り替えられた方が、楽に思えるのですが。

調べて見たところ、↑、↓、PageUp、PageDown などキー押下が取れると思うのですが。

そのキーには何かアサインされているのが普通なのかなぁ?
それとも、皆さん方は、椅子にすわっても、マウスに手が行っているのが普通なのかなぁ?
はたまた、座っている椅子にタッチでアニメ切り替えが、ディファクト・スタンダードの操作になっちゃっているのかな?

この辺は経験がないので良く分からん。

2009年7月6日月曜日

ヘタレなショーの始まり

GOTTiさんから、思わぬ(うれしい)申し出を受けました。

Scripters' Cafe で、定期的に(漫才)ショーをしないかとのお誘いです。基本的に人の前で話すことは嫌いではない、人前で話すことが苦痛ならば教員という商売はかなり苦痛になります。
ただ話を聞かれる方々が問題で、その方々に楽しんでいただける話ができるかが気になるところです。

現時点で用意できるネタとしては、

・ソフトウエアテストの話(ここの焼き直し)
・いくつかのソースのワタクシ的ツッコミ
・オープンソースのビジネスモデル
・(セカンドライフで)役に立つオープンソースソフトウエア

などがあるのですが、月に1回でよいと聞きましたので何とかこなしてゆけるでしょう。
この話を、お受けした背景は、セカンドライフ内で実社会と同じような講演のテクニック使えるのか、それともまったく別なテクニックが必要なのか、という部分を検証をしてみたいという欲求が、むずむずと沸いて来たのが最大の理由です。
できれば、スクリプトが書ける人だけが楽しめる話ではなく、一般の人にも楽しんで頂ける話ができたらと思いますが、それは取らぬたぬきのなんとやら。努力目標としておきます。

2009年7月4日土曜日

ドアのスクリプトをいじくり倒す

む、Hidenoriさん、さすが、問題点をまとめて下さった

stateの分け方、使い所
寡聞にしてこのstateというもの、セカンドライフに来てはじめて知った制御構造なので、まだ、それほど経験がないため、細かい実装に関してはよく分からない部分があります。そこで一般論としての話になってしまいますが、みどりさんが書いているブログのこのあたりに一般論としての正解があるように思います。
原則的に、ステートチャート図を書いたときに遷移条件が、外的条件によって複数に分かれるもの(私は停留があると表現してます)はLSLでもステートになるでしょう。
問題は、ただ単に通過するステートの存在を許すか、許さないかになると思いますが、そのステートの中でのみ使われるイベントハンドラがある場合(例えば、初期化時にノートカードの読み込みを行うなど)は、十分にステートの候補になります。
また、逆に複数の契機によって同じイベントが発生する場合(例えば、dataserver(key queryid, string data)イベントは、ノートカードの読み込みだけではなく、他の関数の実行でも、イベントが発生します)も、ステートに分けてしまうことができるならば、分けてしまった方が良いと思います。
LSLはイベント・ドリブンな環境なのですから、そこに注目というか、それをよりどころにすれば良いと思います。
また、これは、実装者の個性が出る部分でもあります。

関数にどこまで入れるのか
ソフトウエア工学的には、モジュールの結合度は低く抑えるのが再利用という観点から見たときにはよろしい、ということになっています。私がソフトウエアタイマーの起動処理(llSetTimerEvent)を関数の外に出したのは、まさにモジュールの結合度を低めるためです。仮にこの関数を他のところで使い回す必要が出たときに、関数内部でソフトウエアタイマーを起動しているとすると、この関数を使うことができるステートには、必ずtimerイベントのハンドラがなきゃいけない(ここは突っ込みどころで、必ずしもtimerイベントがなくても動作すると思います)と言うことになれば、使い勝手が悪い。いや、経験がないので、悪そうだと考えたからなのです。
また、ドアを開けることと、時間がたてば自動的にドアを閉めることが、不可分の関係にあるならば、ドアを開ける関数内でソフトウエア・タイマーを起動しても良いでしょう。しかし、開けっ放しにしておく(おきたい)ドアもある得るということが、頭の片隅をよぎったため、分離するということの補強材料になりました。
かてて加えて、イベント・ドリブン型のプログラミング言語で、ソースコードの可読性を考えた場合、契機となるllSetTimerEvent関数の呼び出しと、その処理を行うtimerイベントハンドラが、近くにあることでよりわかりやすくなると信じてもいます。

初期rotationをいつ保存するか
これは、初期rotationを保存する目的に関わる問題です。初期rotationを保存する目的は、ドアが閉じているときに、ドアを開けた後、ドアを元に戻すために保存しているのですから、ドアを開ける直前の、ドアが閉じた状態を保存すべきです。これは自明のことで議論を待たないと思いますが、それがいつであるのかという部分は、LSLに対する知見の有無というか、誤解があると実装を失敗するように思います。

以上は、私がLSLに関する知識があまりないまま、一般論として書いた物ですので、LSLに詳しい方から、特にタイミング関連については、かなり突っ込みがあると思います。そのあたりのことの、フォローお願いします。

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)関数内)がかなりありますね。直すのがかったるいので、ごめんなさい。

あれ、今週ないのか

先週まであった、スクリプトの勉強会、今週はないのかな。
確か、今日はライトのスクリプトと言っていましたが、飽きちゃったんでしょうか?

2009年6月30日火曜日

ソースコードの品質

ソースコードの品質について考えてみました。

とりあえずソースコードの品質なので、そのソースコードから作られるオブジェクトの品質と切り離して考えることにします。

読みやすさ
いきなり、プログラムのライフサイクルに関わる話になってしまいますが、書いた、動いた、それで終わり、というパラダイムの製品という物は、最近は滅多になくなってしまいました。それはセカンドライフの世界でも同じでしょう。現代では「書いた、動いた、不具合出た」ということが普通ですし、うまく言っても「書いた、動いた、欲が出た」と改良されてゆくことが普通です。ですから、不具合の原因を探るにも、また、機能を追加するにも、以前のソースが読みやすくなければ話になりません。読めないソースでは、不具合の原因を探し修正することも、また、機能を追加することも不可能です。

それでは、ソースの読みやすさ(可読性)とは何でしょうか。

統一されたインデント
K&Rスタイルでも、オールマンスタイル構いません。非常識でないスタイル(常識的なインデントはここ)全ソース内で統一したインデントスタイルが使われている必要があります。正しくインデントが取られたソースは、制御構造の見通しがとても良くなります。

わかりやすい変数名
ソースコード中でaやb、zなどという変数名が使われていると、その中に格納されているデータの内容を把握することが困難になります。逆に言うとその中に格納されているデータを的確に表す変数名が使われているソースコードは、処理内容がとても把握しやすくなります。
またネームスペース(ここでは簡単にいうと変数が利用できる範囲)を意識した変数命名規則、例えばグローバル変数(私個人の言い方として Perpetuity Variables もしくは Permanent Variables)の先頭の1文字目は大文字で書く、逆にローカル変数(私個人の言い方として Temporary Variables)は先頭文字は小文字にする、ループカウンタは i, j , k の順で使うなど統一したルールが存在しているソースコードは、不具合の発見がとてもしやすくなります。

ブロック化(処理のドキュメント化)
例えば、2つの整数変数aとbの内容を交換する時に、
int temp = a;
a = b;
b = temp;
と書くよりも、引き数付き置換マクロ
#define XSWAP(a, b, type) {type temp = a; a = b; b = t;}
を定義して、
XSWAP(a, b, int);
と書いた方が、そこで何をしているのか直感的にわかりやすくなります。プログラミングの初等教科書等にサブルーチン(関数)のことを、「良く似た同じ処理をまとめ、プログラムサイズを小さくするために使われる」と書かれていることが多いが、私は1回しか使わない処理でも、ソースのドキュメント性をあげるために、あえて関数化することがあります。

適切なコメント
コメントの付き過ぎたソースは逆に見にくくなるが、適切なコメントをつけることで、プログラムがより読みやすくなります。
例えば、最近知ったLSLのブードゥーマジック(最近は有害になっていると聞くけど)を使ったソースでは
a = (a=[]) + a + [llDetectedKey(0)]; // a += [llDetectedKey(0)];
と書くことで、より意味がわかりやすくなります。

これらの可読性の着目点に沿って、ソースコードを採点した場合、

高 ・教育に使用するソース
↑ ・人に見せることを前提として書かれたソース
↑ ・人に見せる可能性のあるソース
↑ ・人には見せないけど、再利用する可能性があるプログラムのソース(実験レベル)
低 ・人に見せず、かつ再利用もしない一時的使用するカスプログラムのソース

という順で高い水準の可読性が要求されるのが、あるべき姿でしょう。あえて言えば、カスプログラムのソースはどうでも良いんです。

また、ソースコードの主役はアルゴリズムであって、書式はアルゴリズムを引き立てる脇役、目立ってはいけない(読み手に違和感を抱かせてはいけない)ということも、意識すべきでしょう。
(しかし、まれではありますが、カスプログラムのソースが、教育目的で出回っちゃうこともあるのですが。持って瞑目すべし)

2009年6月28日日曜日

全く前の話とは関係ない話

時差を計算する関数、というか時刻を正規化する関数がLSLにないので書いてみました。この程度なら、LSLの知識がほとんどなくても書けます。

   1: integer JST = 9;
2: integer PDT = -7;
3:
4: integer FORMAT0 = 0;
5: integer FORMAT1 = 1;
6: integer FORMAT2 = 2;
7:
8: integer YEAR = 0;
9: integer MONTH = 5;
10: integer DAY = 8;
11: integer HOUR = 11;
12: integer MIN = 14;
13: integer SEC = 17;
14:
15: integer getDateTimeElement(string dateTimeString, integer startPos, integer length)
16: {
17: return (integer) llGetSubString(dateTimeString,
18: startPos, startPos + length - 1);
19: }
20:
21: string getTimeString1(
22: integer year, integer month, integer day,
23: integer hour, integer min, integer sec,
24: string timeZone)
25: {
26: return getTimeString3(year, month, day, hour, min, sec, "") +
27: llGetSubString("0" + (string) sec, -2, -1) + ".000000" + timeZone;
28: }
29:
30: string getTimeString2(
31: integer year, integer month,
32: integer day, integer hour, integer min, integer sec,
33: string timeZone)
34: {
35: return getTimeString3(year, month, day, hour, min, sec, "") + ":" +
36: llGetSubString("0" + (string) sec, -2, -1) + timeZone;
37: }
38:
39: string getTimeString3(
40: integer year, integer month, integer day,
41: integer hour, integer min, integer sec,
42: string timeZone)
43: {
44: return (string) year + "-" +
45: llGetSubString("0" + (string) month, -2, -1) + "-" +
46: llGetSubString("0" + (string) day, -2, -1) + " " +
47: llGetSubString("0" + (string) hour, -2, -1) + ":" +
48: llGetSubString("0" + (string) min, -2, -1) + timeZone;
49: }
50:
51: integer isLeap(integer year)
52: {
53: return (year % 400) == 0 || ((year % 4) == 0 && (year % 100) != 0);
54: }
55:
56: integer getMaxDay(integer year, integer month)
57: {
58: integer result = 30;
59:
60: if (month == 1 || month == 3 || month == 5 || month == 7 ||
61: month == 8 || month == 10 || month == 12) {
62: result = 31;
63: } else if (month == 2) {
64: result = 28 + isLeap(year);
65: }
66: return result;
67: }
68:
69: integer isChangeMonth(integer year, integer month, integer day)
70: {
71: integer result = 0;
72:
73: if (day < 1) {
74: result = -1;
75: } else if (getMaxDay(year, month) < day) {
76: result = 1;
77: }
78: return result;
79: }
80:
81: string normalize(string dateTimeString, integer timeDifference,
82: integer flag, string timeZone)
83: {
84: string result;
85: integer correction = 0;
86: integer year = getDateTimeElement(dateTimeString, YEAR, 4);
87: integer month = getDateTimeElement(dateTimeString, MONTH, 2);
88: integer day = getDateTimeElement(dateTimeString, DAY, 2);
89: integer hour = getDateTimeElement(dateTimeString, HOUR, 2);
90: integer min = getDateTimeElement(dateTimeString, MIN, 2);
91: integer sec = getDateTimeElement(dateTimeString, SEC, 2);
92:
93: // 時刻の補正
94: hour += timeDifference;
95: correction = 0;
96: while (hour < 0 || 24 <= hour) {
97: if (hour < 0) {
98: hour += 24;
99: correction--;
100: } else {
101: hour -= 24;
102: correction++;
103: }
104: }
105:
106: // 年・月・日の補正
107: day += correction;
108: correction = isChangeMonth(year, month, day);
109: while (correction != 0) {
110: month += correction;
111: if (month < 1) {
112: year--;
113: month = 12;
114: day = 31;
115: } else if (12 < month) {
116: year++;
117: month = 1;
118: day = 1;
119: } else {
120: if (correction < 0) {
121: day = getMaxDay(year, month);
122: } else {
123: day = 1;
124: }
125: }
126: correction = isChangeMonth(year, month, day);
127: }
128:
129: if (flag == FORMAT0) {
130: result = getTimeString1(year, month, day, hour, min, sec, timeZone);
131: } else if (flag == FORMAT1) {
132: result = getTimeString2(year, month, day, hour, min, sec, timeZone);
133: } else {
134: result = getTimeString3(year, month, day, hour, min, sec, timeZone);
135: }
136: return result;
137: }

これ、年甲斐もなく、結構悩みました。ここを見てみると、とてもちぐはぐな感じが否めません。
システム内で、LSLで日時を扱う標準形式がなんなのかよく分からないのです。「integer llGetUnixTime( );」で求まる整数値(1970/01/01 00:00からの経過秒数)が一番シンプルなのですが、文字列形式と、この経過秒数の変換方法が全く提供されていません。で、考えることしばし。

結局、「string llGetTimestamp( );」で得られる、"YYYY-MM-DDThh:mm:ss.ff..fZ"形式を使ってしまいました。ちぐはぐというか、文字列を標準形式にするのとても気持ち悪いです。これW3Cで決まっているある程度権威のあるフォーマットなのですが、LSLの文字列処理機能の貧弱さ(この貧弱さというのは無視して下さい。筆者はC言語以外の言語で、文字列の取り扱いをするのが大嫌いなので)を考えると、いやだなぁと思ってしまいます。

ということで、この関数
string localTime = normalize(llGetTimestamp(), JST, FORMAT3, "");
なんて使うと、現在の日本時間が得られます。JST を PDT に変えればリンデン時間が得られます。
また、、"YYYY-MM-DDThh:mm:ss.ff..fZONE"形式で時刻をセットして JSTを JST - PDT にすれば、リンデン時間を日本時間に、逆に PDT - JST をセットすれば日本時間をリンデン時間にも変換できます。

なんでこんな物を作ったのかというと、公式ページなんかでイベントをリンデン時間で書いてあるのを日本時間に、頭の中で変換して読むのがめんどくさかったので、時刻変換時計を作りました。これは、その副産物です。
そうそう、リンデン時間は米国西岸時間なので、サマータイムの事も考えなきゃいけないんでしたが、それはそれ、これはこれということで。