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 をセットすれば日本時間をリンデン時間にも変換できます。

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

何がやりたいのか

こんにちは、小菅です。

あることで私のセカンドライフ利用の目的を整理する必要ができましたので、今日はそれをまとめたいと思います。

これまでセカンドライフの世界を様々に見たり、試したりしてきましたが、その結果、ある程度使えるのかなという感触を得ることができました。これまで、コンピュータ・グラフィックなどのコンテンツ系分野や語学教育分野では、それなりの実績を残してきているセカンドライフですが、私が考えているのは、IT教育関連でのセカンドライフの利用になります。
これまでのところ、私がやってみたいと思い、かつ、セカンドライフ内で可能と考えている教育は以下のような物です。

・飛行船の制御
わたしは、これまで、「財団法人 コンピュータ教育開発センターの事業であるOpen School Platform(OSP)に技術面で参加してきました。
その中の(高等学校での)事例なのですが、飛行船の制御実習というものがあります。この実習は、実際に飛行船を飛ばしてその制御の体験を行う実習です。
最近の学生さんは、身近にPCがあるためPC内で帰結する物に対してはあまり関心を示しませんが、このようなハードウエアを直接コントロールすることをとても珍しがり、教材としてはとても食いつきが良いように思います。
ですが、この実習は飛行船本体を輸入しなければならず、また維持管理するためにも高価なヘリウムガスを使用します。そのため、いつどこでも、気軽に行える物ではありません。
そこで、これをセカンドライフ内で行うことができたら、低予算でどこでも行うことができると考えました。セカンドライフは18歳以上の世界とのとこで、先ほどのWebページは高等学校の事例なのです。ここが誤解されやすいところですが、18歳以上の工学教育でも、これが利用できると考えています。
これはプロトタイプの作成は完成しています。

・プログラミング(アルゴリズム)の基礎教育
わたしは、これまで、プログラミングの導入教育として、子供とゴリラ(ソフトウエア本体指導書)、Nuclear Rorobt Kaffir(ソフトウエア本体概要-お友達のブログです)などを作ってきましたが、それをセカンドライフ内で実現することでより、没入感のある教材にすることができると考えています。
これも、お友達の協力によって、ほぼ完成品ができています。

・ソフトウエア・テストの実践教育
本学ではソフトウエア・テストを通じて、IT関連製品・システムの品質管理技術者を養成する学科があります。そしてセカンドライフ内では、さまざまなスクリプト製品がありますので、その製品のテストを行うことで、品質管理技術者の初等実践教育がおこなえると考えています。

・その他
本学は、工業系専修学校のデパートと呼ばれるほどの学科があります。それらの学科の中で、卒業発表を行う学科が多いのですが、その発表をセカンドライフ内で行うことができる学科がいくつかあり、話をしたところ興味を示している学科があります。

これから、この目的を実現するためのトピックスがありましたら、ここで公開してゆきたいと思っています。

2009年6月25日木曜日

振子の研究

前回インデントについての私の考えを書きました。今回あるソースを入手しましたので、それを紹介します。
ソースの表示はインデントの様子も良く分るように、Hidenoriさんのブログに紹介されていた、LSLコード色づけをしました。

integer sw=1;
rotation rot;
integer n = 5;
 
