defsubとgetparam

defsub

defsubは、新命令を自作するためのdefine節専用の命令で、defsubで作られた命令は、同名のラベルで始まるサブルーチンによって実装される。

defsub sample_func ; sample_func命令を作る。

と宣言したのならば、

; これがsample_func命令の実体
*sample_func
return ; 何もしないで戻る。

最低限これだけの記述が必要になると言うこと。
以降、start節中でsample_funcと記述する度に、ラベル*sample_funcにgosubする。gosubとの違いは、sample_func後に引数を記述できる点。gosubでこれはできない。この引数の活用方法を次に述べる。

getparam

getparamは、defsubによって作られた命令の引数を、サブルーチン側で受け取る命令である。
引数は「ひきすう」と読み、サブルーチンや命令を動かすのに必要なパラメータのことである。

*define
defsub sample_func
game
*sample_func
getparam %100 ; 引数として数値変数を一つとり、その内容を%100に代入する。
; やってることは引数の内容を表示するだけなんだが。
%100
return
*start
rnd2 %0,1,100
sample_func %0
@
end

引数の種類

引数とgetparamで指定された変数は、数値変数なら数値変数、文字列変数なら文字列変数に一致されなければならない。

*define
defsub sample_func
game
*sample_func
getparam %100
%100
return
*start
mov $0,"文字列"
sample_func $0 ; 数値を期待しているところに文字列を入れてみる。
@
end

このようにすると、当然ながら構文エラー「数字がくるべきところに数字がありません」が出る。
逆に、文字列変数を期待しているところに数値を入れると、構文エラー「文字列がくるべきところに文字列がありません」が出る。

引数の数

サブルーチン側のgetparamで設定した引数の数と、呼び出した側で書いた引数の数が違えば、当然まともに動作しなくなるのだが、引数の数が多いか少ないかで動作が違うので注意したい。
まずは、引数の数が少ない場合(サブルーチンは二つの引数を要求しているのに、呼び出す時に一つの引数しか書かなかった場合)。

*define
defsub sample_func
game ; start節開始
*sample_func ; sample_funcの実体
getparam %100,%101
return
*start ;
sample_func 20 ; sample_funcを呼び出す。
end

8行目で構文エラーが出て「コンマが必要なところにコンマが有りません」と言うメッセージを送ってくる。このエラーメッセージが出た時は「引数の数が足りません」と言う意味だと脳内で翻訳すること。
getparamの部分でエラーが出る訳ではないので、どこで間違いを起こしたのか非常にわかりやすい。
*1
逆に、引数の数が多い場合(サブルーチンは二つの引数を要求しているのに、呼び出す時に三つの引数を書いてしまった場合)を想定してみる。

*define
defsub sample_func
game ; start節開始
*sample_func ; sample_funcの実体
getparam %100,%101
return
*start ;
sample_func 20,20,20 ; sample_funcを呼び出す。
@
end

と、この場合、何故かエラーは出ずに「,20 ; samplefuncを呼び出す。」と画面に表示されてしまう。これは余分な引数以降が表示文と解釈されてしまうから起きる現象だ。
このエラーはエラーとして検出されない。画面にゴミが表示される場合、引数を多く書きすぎたことを疑ってスクリプトを検証した方がよいと言うことだ。ゴミの内容を見てどの部分か類推できるだろう。

組み込み関数の引数

getparamを使用した自作の命令の場合、必要な引数の数は固定である。
一部の(lspなど)命令では、引数の数がある程度の範囲で変更できるが、これを自作命令で実現する方法は今のところない。*2

結果を受け取る

多くの命令では結果を受け取る必要がある。大半の数学関数などがそうだ。
従来の多くのプログラム言語では、「返り値を受け取る」などと呼ばれる行為だ。たとえばPerlである値のコサインを計算する場合、以下のようになる。

$cos = cos $radian; # $cosに$radianのコサインが入る。

同じことをNScriptで実現するには以下のようにする。