default
{
state_entry()
{ // ここはオールマン記法
rot=llGetRot(); // タブ幅が4+3
}
 
touch_start(integer num)
{ // ここもオールマン記法
if(sw==1){ sw=0; // なんで{の後に文が続く? + ここはK&R記法 + タブ幅が4+3
llSetTimerEvent(3.0); // タブ幅が4+3+2 だんだん小さくなるんですね
}else if(sw==0){ sw=1; // なんで{の後に文が続く? + ここはK&R記法
llSetTimerEvent(0.0); // タブ幅が4+3+2 だんだん小さくなるんですね
llTargetOmega(<0,0,0>,0.0,3); // タブ幅が4+3+2 だんだん小さくなるんですね
llSetRot(rot); // タブ幅が4+3+2 だんだん小さくなるんですね
} // タブ幅が4+3
}
 
timer(){ // ここはK&R記法
llTargetOmega(<n,0,0>*rot,0.1,3); // 一転タブ幅が4+5
llSleep(1.5); // 一転タブ幅が4+5
n = -n; // 一転タブ幅が4+5
llTargetOmega(<n,0,0>*rot,0.1,3); // 一転タブ幅が4+5
n = n; // 一転タブ幅が4+5
} // タブ幅3・・・もしかして上は4+5と思っていたけど違うのか?
 
on_rez(integer param) // タブ幅3
{ // ここはオールマン記法 // タブ幅3
llResetScript(); // 一転タブ幅が3+5
} // タブ幅3
}


元のソースコードには一切のコメントがなかったため、以下は、小菅がインデントに関してのコメントをつけました。
この中で私が一番問題と思ったのが、「if(sw==1){ sw=0;」と、条件判断と同じ行に実行文が記述してあるところです。これ、学生にもいつも口を酸っぱくして言っていますが、ソースコードを読むときに常に気になるのが視線の動きです。
人間の目(視線)は、まず左上から、右方向、そして下方向に流れます。それが現代日本で教育を受けた人間の常識です(きっとアラビア語圏で教育を受ければ、右上から右へ、次いで下へとなるのかな)。
プログラマはそれまでの経験からプログラムが下に続いていることを知っているため、上から下にソースコードを読む癖がついているのが普通です。このように、if (条件) { の後に何かが続くことがあるとすれば、それは通常コメントです。そのような場所に、実行文を書かれてしまうと、うっかり読み落とす危険があります。
それにコードが読みにくいと、こんどは論理を追えなくなってくる。プログラマがソースコードを読む目的はプログラムの論理を追うためです。ですので、それを邪魔するすべての要素はすべて取り除いて、書式に則ったソースコードを書くことが、まず最低の必要条件だと学生には教えています。

そこで、以下では、私が使用しているK&R記法に則って、このソースコードを書き換えました。

  1: integer     sw = 1;
2: rotation rot;
3: integer n = 5;
4:
5: default
6: {
7: state_entry() {
8: rot = llGetRot();
9: }
10: touch_start(integer touched_num) {
11: if (sw == 1) {
12: sw = 0;
13: llSetTimerEvent(3.0);
14: } else if (sw == 0) {
15: sw = 1;
16: llSetTimerEvent(0.0);
17: llTargetOmega(<0, 0, 0>, 0.0, 3);
18: llSetRot(rot);
19: }
20: }
21:
22: timer() {
23: llTargetOmega(<n, 0, 0>*rot, 0.1, 3);
24: llSleep(1.5);
25: n = -n;
26: llTargetOmega(<n, 0, 0>*rot, 0.1, 3);
27: n = n;
28: }
29: on_rez(integer param) {
30: llResetScript();
31: }
32: }


ふむ。このソースを見ていただければ分かるでしょうがこれは「やじろべい」のスクリプトだそうです。
そしてこのソースにある問題は、インデントやプログラム書式だけではない。以下に箇条書きで気づいた点を書きました。

・2、3行目: グローバル変数として、rot、n などというどこでも使われる可能性がある名前は不適切です。良くある使い方としては、変数名をすべて大文字にしてしまう、変数名の先頭をGなどで始めるなどの工夫が必要です。
・8行目: state_entry中でrotに現在の回転値を保存しているが、ここを通過するのはスクリプトが初期化された時のみです。「やじろべい」を出してから、角度を変えても反映されません。rotに現在の回転値を保存するのを、14〜16行目付近で行えばこの問題は改善します。
・11、12行目他: sw変数の値に0および1と比較または代入しているが、BOOLEANの代わりにintegerを使用しているのですからTRUEやFALSEを使うべきです。
・17行目他: llTargetOmega関数の第1引数の型はvectorですが、その各要素はfloatなので、<0,0,0>ではなく<0.0, 0.0, 0.0>と書くべきです。
・17行目他: llTargetOmega関数の第3引数は、float gain ですが、3(この値自身根拠不明)とinteger になっています。3.0と書くべきです。
・23行目他: 上にも関連しますが、vector型の各要素は、float です。integer の n を指定するのはおかしい。というよりも、ここでは、n を float として宣言すべきです。そしてそれならば、n という変数名は不適切です。
・23、26行目: llTargetOmega関数の第1引数の型は回転の軸を表すベクトルが正規化されていない。実験してみると、非物理オブジェクトに対して、llTargetOmega(vector axis, float spinrate, float gain)関数で、ビュワー側回転を与えると、回転の角速度は llVecMag(axis)*spinrate で決定されるように様に見える・・・要は「llTargetOmega(<0.0, 0.0, 2.0>, PI_BY_TWO, 1.0)」と「llTargetOmega(<0.0, 0.0, 1.0>, PI, 1.0)」は同じ回転を与える・・・しかし、axisは単位ベクトルにし、spinrate で角速度を与える方が直感的です。
・23、26行目: 逆向きの回転を作るのに、回転の軸を表すベクトルの向きを逆にしているが、前述のように第2引数のspinRateをにした方が直感的に何をしているのかわかりやすい。
・25、26行目: 回転軸のベクトルの向きを逆にするだけならば、26行目は「llTargetOmega(<-n, 0, 0>*rot, 0.1, 3); 」で良いはず。わざわざnの符号を反転させる必要はない。しかも、前にも述べたように、回転の向きを逆転させるのであれば、回転軸を操作するのではなく、角速度を逆にすべき。
・27行目: 「n = n;」脳みそ解けていませんか? 大丈夫ですか? この部分について、説明を求めたところ、25行目でnの符号を反転させたのを元に戻しているというとのことで、後で分かるようになると説明があったが・・・。
・そもそも論ですが、やじろべいのように、同じ位置で動揺するものの動きをllTargetOmegaで作成するのはかなり無理があることが予想できます。なぜならばllTargetOmegaは非物理オブジェジェクトに対しては、完全にローカルであるビュワー側処理になります。サーバは回転の命令を出すだけですので、誰もが同じ状態を見ているとは限らない。そしてサーバ側からの回転命令が到着するタイミングが少しでもズレれば、変な位置で動揺することになるでしょう。事実、動揺の中心位置はずれまくりでした。

実はこのコードは実際に使われているコードではないとのことです。このコードはある勉強会のときにサンプルコードとしてもらったのですが、その時に我が耳を疑ったのが「このスクリプトを元に、各自工夫してスクリプトを作って見てください」という言葉です。
これは、まだ経験が浅い私が感じたことなので、まるで見当外れという可能性もあるのですが(もしそうならご指摘ください)、このソースをどのようにひねくり繰り返しても、事態が悪化するばかりで、何も得られるものはないでしょう。わたしは、このソースを部分的に書き換えて、少なくとも私が納得できるソースにして掲載することを考えましたが、スクラッチで書いてしまった方が、絶対に早いため断念しました。
以下がオリジナルを考慮してllTargetOmegaを利用してなめらかに、やじろべいが動作するように、スクラッチで書いたソースです。といっても動きすら変わってしまっていますが(苦笑。
実はこのソース、やじろべいの軸部分に仕込んでもうまく動作しません。それは見た目の位置と、実際の位置のずれを修正するために、定期的に一瞬物理属性を付与する処理を行っていますが、そのときにやじろべいの重心の位置と、編集位置の差にある関係が成り立っていないと、静的な安定性が得られないのが原因のように見えます。ただしこの問題については、感触程度の物で実際に検証した訳ではありません。

//
// YAJIROBEI
//
float GPeriod = 0.5; // Half Cycle
vector GRotAxis = <0.0, 0.0, 1.0>; // Rotation Axis
float GSRFactor = 16.0; // SpinRate Factor
 
// do not change under value
float GGravAccel = 9.80665; // gravitational acceleration
integer GCount = 0;
integer GFlag = FALSE;
float GSpinRate;
vector GNowRotAxis;
 
// いったんやじろべいの動きを止め、瞬間的に物理属性を付与することにより
// 実際の位置と見た目の位置を一致させる処理 これを周期的に実行する
// これは、midori Paulse さんに教えていただきました。
resetRotation()
{
llTargetOmega(ZERO_VECTOR, 0.0, 0.0);
llSetPrimitiveParams([PRIM_PHYSICS, TRUE, PRIM_PHYSICS, FALSE]);
}
 
default
{
on_rez(integer start_param) {
llResetScript();
}
state_entry() {
llTargetOmega(ZERO_VECTOR, 0.0, 0.0);
// 瞬間的に物理属性を与えたときに落ちないように上向きの力を付与
llSetForce(<0.0, 0.0, GGravAccel*llGetMass()>, FALSE);
GSpinRate = PI/GSRFactor;
}
 
touch_start(integer num_detected) {
if (llDetectedKey(0) == llGetOwner()) {
GFlag = !GFlag;
if (GFlag) {
GCount = 0;
GNowRotAxis = llVecNorm(GRotAxis*llGetRot());
llSetTimerEvent(0.1);
} else {
llSetTimerEvent(0.0);
resetRotation();
}
}
}
timer() {
llSetTimerEvent(0.0);
if ((GCount & 0x03) == 0) {
if ((GCount & 0x17) == 0) {
resetRotation();
}
llTargetOmega(GNowRotAxis, GSpinRate, 1.0);
} else if ((GCount & 0x03) == 1) {
llTargetOmega(GNowRotAxis, -GSpinRate, 1.0);
} else if ((GCount & 0x03) == 2) {
llTargetOmega(GNowRotAxis, -GSpinRate, 1.0);
} else if ((GCount & 0x03) == 3) {
llTargetOmega(GNowRotAxis, GSpinRate, 1.0);
}
GCount = ++GCount & 0xffff;
llSetTimerEvent(GPeriod);
}
}


このドキュメントは、このソースを書き、そして学習会のサンプルとして公開した人に対する個人攻撃と受け取られかねない内容です。今後トラブルの種になることも、予想できます。しかしあえてこのドキュメント公開した理由を以下で述べます。

個人がどのようなソースを、たとえそれが問題だらけで、正常に動作しないソースであったとしても、まったく構わないと思っています。また、そのようなソースをWebで公開したとしても、わたしが、この様なドキュメントを書くことはなかったと思います。しかし、これはスクリプト初心者に向けた勉強会でサンプルとして配布された物です。そのような目的としては不適当なサンプルソースであると考えざるおえません。

また、このログラムを書き、そして学習会のサンプルとして公開した人に「ダメだ、お前は」と言うつもりもありません。ただ、スクリプト初心者に向けた勉強会でサンプルとしては、前述のように不適切と考えられますので、今後の更なる研鑽をお願いしたい。ということです。
学習会には多くのスクリプト初心者が参加しています。その方達は真剣に「スクリプトがかけるようになりたい」と思って参加されているのだと思います。その方達に対して、真剣にスクリプトの勉強会に取り組んでくださるようお願いしたいということです。

もし、スクリプト勉強会の「先生」が、これをごらんになることがあれば、更なる研鑽をお願いしたいと思います。

2009年6月24日水曜日

スクリプトのインデント

1週間前の話で恐縮なのですが、セカンドライフで知り合った友達に教えてもらい、スクリプトの勉強会というのに行ってきました。
当日はメインのPCが、グラフィックカードの交換および、その後の作業のため使えず、出張時に使っているノートPCでログインしたため、周囲がいつまで経っても灰色のままだったので、初回の「ドアめぐりツアー」に付いてゆくのは大変でした。
ふむ、どうもモデレータの方は、ドアが大好きみたいで、盛んにドアがスクリプトの基礎だとおっしゃっていましたが、見せていただくドアは、良く見えなかったせいもあって、それがどうしたの? と言う機能しかなかったのが残念でした。最後には何やらスプラッタ系の物が用意されていたのですが、ほぼそれも見えずでした。

で、その終了時に、スクリプトを頂いたのですが、これが読めない。

読めない原因はインデントなのですが、このインデント・スタイル、K&Rスタイルとオールマン・スタイルの2つのスタイルが有名です。わたしは行きがかり上K&Rスタイルを使っていますが、セカンドライフの中では新規のスクリプトを作ると、オールマン・スタイルのスクリプトがテンプレートとして出てくるためか、オールマン・スタイルが多い様です。まあ、読む分には、どのようなインデント・スタイルでも一貫性があれば読めてしまうのですが、この頂いたスクリプトは、私にはダメでした。

これはWebの問題点なのですが、Web上で正しくインデント・スタイルを再現することがとても難しい、これが問題を悪化させているように思います。
話に聞くところによると、セカンドライフではスクリプトをコピペで使う人が多いようです。そのときに、この様なインデント不良(*1)なスクリプトをコピペで使用してしまうと、いざ改造しようとなったときに、プログラムの構造が分からないため、手の出しようがない。それが「スクリプトは難しい」というように思われてしまう原因の一つであるような気がします。

うむ、「インデントのダメなソースは、正しいインデントのソースを駆逐する」

こうならないように、気をつけたいものです。

(*1) わたしの作成した学生のプログラム課題提出システムでは、半自動でプログラムのインデントを検査して、ダメなら目視検査以前に「インデント不良」のフラグを立ててリジェクトしています。

2009年6月22日月曜日

セカンドライフ始めました

ある人の勧めでセカンドライフを始めて見ました。

これまであまりゲームなどやったことはなく(ドラゴンクエストは除く)、ましてオンラインゲームの類はセカンドライフが初めてだったので、かなり驚いてしまいました。

すごいなぁと感心したところ
・かなり現実的で、非現実に対しても没入感があること。あるところにお邪魔したときに、そこには人間だけではなく、ペンギンさんをはじめとする異形の人々(あえて背後に人間がいることを考えて人と)がいました(人間でも忍者さんとかもいました)が、まったく違和感がなかったです。しかも下手をすると現実の社会より、話がかみ合うかもしれない。夢に出てくる世界の様であり、妙に現実っぽくもありました。
・プログラムが、世界の成立に見える形で関わっている世界。現実の社会でも、コンピュータシステムは私たちの生活を支えているのですが、セカンドライフ内ではドアの開け閉めにもプログラム(以下、セカンドライフ風にスクリプトと記述)が必要になり、人々の目にスクリプトが触れることが多いため、現実の社会よりスクリプトを書く人への認知度が高いように思いました。現実社会で縁の下の力持ちをしているプログラマ・SEが主役とは言わないまでも、セカンドライフ内ではスクリプターは表舞台に立てる社会なのかなと思いました。
・物理現象がある程度有効な世界。移動に時間・費用などのコストがかからない、人が飛べる、落ちても死なない、など非現実な部分がある一方で、ある程度の物理法則が世界を支配しているようです。

なぁんだと思ったところ
・チャットでの入力が面倒くさそうだったので、ボイス・チャットに多少期待をしたのですが、文字チャットと違って記録に残らない(チャット会話中など資料のURLがポンポン飛び出すのでログは必須に思いました)、文字チャットでもそれほど不自由を感じない、などの理由でボイス・チャット、思ったほど有効ではありませんでした。

初めてから10日で感じた事ですが、全体としてセカンドライフは、ゲームというよりも、コミュニケーションのプラットフォームであるように思えました。