cos %cos,%degree ; %cosに%degreeのコサインが入る。

Perlではラディアンを使うが、小数点以下が使えないNScriptではディグリー(角度)を使う。同様に結果もまた実際の1000倍の値になる。
引数の中に結果を受け取る変数を入れ込まなければならないので、ダイクストラ原理主義者(もしそんなのがいたとしたら)からは、「直観的ではない」とか「関数はすべからく返り値を持たなければならないのに」とか非難を受ける可能性があるが、普通にNScripterやってる人間にとっては「これはそういう仕様なんだ」と思うだけである。
例えば、引数の二つの数値を掛け合わせて結果を返す新命令を考えてみる。(実際にはこんな命令は全く使う必要はないのだが)

*define
defsub my_mul
numalias my_mul_result,100
numalias my_mul_param1,101
numalias my_mul_param2,102
game
*my_mul
getparam i%my_mul_result,%my_mul_param1,%my_mul_param2
mov %%my_mul_result,%my_mul_param1
mul %%my_mul_result,%my_mul_param2
return
*start
my_mul %0,200,300 ; 200と300を掛け合わせた結果を%0に入れる。
%0@
end

他の組み込み命令を見てもらえればわかるが、NScripterでは結果を受け取る変数を引数の先頭に置く。ルール上はどこに置いても構わない訳だが、既存の命令と違うことやっても混乱するだけなので、なるべく同じようにするべきだ。特にライブラリーなどと言って公開する場合には、だ。
なお、既存の命令でも違う場合もある。多くは複数の結果を受け取る変数が必要な命令で、例としては「getspsize(スプライトのサイズを取得する)」などである。これなどは第一引数がスプライト番号で、それ以下が全て結果を格納するための変数となる。
getparamを使って、結果を受け取る変数の番号を取得するには、変数の頭にiをくっつける。引数に文字列変数を期待するならば、sをくっつける。そうすると、変数の番号がその数値変数に入るので、後は%%my_mul_resultのようにアクセスするだけである。
なお、dimでも触れたが、getparamな配列変数とは相性が悪い。

既存の命令の上書き

defsubは自作するだけではなく、最初からある組み込み関数を上書き(オーバーロード)することにも使える。
たとえば、古いバージョンのNScripterでは、0除算(コンピュータでやってはいけないことの一つ。ある数値を0で割ろうとする)は何もメッセージを出さずにNScripter自体が落ちていた(現在のバージョンではエラーメッセージが出る)。これを検出しようとするならば、defsubでdivを上書きして使うことができる。

*define
defsub div
game
*div
getparam i%100,%101
if %101=0 mesbox "数値を0で割ろうとしました。","0除算エラー":end
_div %%100,%101
return
*start
mov %0,100
div %0,0
%0
@
end

上書きした後、本来の命令を使おうと思ったら、命令名の前に"_"(アンダーバー)を追加すればいい。

組み込み関数の上書き例その2

bgを上書きして、ファイルのパスを表示する動作を付け加えた。
メッセージボックスの部分をコメントアウトすれば何も起きなくなる。その場合でも、$bg_fileにアクセスすることで現在の背景画像を取得できるメリットがある。

defsub bg ; bg 命令を上書き
numalias bg_file,100 ; 適当に変更
*bg
getparam $bg_file,%bg_file
mesbox $bg_file,"今設定された背景の画像は、これです。"
_bg $bg_gile,%bg_file ; アンダーバーをつけて本来のbgを実行。
return

引数はgetparamで受けた後、そのまま元命令に渡す処理を自分で書く必要がある。

*1:なお、蛇足ながらこのエラーメッセージを送るなら「カンマが必要なところにカンマが有りません」ではないのかと思ったりもする。コンマっつーたら「.」こっちだろ。まあ、何か深遠な理由があるのかも知れないが。本来のターゲット層であるプログラム素人さんに対して「カンマ」言うても通じないからとか。

*2:nslua.dllとluasubを使用することでこの問題はクリアーできる。2009年12月1日追記